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:

# #
    - #
  1. Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG file into + #
  2. 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.

    #  
  3. - #
  4. Once an object is available in the Project Tab, by selecting it and then + #
  5. 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
  6. @@ -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

    ' % (