diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e3f5aef..2826d52c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,13 @@ CHANGELOG for FlatCAM beta
=================================================
+27.04.2020
+
+- finished the moving of all Tcl Shell stuff out of the FlatCAAMApp class to flatcamTools.ToolShell class
+- updated the requirements.txt file to request that the Shapely package needs to be at least version 1.7.0 as it is needed in the latest versions of FlatCAM beta
+- some TOOD cleanups
+- minor changes
+
25.04.2020
- ensured that on Graceful Exit (CTRL+ALT+X key combo) if using Progressive Plotting, the eventual residual plotted lines are deleted. This apply for Tool NCC and Tool Paint
diff --git a/FlatCAMApp.py b/FlatCAMApp.py
index 0cd6a882..74b75f6b 100644
--- a/FlatCAMApp.py
+++ b/FlatCAMApp.py
@@ -73,8 +73,6 @@ from vispy.io import write_png
from flatcamTools import *
-import tclCommands
-
import gettext
import FlatCAMTranslation as fcTranslate
import builtins
@@ -1173,6 +1171,15 @@ class App(QtCore.QObject):
# set FlatCAM units in the Status bar
self.set_screen_units(self.defaults['units'])
+ # #############################################################################
+ # ################################ AUTOSAVE SETUP #############################
+ # #############################################################################
+
+ self.block_autosave = False
+ self.autosave_timer = QtCore.QTimer(self)
+ self.save_project_auto_update()
+ self.autosave_timer.timeout.connect(self.save_project_auto)
+
# #############################################################################
# ######################## UPDATE PREFERENCES GUI FORMS #######################
# #############################################################################
@@ -1960,15 +1967,6 @@ class App(QtCore.QObject):
self.worker_task.connect(self.workers.add_task)
self.log.debug("Finished creating Workers crew.")
- # #############################################################################
- # ################################ AUTOSAVE SETUP #############################
- # #############################################################################
-
- self.block_autosave = False
- self.autosave_timer = QtCore.QTimer(self)
- self.save_project_auto_update()
- self.autosave_timer.timeout.connect(self.save_project_auto)
-
# #############################################################################
# ################################# Activity Monitor ##########################
# #############################################################################
@@ -2553,23 +2551,11 @@ class App(QtCore.QObject):
# ####################################################################################
# ####################### Shell SETUP ################################################
# ####################################################################################
- # this will hold the TCL instance
- self.tcl = None
- # the actual variable will be redeclared in setup_tcl()
- self.tcl_commands_storage = None
-
- self.init_tcl()
-
- self.shell = FCShell(self, version=self.version)
- self.shell._edit.set_model_data(self.myKeywords)
- self.shell.setWindowIcon(self.ui.app_icon)
- self.shell.setWindowTitle("FlatCAM Shell")
- self.shell.resize(*self.defaults["global_shell_shape"])
- self.shell._append_to_browser('in', "FlatCAM %s - " % self.version)
- self.shell.append_output('%s\n\n' % _("Type >help< to get started"))
+ self.shell = FCShell(app=self, version=self.version)
self.ui.shell_dock.setWidget(self.shell)
+ self.log.debug("TCL Shell has been initialized.")
# show TCL shell at start-up based on the Menu -? Edit -> Preferences setting.
if self.defaults["global_shell_at_startup"]:
@@ -4251,7 +4237,7 @@ class App(QtCore.QObject):
# Object creation/instantiation
obj = classdict[kind](name)
- obj.units = self.options["units"] # TODO: The constructor should look at defaults.
+ obj.units = self.options["units"]
# IMPORTANT
# The key names in defaults and options dictionary's are not random:
@@ -6128,9 +6114,10 @@ class App(QtCore.QObject):
def on_fullscreen(self, disable=False):
self.report_usage("on_fullscreen()")
+ flags = self.ui.windowFlags()
if self.toggle_fscreen is False and disable is False:
# self.ui.showFullScreen()
- self.ui.setWindowFlags(self.ui.windowFlags() | Qt.FramelessWindowHint)
+ self.ui.setWindowFlags(flags | Qt.FramelessWindowHint)
a = self.ui.geometry()
self.x_pos = a.x()
self.y_pos = a.y()
@@ -6158,7 +6145,7 @@ class App(QtCore.QObject):
self.ui.splitter_left.setVisible(False)
self.toggle_fscreen = True
elif self.toggle_fscreen is True or disable is True:
- self.ui.setWindowFlags(self.ui.windowFlags() & ~Qt.FramelessWindowHint)
+ self.ui.setWindowFlags(flags & ~Qt.FramelessWindowHint)
self.ui.setGeometry(self.x_pos, self.y_pos, self.width, self.height)
self.ui.showNormal()
self.restore_toolbar_view()
@@ -8312,7 +8299,7 @@ class App(QtCore.QObject):
:return: None
"""
if self.is_legacy is False:
- self.plotcanvas.update() # TODO: Need update canvas?
+ self.plotcanvas.update()
else:
self.plotcanvas.auto_adjust_axes()
@@ -8320,7 +8307,6 @@ class App(QtCore.QObject):
self.collection.update_view()
# self.inform.emit(_("Plots updated ..."))
- # TODO: Rework toolbar 'clear', 'replot' functions
def on_toolbar_replot(self):
"""
Callback for toolbar button. Re-plots all objects.
@@ -8372,7 +8358,6 @@ class App(QtCore.QObject):
def on_collection_updated(self, obj, state, old_name):
"""
Create a menu from the object loaded in the collection.
- TODO: should use the collection model to do this
:param obj: object that was changed (added, deleted, renamed)
:param state: what was done with the object. Can be: added, deleted, delete_all, renamed
@@ -8572,8 +8557,11 @@ class App(QtCore.QObject):
grid_toggle.triggered.connect(lambda: self.ui.grid_snap_btn.trigger())
def set_grid(self):
- self.ui.grid_gap_x_entry.setText(self.sender().text())
- self.ui.grid_gap_y_entry.setText(self.sender().text())
+ menu_action = self.sender()
+ assert isinstance(menu_action, QtWidgets.QAction), "Expected QAction got %s" % type(menu_action)
+
+ self.ui.grid_gap_x_entry.setText(menu_action.text())
+ self.ui.grid_gap_y_entry.setText(menu_action.text())
def on_grid_add(self):
# ## Current application units in lower Case
@@ -9324,7 +9312,8 @@ class App(QtCore.QObject):
"""
Returns the application to its startup state. This method is thread-safe.
- :return: None
+ :param cli: Boolean. If True this method was run from command line
+ :return: None
"""
self.report_usage("on_file_new")
@@ -9440,7 +9429,7 @@ class App(QtCore.QObject):
self.report_usage("obj_move()")
self.move_tool.run(toggle=False)
- def on_fileopengerber(self, signal: bool = None, name=None):
+ def on_fileopengerber(self, signal, name=None):
"""
File menu callback for opening a Gerber.
@@ -9487,7 +9476,7 @@ class App(QtCore.QObject):
if filename != '':
self.worker_task.emit({'fcn': self.open_gerber, 'params': [filename]})
- def on_fileopenexcellon(self, signal: bool = None, name=None):
+ def on_fileopenexcellon(self, signal, name=None):
"""
File menu callback for opening an Excellon file.
@@ -9524,7 +9513,7 @@ class App(QtCore.QObject):
if filename != '':
self.worker_task.emit({'fcn': self.open_excellon, 'params': [filename]})
- def on_fileopengcode(self, signal: bool = None, name=None):
+ def on_fileopengcode(self, signal, name=None):
"""
File menu call back for opening gcode.
@@ -9566,7 +9555,7 @@ class App(QtCore.QObject):
if filename != '':
self.worker_task.emit({'fcn': self.open_gcode, 'params': [filename, None, True]})
- def on_file_openproject(self, signal: bool = None):
+ def on_file_openproject(self, signal):
"""
File menu callback for opening a project.
@@ -9597,12 +9586,13 @@ class App(QtCore.QObject):
# thread safe. The new_project()
self.open_project(filename)
- def on_fileopenhpgl2(self, signal: bool = None, name=None):
+ def on_fileopenhpgl2(self, signal, name=None):
"""
File menu callback for opening a HPGL2.
- :param signal: required because clicking the entry will generate a checked signal which needs a container
- :return: None
+ :param signal: required because clicking the entry will generate a checked signal which needs a container
+ :param name:
+ :return: None
"""
self.report_usage("on_fileopenhpgl2")
@@ -9635,12 +9625,12 @@ class App(QtCore.QObject):
if filename != '':
self.worker_task.emit({'fcn': self.open_hpgl2, 'params': [filename]})
- def on_file_openconfig(self, signal: bool = None):
+ def on_file_openconfig(self, signal):
"""
File menu callback for opening a config file.
- :param signal: required because clicking the entry will generate a checked signal which needs a container
- :return: None
+ :param signal: required because clicking the entry will generate a checked signal which needs a container
+ :return: None
"""
self.report_usage("on_file_openconfig")
@@ -9726,8 +9716,7 @@ class App(QtCore.QObject):
image = _screenshot()
data = np.asarray(image)
if not data.ndim == 3 and data.shape[-1] in (3, 4):
- self.inform.emit('[[WARNING_NOTCL]] %s' %
- _('Data must be a 3D array with last dimension 3 or 4'))
+ self.inform.emit('[[WARNING_NOTCL]] %s' % _('Data must be a 3D array with last dimension 3 or 4'))
return
filter_ = "PNG File (*.png);;All Files (*.*)"
@@ -9770,8 +9759,7 @@ class App(QtCore.QObject):
# Check for more compatible types and add as required
if not isinstance(obj, FlatCAMGerber):
- self.inform.emit('[ERROR_NOTCL] %s' %
- _("Failed. Only Gerber objects can be saved as Gerber files..."))
+ self.inform.emit('[ERROR_NOTCL] %s' % _("Failed. Only Gerber objects can be saved as Gerber files..."))
return
name = self.collection.get_active().options["name"]
@@ -10392,7 +10380,6 @@ class App(QtCore.QObject):
# The Qt methods above will return a QString which can cause problems later.
# So far json.dump() will fail to serialize it.
- # TODO: Improve the serialization methods and remove this fix.
filename = str(filename)
if filename == "":
@@ -10853,7 +10840,6 @@ class App(QtCore.QObject):
try:
obj = self.collection.get_by_name(str(obj_name))
except Exception:
- # TODO: The return behavior has not been established... should raise exception?
return "Could not retrieve object: %s" % obj_name
else:
obj = local_use
@@ -11003,7 +10989,6 @@ class App(QtCore.QObject):
try:
obj = self.collection.get_by_name(str(obj_name))
except Exception:
- # TODO: The return behavior has not been established... should raise exception?
return "Could not retrieve object: %s" % obj_name
else:
obj = local_use
@@ -11806,7 +11791,6 @@ class App(QtCore.QObject):
:return:
"""
- # TODO: Move this to constructor
icons = {
"gerber": self.resource_location + "/flatcam_icon16.png",
"excellon": self.resource_location + "/drill16.png",
@@ -11990,32 +11974,38 @@ class App(QtCore.QObject):
tsize = fsize + int(fsize / 2)
# selected_text = (_('''
- #
Selected Tab - Choose an Item from Project Tab
+ # Selected Tab - Choose an Item from Project Tab
+ #
#
# Details:
# The normal flow when working in FlatCAM is the following:
#
#
- # - Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG file into
+ #
- Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG
+ # file into
# FlatCAM using either the menu's, toolbars, key shortcuts or
# even dragging and dropping the files on the GUI.
#
- # You can also load a FlatCAM project by double clicking on the project file, drag & drop of the
+ # You can also load a FlatCAM project by double clicking on the project file, drag &
+ # drop of the
# file into the FLATCAM GUI or through the menu/toolbar links offered within the app.
#
- # - Once an object is available in the Project Tab, by selecting it and then
+ #
- Once an object is available in the Project Tab, by selecting it
+ # and then
# focusing on SELECTED TAB (more simpler is to double click the object name in the
# Project Tab), SELECTED TAB will be updated with the object properties according to
# it's kind: Gerber, Excellon, Geometry or CNCJob object.
#
- # If the selection of the object is done on the canvas by single click instead, and the SELECTED TAB
+ # If the selection of the object is done on the canvas by single click instead, and the
+ # SELECTED TAB
# is in focus, again the object properties will be displayed into the Selected Tab. Alternatively,
# double clicking on the object on the canvas will bring the SELECTED TAB and populate
# it even if it was out of focus.
#
# You can change the parameters in this screen and the flow direction is like this:
#
- # Gerber/Excellon Object -> Change Param -> Generate Geometry -> Geometry Object
+ # Gerber/Excellon Object -> Change Param -> Generate Geometry ->
+ # Geometry Object
# -> Add tools (change param in Selected Tab) -> Generate CNCJob -> CNCJob Object
# -> Verify GCode (through Edit CNC Code) and/or append/prepend to GCode (again, done in
# SELECTED TAB) -> Save GCode
@@ -12128,7 +12118,7 @@ class App(QtCore.QObject):
# no_stats dict; just so it won't break things on website
no_ststs_dict = {}
no_ststs_dict["global_ststs"] = {}
- full_url = App.version_url + "?s=" + str(self.defaults['global_serial']) + "&v=" + str(self.version) + \
+ full_url = App.version_url + "?s=" + str(self.defaults['global_serial']) + "&v=" + str(self.version) +\
"&os=" + str(self.os) + "&" + urllib.parse.urlencode(no_ststs_dict["global_ststs"])
App.log.debug("Checking for updates @ %s" % full_url)
@@ -12441,7 +12431,7 @@ class App(QtCore.QObject):
new_color = self.defaults['gerber_plot_fill']
clicked_action = self.sender()
- assert isinstance(clicked_action, QAction), "Expected a QAction, got %s" % isinstance(clicked_action, QAction)
+ assert isinstance(clicked_action, QAction), "Expected a QAction, got %s" % type(clicked_action)
act_name = clicked_action.text()
sel_obj_list = self.collection.get_selected()
@@ -12717,8 +12707,12 @@ class App(QtCore.QObject):
:return:
"""
log.debug("App.save_project_auto_update() --> updated the interval timeout.")
- if self.autosave_timer.isActive():
- self.autosave_timer.stop()
+ try:
+ if self.autosave_timer.isActive():
+ self.autosave_timer.stop()
+ except Exception:
+ pass
+
if self.defaults['global_autosave'] is True:
self.autosave_timer.setInterval(int(self.defaults['global_autosave_timeout']))
self.autosave_timer.start()
@@ -12749,211 +12743,6 @@ class App(QtCore.QObject):
self.options.update(self.defaults)
# self.options_write_form()
- def init_tcl(self):
- """
- Initialize the TCL Shell. A dock widget that holds the GUI interface to the FlatCAM command line.
- :return: None
- """
- if hasattr(self, 'tcl') and self.tcl is not None:
- # self.tcl = None
- # TODO we need to clean non default variables and procedures here
- # new object cannot be used here as it will not remember values created for next passes,
- # because tcl was executed in old instance of TCL
- pass
- else:
- self.tcl = tk.Tcl()
- self.setup_shell()
- self.log.debug("TCL Shell has been initialized.")
-
- def setup_shell(self):
- """
- Creates shell functions. Runs once at startup.
-
- :return: None
- """
-
- self.log.debug("setup_shell()")
-
- # def shelp(p=None):
- # pass
-
- # --- Migrated to new architecture ---
- # def options(name):
- # ops = self.collection.get_by_name(str(name)).options
- # return '\n'.join(["%s: %s" % (o, ops[o]) for o in ops])
-
- # def h(*args):
- # """
- # Pre-processes arguments to detect '-keyword value' pairs into dictionary
- # and standalone parameters into list.
- # """
- #
- # kwa = {}
- # a = []
- # n = len(args)
- # name = None
- # for i in range(n):
- # match = re.search(r'^-([a-zA-Z].*)', args[i])
- # if match:
- # assert name is None
- # name = match.group(1)
- # continue
- #
- # if name is None:
- # a.append(args[i])
- # else:
- # kwa[name] = args[i]
- # name = None
- #
- # return a, kwa
-
- # @contextmanager
- # def wait_signal(signal, timeout=10000):
- # """
- # Block loop until signal emitted, timeout (ms) elapses
- # or unhandled exception happens in a thread.
- #
- # :param timeout: time after which the loop is exited
- # :param signal: Signal to wait for.
- # """
- # loop = QtCore.QEventLoop()
- #
- # # Normal termination
- # signal.connect(loop.quit)
- #
- # # Termination by exception in thread
- # self.thread_exception.connect(loop.quit)
- #
- # status = {'timed_out': False}
- #
- # def report_quit():
- # status['timed_out'] = True
- # loop.quit()
- #
- # yield
- #
- # # Temporarily change how exceptions are managed.
- # oeh = sys.excepthook
- # ex = []
- #
- # def except_hook(type_, value, traceback_):
- # ex.append(value)
- # oeh(type_, value, traceback_)
- #
- # sys.excepthook = except_hook
- #
- # # Terminate on timeout
- # if timeout is not None:
- # QtCore.QTimer.singleShot(timeout, report_quit)
- #
- # # # ## Block ## ##
- # loop.exec_()
- #
- # # Restore exception management
- # sys.excepthook = oeh
- # if ex:
- # self.raise_tcl_error(str(ex[0]))
- #
- # if status['timed_out']:
- # raise Exception('Timed out!')
- #
- # def make_docs():
- # output = ''
- # import collections
- # od = collections.OrderedDict(sorted(self.tcl_commands_storage.items()))
- # for cmd_, val in od.items():
- # output += cmd_ + ' \n' + ''.join(['~'] * len(cmd_)) + '\n'
- #
- # t = val['help']
- # usage_i = t.find('>')
- # if usage_i < 0:
- # expl = t
- # output += expl + '\n\n'
- # continue
- #
- # expl = t[:usage_i - 1]
- # output += expl + '\n\n'
- #
- # end_usage_i = t[usage_i:].find('\n')
- #
- # if end_usage_i < 0:
- # end_usage_i = len(t[usage_i:])
- # output += ' ' + t[usage_i:] + '\n No parameters.\n'
- # else:
- # extras = t[usage_i + end_usage_i + 1:]
- # parts = [s.strip() for s in extras.split('\n')]
- #
- # output += ' ' + t[usage_i:usage_i + end_usage_i] + '\n'
- # for p in parts:
- # output += ' ' + p + '\n\n'
- #
- # return output
-
- '''
- Howto implement TCL shell commands:
-
- All parameters passed to command should be possible to set as None and test it afterwards.
- This is because we need to see error caused in tcl,
- if None value as default parameter is not allowed TCL will return empty error.
- Use:
- def mycommand(name=None,...):
-
- Test it like this:
- if name is None:
-
- self.raise_tcl_error('Argument name is missing.')
-
- When error ocurre, always use raise_tcl_error, never return "sometext" on error,
- otherwise we will miss it and processing will silently continue.
- Method raise_tcl_error pass error into TCL interpreter, then raise python exception,
- which is catched in exec_command and displayed in TCL shell console with red background.
- Error in console is displayed with TCL trace.
-
- This behavior works only within main thread,
- errors with promissed tasks can be catched and detected only with log.
- TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for
- TCL shell.
-
- Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules.
-
- '''
-
- self.tcl_commands_storage = {}
- # commands = {
- # 'help': {
- # 'fcn': shelp,
- # 'help': _("Shows list of commands."),
- # 'description': ''
- # },
- # }
-
- # Import/overwrite tcl commands as objects of TclCommand descendants
- # This modifies the variable 'commands'.
- tclCommands.register_all_commands(self, self.tcl_commands_storage)
-
- # Add commands to the tcl interpreter
- for cmd in self.tcl_commands_storage:
- self.tcl.createcommand(cmd, self.tcl_commands_storage[cmd]['fcn'])
-
- # Make the tcl puts function return instead of print to stdout
- self.tcl.eval('''
- rename puts original_puts
- proc puts {args} {
- if {[llength $args] == 1} {
- return "[lindex $args 0]"
- } else {
- eval original_puts $args
- }
- }
- ''')
-
- # TODO: This shouldn't be here.
- class TclErrorException(Exception):
- """
- this exception is defined here, to be able catch it if we successfully handle all errors from shell command
- """
- pass
-
def toggle_shell(self):
"""
Toggle shell: if is visible close it, if it is closed then open it
@@ -13025,62 +12814,6 @@ class App(QtCore.QObject):
except AttributeError:
log.debug("shell_message() is called before Shell Class is instantiated. The message is: %s", str(msg))
- def raise_tcl_unknown_error(self, unknownException):
- """
- Raise exception if is different type than TclErrorException
- this is here mainly to show unknown errors inside TCL shell console.
-
- :param unknownException:
- :return:
- """
-
- if not isinstance(unknownException, self.TclErrorException):
- self.raise_tcl_error("Unknown error: %s" % str(unknownException))
- else:
- raise unknownException
-
- def display_tcl_error(self, error, error_info=None):
- """
- Escape bracket [ with '\' otherwise there is error
- "ERROR: missing close-bracket" instead of real error
-
- :param error: it may be text or exception
- :param error_info: Some informations about the error
- :return: None
- """
-
- if isinstance(error, Exception):
- exc_type, exc_value, exc_traceback = error_info
- if not isinstance(error, self.TclErrorException):
- show_trace = 1
- else:
- show_trace = int(self.defaults['global_verbose_error_level'])
-
- if show_trace > 0:
- trc = traceback.format_list(traceback.extract_tb(exc_traceback))
- trc_formated = []
- for a in reversed(trc):
- trc_formated.append(a.replace(" ", " > ").replace("\n", ""))
- text = "%s\nPython traceback: %s\n%s" % (exc_value, exc_type, "\n".join(trc_formated))
- else:
- text = "%s" % error
- else:
- text = error
-
- text = text.replace('[', '\\[').replace('"', '\\"')
- self.tcl.eval('return -code error "%s"' % text)
-
- def raise_tcl_error(self, text):
- """
- This method pass exception from python into TCL as error, so we get stacktrace and reason
-
- :param text: text of error
- :return: raise exception
- """
-
- self.display_tcl_error(text)
- raise self.TclErrorException(text)
-
class ArgsThread(QtCore.QObject):
open_signal = pyqtSignal(list)
diff --git a/flatcamGUI/ObjectUI.py b/flatcamGUI/ObjectUI.py
index 3b91514c..01ebf384 100644
--- a/flatcamGUI/ObjectUI.py
+++ b/flatcamGUI/ObjectUI.py
@@ -1241,6 +1241,7 @@ class ExcellonObjectUI(ObjectUI):
self.pdepth_entry.set_precision(self.decimals)
self.pdepth_entry.set_range(-9999.9999, 9999.9999)
self.pdepth_entry.setSingleStep(0.1)
+ self.pdepth_entry.setObjectName("e_depth_probe")
self.grid5.addWidget(self.pdepth_label, 13, 0)
self.grid5.addWidget(self.pdepth_entry, 13, 1)
@@ -1258,7 +1259,7 @@ class ExcellonObjectUI(ObjectUI):
self.feedrate_probe_entry.set_precision(self.decimals)
self.feedrate_probe_entry.set_range(0.0, 9999.9999)
self.feedrate_probe_entry.setSingleStep(0.1)
- self.feedrate_probe_entry.setObjectName(_("e_fr_probe"))
+ self.feedrate_probe_entry.setObjectName("e_fr_probe")
self.grid5.addWidget(self.feedrate_probe_label, 14, 0)
self.grid5.addWidget(self.feedrate_probe_entry, 14, 1)
diff --git a/flatcamGUI/PreferencesUI.py b/flatcamGUI/PreferencesUI.py
index 8ad23154..96c5e52e 100644
--- a/flatcamGUI/PreferencesUI.py
+++ b/flatcamGUI/PreferencesUI.py
@@ -8420,7 +8420,7 @@ class Tools2PunchGerberPrefGroupUI(OptionsGroupUI):
"- Excellon Object-> the Excellon object drills center will serve as reference.\n"
"- Fixed Diameter -> will try to use the pads center as reference adding fixed diameter holes.\n"
"- Fixed Annular Ring -> will try to keep a set annular ring.\n"
- "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.\n")
+ "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.")
)
grid_lay.addWidget(self.hole_size_label, 9, 0)
grid_lay.addWidget(self.hole_size_radio, 9, 1)
diff --git a/flatcamTools/ToolPunchGerber.py b/flatcamTools/ToolPunchGerber.py
index 2f984f5f..508517bb 100644
--- a/flatcamTools/ToolPunchGerber.py
+++ b/flatcamTools/ToolPunchGerber.py
@@ -142,7 +142,7 @@ class ToolPunchGerber(FlatCAMTool):
"- Excellon Object-> the Excellon object drills center will serve as reference.\n"
"- Fixed Diameter -> will try to use the pads center as reference adding fixed diameter holes.\n"
"- Fixed Annular Ring -> will try to keep a set annular ring.\n"
- "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.\n")
+ "- Proportional -> will make a Gerber punch hole having the diameter a percentage of the pad diameter.")
)
self.method_punch = RadioSet(
[
diff --git a/flatcamTools/ToolShell.py b/flatcamTools/ToolShell.py
index edbb3c7d..c4ff2848 100644
--- a/flatcamTools/ToolShell.py
+++ b/flatcamTools/ToolShell.py
@@ -13,8 +13,10 @@ from PyQt5.QtWidgets import QVBoxLayout, QWidget
from flatcamGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit
import html
import sys
+import traceback
import tkinter as tk
+import tclCommands
import gettext
import FlatCAMTranslation as fcTranslate
@@ -110,7 +112,7 @@ class TermWidget(QWidget):
elif style == 'err':
text = '%s'\
'%s'\
- %(mtype, body)
+ % (mtype, body)
elif style == 'warning':
# text = '%s' % text
text = '%s' \
@@ -253,15 +255,90 @@ class TermWidget(QWidget):
class FCShell(TermWidget):
- def __init__(self, sysShell, version, *args):
+ def __init__(self, app, version, *args):
"""
+ Initialize the TCL Shell. A dock widget that holds the GUI interface to the FlatCAM command line.
- :param sysShell: When instantiated the sysShell will be actually the FlatCAMApp.App() class
+ :param app: When instantiated the sysShell will be actually the FlatCAMApp.App() class
:param version: FlatCAM version string
:param args: Parameters passed to the TermWidget parent class
"""
- TermWidget.__init__(self, version, *args, app=sysShell)
- self._sysShell = sysShell
+ TermWidget.__init__(self, version, *args, app=app)
+ self.app = app
+
+ self.tcl_commands_storage = {}
+
+ if hasattr(self, 'tcl') and self.tcl is not None:
+ # self.tcl = None
+ # new object cannot be used here as it will not remember values created for next passes,
+ # because tcl was executed in old instance of TCL
+ pass
+ else:
+ self.tcl = tk.Tcl()
+ self.setup_shell()
+
+ self._edit.set_model_data(self.app.myKeywords)
+ self.setWindowIcon(self.app.ui.app_icon)
+ self.setWindowTitle("FlatCAM Shell")
+ self.resize(*self.app.defaults["global_shell_shape"])
+ self._append_to_browser('in', "FlatCAM %s - " % version)
+ self.append_output('%s\n\n' % _("Type >help< to get started"))
+
+ def setup_shell(self):
+ """
+ Creates shell functions. Runs once at startup.
+
+ :return: None
+ """
+
+ '''
+ How to implement TCL shell commands:
+
+ All parameters passed to command should be possible to set as None and test it afterwards.
+ This is because we need to see error caused in tcl,
+ if None value as default parameter is not allowed TCL will return empty error.
+ Use:
+ def mycommand(name=None,...):
+
+ Test it like this:
+ if name is None:
+
+ self.raise_tcl_error('Argument name is missing.')
+
+ When error occurred, always use raise_tcl_error, never return "some text" on error,
+ otherwise we will miss it and processing will silently continue.
+ Method raise_tcl_error pass error into TCL interpreter, then raise python exception,
+ which is caught in exec_command and displayed in TCL shell console with red background.
+ Error in console is displayed with TCL trace.
+
+ This behavior works only within main thread,
+ errors with promissed tasks can be catched and detected only with log.
+ TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for
+ TCL shell.
+
+ Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules.
+
+ '''
+
+ # Import/overwrite tcl commands as objects of TclCommand descendants
+ # This modifies the variable 'self.tcl_commands_storage'.
+ tclCommands.register_all_commands(self.app, self.tcl_commands_storage)
+
+ # Add commands to the tcl interpreter
+ for cmd in self.tcl_commands_storage:
+ self.tcl.createcommand(cmd, self.tcl_commands_storage[cmd]['fcn'])
+
+ # Make the tcl puts function return instead of print to stdout
+ self.tcl.eval('''
+ rename puts original_puts
+ proc puts {args} {
+ if {[llength $args] == 1} {
+ return "[lindex $args 0]"
+ } else {
+ eval original_puts $args
+ }
+ }
+ ''')
def is_command_complete(self, text):
def skipQuotes(txt):
@@ -293,7 +370,7 @@ class FCShell(TermWidget):
:return: output if there was any
"""
- self._sysShell.report_usage('exec_command')
+ self.app.report_usage('exec_command')
return self.exec_command_test(text, False, no_echo=no_echo)
@@ -315,15 +392,15 @@ class FCShell(TermWidget):
if no_echo is False:
self.open_processing() # Disables input box.
- result = self._sysShell.tcl.eval(str(tcl_command_string))
+ result = self.tcl.eval(str(tcl_command_string))
if result != 'None' and no_echo is False:
self.append_output(result + '\n')
except tk.TclError as e:
# This will display more precise answer if something in TCL shell fails
- result = self._sysShell.tcl.eval("set errorInfo")
- self._sysShell.log.error("Exec command Exception: %s" % (result + '\n'))
+ result = self.tcl.eval("set errorInfo")
+ self.app.log.error("Exec command Exception: %s" % (result + '\n'))
if no_echo is False:
self.append_error('ERROR: ' + result + '\n')
# Show error in console and just return or in test raise exception
@@ -335,39 +412,101 @@ class FCShell(TermWidget):
pass
return result
- # """
- # Code below is unsused. Saved for later.
- # """
+ def raise_tcl_unknown_error(self, unknownException):
+ """
+ Raise exception if is different type than TclErrorException
+ this is here mainly to show unknown errors inside TCL shell console.
- # parts = re.findall(r'([\w\\:\.]+|".*?")+', text)
- # parts = [p.replace('\n', '').replace('"', '') for p in parts]
- # self.log.debug(parts)
- # try:
- # if parts[0] not in commands:
- # self.shell.append_error("Unknown command\n")
- # return
- #
- # #import inspect
- # #inspect.getargspec(someMethod)
- # if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \
- # (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]):
- # self.shell.append_error(
- # "Command %s takes %d arguments. %d given.\n" %
- # (parts[0], commands[parts[0]]["params"], len(parts)-1)
- # )
- # return
- #
- # cmdfcn = commands[parts[0]]["fcn"]
- # cmdconv = commands[parts[0]]["converters"]
- # if len(parts) - 1 > 0:
- # retval = cmdfcn(*[cmdconv[i](parts[i + 1]) for i in range(len(parts)-1)])
- # else:
- # retval = cmdfcn()
- # retfcn = commands[parts[0]]["retfcn"]
- # if retval and retfcn(retval):
- # self.shell.append_output(retfcn(retval) + "\n")
- #
- # except Exception as e:
- # #self.shell.append_error(''.join(traceback.format_exc()))
- # #self.shell.append_error("?\n")
- # self.shell.append_error(str(e) + "\n")
+ :param unknownException:
+ :return:
+ """
+
+ if not isinstance(unknownException, self.TclErrorException):
+ self.raise_tcl_error("Unknown error: %s" % str(unknownException))
+ else:
+ raise unknownException
+
+ def display_tcl_error(self, error, error_info=None):
+ """
+ Escape bracket [ with '\' otherwise there is error
+ "ERROR: missing close-bracket" instead of real error
+
+ :param error: it may be text or exception
+ :param error_info: Some informations about the error
+ :return: None
+ """
+
+ if isinstance(error, Exception):
+ exc_type, exc_value, exc_traceback = error_info
+ if not isinstance(error, self.TclErrorException):
+ show_trace = 1
+ else:
+ show_trace = int(self.app.defaults['global_verbose_error_level'])
+
+ if show_trace > 0:
+ trc = traceback.format_list(traceback.extract_tb(exc_traceback))
+ trc_formated = []
+ for a in reversed(trc):
+ trc_formated.append(a.replace(" ", " > ").replace("\n", ""))
+ text = "%s\nPython traceback: %s\n%s" % (exc_value, exc_type, "\n".join(trc_formated))
+ else:
+ text = "%s" % error
+ else:
+ text = error
+
+ text = text.replace('[', '\\[').replace('"', '\\"')
+ self.tcl.eval('return -code error "%s"' % text)
+
+ def raise_tcl_error(self, text):
+ """
+ This method pass exception from python into TCL as error, so we get stacktrace and reason
+
+ :param text: text of error
+ :return: raise exception
+ """
+
+ self.display_tcl_error(text)
+ raise self.TclErrorException(text)
+
+ class TclErrorException(Exception):
+ """
+ this exception is defined here, to be able catch it if we successfully handle all errors from shell command
+ """
+ pass
+
+ # """
+ # Code below is unsused. Saved for later.
+ # """
+
+ # parts = re.findall(r'([\w\\:\.]+|".*?")+', text)
+ # parts = [p.replace('\n', '').replace('"', '') for p in parts]
+ # self.log.debug(parts)
+ # try:
+ # if parts[0] not in commands:
+ # self.shell.append_error("Unknown command\n")
+ # return
+ #
+ # #import inspect
+ # #inspect.getargspec(someMethod)
+ # if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \
+ # (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]):
+ # self.shell.append_error(
+ # "Command %s takes %d arguments. %d given.\n" %
+ # (parts[0], commands[parts[0]]["params"], len(parts)-1)
+ # )
+ # return
+ #
+ # cmdfcn = commands[parts[0]]["fcn"]
+ # cmdconv = commands[parts[0]]["converters"]
+ # if len(parts) - 1 > 0:
+ # retval = cmdfcn(*[cmdconv[i](parts[i + 1]) for i in range(len(parts)-1)])
+ # else:
+ # retval = cmdfcn()
+ # retfcn = commands[parts[0]]["retfcn"]
+ # if retval and retfcn(retval):
+ # self.shell.append_output(retfcn(retval) + "\n")
+ #
+ # except Exception as e:
+ # #self.shell.append_error(''.join(traceback.format_exc()))
+ # #self.shell.append_error("?\n")
+ # self.shell.append_error(str(e) + "\n")
diff --git a/requirements.txt b/requirements.txt
index 89cda8e4..15cffc72 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -18,7 +18,7 @@ vispy
ortools>=7.0
svg.path
simplejson
-shapely>=1.3
+shapely>=1.7.0
freetype-py
fontTools
rasterio
diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py
index 45378180..ed2d0260 100644
--- a/tclCommands/TclCommand.py
+++ b/tclCommands/TclCommand.py
@@ -71,7 +71,7 @@ class TclCommand(object):
:return: none
"""
- self.app.raise_tcl_error(text)
+ self.app.shell.raise_tcl_error(text)
def get_current_command(self):
"""
@@ -275,7 +275,7 @@ class TclCommand(object):
# because of signaling we cannot call error to TCL from here but when task
# is finished also non-signaled are handled here to better exception
# handling and displayed after command is finished
- raise self.app.TclErrorException(text)
+ raise self.app.shell.TclErrorException(text)
def execute_wrapper(self, *args):
"""
@@ -296,7 +296,7 @@ class TclCommand(object):
except Exception as unknown:
error_info = sys.exc_info()
self.log.error("TCL command '%s' failed. Error text: %s" % (str(self), str(unknown)))
- self.app.display_tcl_error(unknown, error_info)
+ self.app.shell.display_tcl_error(unknown, error_info)
self.raise_tcl_unknown_error(unknown)
@abc.abstractmethod
@@ -400,9 +400,9 @@ class TclCommandSignaled(TclCommand):
raise ex[0]
if status['timed_out']:
- self.app.raise_tcl_unknown_error("Operation timed outed! Consider increasing option "
- "'-timeout ' for command or "
- "'set_sys global_background_timeout '.")
+ self.app.shell.raise_tcl_unknown_error("Operation timed outed! Consider increasing option "
+ "'-timeout ' for command or "
+ "'set_sys global_background_timeout '.")
try:
self.log.debug("TCL command '%s' executed." % str(type(self).__name__))
@@ -439,5 +439,5 @@ class TclCommandSignaled(TclCommand):
else:
error_info = sys.exc_info()
self.log.error("TCL command '%s' failed." % str(self))
- self.app.display_tcl_error(unknown, error_info)
+ self.app.shell.display_tcl_error(unknown, error_info)
self.raise_tcl_unknown_error(unknown)
diff --git a/tclCommands/TclCommandHelp.py b/tclCommands/TclCommandHelp.py
index cb19ddf3..a5688381 100644
--- a/tclCommands/TclCommandHelp.py
+++ b/tclCommands/TclCommandHelp.py
@@ -65,10 +65,10 @@ class TclCommandHelp(TclCommand):
if 'name' in args:
name = args['name']
- if name not in self.app.tcl_commands_storage:
+ if name not in self.app.shell.tcl_commands_storage:
return "Unknown command: %s" % name
- help_for_command = self.app.tcl_commands_storage[name]["help"] + '\n\n'
+ help_for_command = self.app.shell.tcl_commands_storage[name]["help"] + '\n\n'
self.app.shell.append_output(help_for_command)
else:
if not args:
@@ -78,19 +78,21 @@ class TclCommandHelp(TclCommand):
try:
# find the maximum length of a command name
max_len = 0
- for cmd_name in self.app.tcl_commands_storage:
+ for cmd_name in self.app.shell.tcl_commands_storage:
curr_len = len(cmd_name)
if curr_len > max_len:
max_len = curr_len
h_space = " "
cnt = 0
- for cmd_name in sorted(self.app.tcl_commands_storage):
- cmd_description = "%s" % self.app.tcl_commands_storage[cmd_name]['description']
+ for cmd_name in sorted(self.app.shell.tcl_commands_storage):
+ cmd_description = "%s" % \
+ self.app.shell.tcl_commands_storage[cmd_name]['description']
curr_len = len(cmd_name)
- cmd_name_colored = "%s" % str(cmd_name)
+ cmd_name_colored = "> %s" % \
+ str(cmd_name)
nr_chars = max_len - curr_len
@@ -105,8 +107,8 @@ class TclCommandHelp(TclCommand):
else:
cnt += 1
except Exception as err:
- self.app.log.debug("App.setup_shell.shelp() when run as 'help' --> %s" % str(err))
- displayed_text = ['> %s\n' % cmd for cmd in sorted(self.app.tcl_commands_storage)]
+ self.app.log.debug("tclCommands.TclCommandHelp() when run as 'help' --> %s" % str(err))
+ displayed_text = ['> %s' % cmd for cmd in sorted(self.app.shell.tcl_commands_storage)]
cmd_enum += '
'.join(displayed_text)
cmd_enum += '
%s
%s
' % (