فهرست منبع

- 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

Marius Stanciu 5 سال پیش
والد
کامیت
61020e3624
9فایلهای تغییر یافته به همراه269 افزوده شده و 387 حذف شده
  1. 7 0
      CHANGELOG.md
  2. 56 323
      FlatCAMApp.py
  3. 2 1
      flatcamGUI/ObjectUI.py
  4. 1 1
      flatcamGUI/PreferencesUI.py
  5. 1 1
      flatcamTools/ToolPunchGerber.py
  6. 184 45
      flatcamTools/ToolShell.py
  7. 1 1
      requirements.txt
  8. 7 7
      tclCommands/TclCommand.py
  9. 10 8
      tclCommands/TclCommandHelp.py

+ 7 - 0
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

+ 56 - 323
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 = (_('''
-        # <p><span style="font-size:{tsize}px"><strong>Selected Tab - Choose an Item from Project Tab</strong></span></p>
+        # <p><span style="font-size:{tsize}px"><strong>Selected Tab - Choose an Item from Project Tab</strong></span>
+        # </p>
         #
         # <p><span style="font-size:{fsize}px"><strong>Details</strong>:<br />
         # The normal flow when working in FlatCAM is the following:</span></p>
         #
         # <ol>
-        # 	<li><span style="font-size:{fsize}px">Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG file into
+        # 	<li><span style="font-size:{fsize}px">Loat/Import a Gerber, Excellon, Gcode, DXF, Raster Image or SVG
+        # 	file into
         # 	FlatCAM using either the menu&#39;s, toolbars, key shortcuts or
         # 	even dragging and dropping the files on the GUI.<br />
         # 	<br />
-        # 	You can also load a <strong>FlatCAM project</strong> by double clicking on the project file, drag &amp; drop of the
+        # 	You can also load a <strong>FlatCAM project</strong> by double clicking on the project file, drag &amp;
+        # 	drop of the
         # 	file into the FLATCAM GUI or through the menu/toolbar links offered within the app.</span><br />
         # 	&nbsp;</li>
-        # 	<li><span style="font-size:{fsize}px">Once an object is available in the Project Tab, by selecting it and then
+        # 	<li><span style="font-size:{fsize}px">Once an object is available in the Project Tab, by selecting it
+        # 	and then
         # 	focusing on <strong>SELECTED TAB </strong>(more simpler is to double click the object name in the
         # 	Project Tab), <strong>SELECTED TAB </strong>will be updated with the object properties according to
         # 	it&#39;s kind: Gerber, Excellon, Geometry or CNCJob object.<br />
         # 	<br />
-        # 	If the selection of the object is done on the canvas by single click instead, and the <strong>SELECTED TAB</strong>
+        # 	If the selection of the object is done on the canvas by single click instead, and the
+        # 	<strong>SELECTED TAB</strong>
         # 	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 <strong>SELECTED TAB</strong> and populate
         # 	it even if it was out of focus.<br />
         # 	<br />
         # 	You can change the parameters in this screen and the flow direction is like this:<br />
         # 	<br />
-        # 	<strong>Gerber/Excellon Object</strong> -&gt; Change Param -&gt; Generate Geometry -&gt;<strong> Geometry Object
+        # 	<strong>Gerber/Excellon Object</strong> -&gt; Change Param -&gt; Generate Geometry -&gt;
+        # 	<strong> Geometry Object
         # 	</strong>-&gt; Add tools (change param in Selected Tab) -&gt; Generate CNCJob -&gt;<strong> CNCJob Object
         # 	</strong>-&gt; Verify GCode (through Edit CNC Code) and/or append/prepend to GCode (again, done in
         # 	<strong>SELECTED TAB)&nbsp;</strong>-&gt; Save GCode</span></li>
@@ -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)

+ 2 - 1
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)

+ 1 - 1
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)

+ 1 - 1
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(
             [

+ 184 - 45
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 = '<span style="font-weight: bold; color: red;">%s</span>'\
                    '<span style="font-weight: bold;">%s</span>'\
-                   %(mtype, body)
+                   % (mtype, body)
         elif style == 'warning':
             # text = '<span style="font-weight: bold; color: #f4b642;">%s</span>' % text
             text = '<span style="font-weight: bold; color: #f4b642;">%s</span>' \
@@ -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.
-        # """
-
-        # 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")
+    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.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")

+ 1 - 1
requirements.txt

@@ -18,7 +18,7 @@ vispy
 ortools>=7.0
 svg.path
 simplejson
-shapely>=1.3
+shapely>=1.7.0
 freetype-py
 fontTools
 rasterio

+ 7 - 7
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 <miliseconds>' for command or "
-                                                 "'set_sys global_background_timeout <miliseconds>'.")
+                self.app.shell.raise_tcl_unknown_error("Operation timed outed! Consider increasing option "
+                                                       "'-timeout <miliseconds>' for command or "
+                                                       "'set_sys global_background_timeout <miliseconds>'.")
 
         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)

+ 10 - 8
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 = "&nbsp;"
                     cnt = 0
-                    for cmd_name in sorted(self.app.tcl_commands_storage):
-                        cmd_description = "<span>%s</span>" % self.app.tcl_commands_storage[cmd_name]['description']
+                    for cmd_name in sorted(self.app.shell.tcl_commands_storage):
+                        cmd_description = "<span>%s</span>" % \
+                                          self.app.shell.tcl_commands_storage[cmd_name]['description']
 
                         curr_len = len(cmd_name)
 
-                        cmd_name_colored = "<span style=\" font-weight: bold; color: red;\" >%s</span>" % str(cmd_name)
+                        cmd_name_colored = "> <span style=\" font-weight: bold; color: red;\" >%s</span>" % \
+                                           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 += '<br>'.join(displayed_text)
                 cmd_enum += '<br><br>%s<br>%s<br><br>' % (