diff --git a/CHANGELOG.md b/CHANGELOG.md index 823be59e..00e3a33b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ CHANGELOG for FlatCAM beta - added a new feature, project auto-saving controlled from Edit -> Preferences -> General -> APP. Preferences -> Enable Auto Save checkbox - fixed some bugs in the Tcl Commands - modified the Tcl Commands to be able to use as boolean values keywords with lower case like 'false' instead of expected 'False' +- refactored some of the code in the App class and created a new Tcl Command named Help 20.04.2020 diff --git a/FlatCAMApp.py b/FlatCAMApp.py index e9f4affc..2b6e0cee 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -2556,6 +2556,9 @@ class App(QtCore.QObject): # 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) @@ -2566,14 +2569,7 @@ class App(QtCore.QObject): self.shell.append_output("FlatCAM %s - " % self.version) self.shell.append_output(_("Type >help< to get started\n\n")) - self.ui.shell_dock = QtWidgets.QDockWidget("FlatCAM TCL Shell") - self.ui.shell_dock.setObjectName('Shell_DockWidget') self.ui.shell_dock.setWidget(self.shell) - self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) - self.ui.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable | - QtWidgets.QDockWidget.DockWidgetFloatable | - QtWidgets.QDockWidget.DockWidgetClosable) - self.ui.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.ui.shell_dock) # show TCL shell at start-up based on the Menu -? Edit -> Preferences setting. if self.defaults["global_shell_at_startup"]: @@ -2883,7 +2879,7 @@ class App(QtCore.QObject): # if there are Windows paths then replace the path separator with a Unix like one if sys.platform == 'win32': command_tcl_formatted = command_tcl_formatted.replace('\\', '/') - self.shell._sysShell.exec_command(command_tcl_formatted, no_echo=True) + self.shell.exec_command(command_tcl_formatted, no_echo=True) except Exception as ext: print("ERROR: ", ext) sys.exit(2) @@ -2903,9 +2899,9 @@ class App(QtCore.QObject): # color=QtGui.QColor("gray")) cmd_line_shellfile_text = myfile.read() if self.cmd_line_headless != 1: - self.shell._sysShell.exec_command(cmd_line_shellfile_text) + self.shell.exec_command(cmd_line_shellfile_text) else: - self.shell._sysShell.exec_command(cmd_line_shellfile_text, no_echo=True) + self.shell.exec_command(cmd_line_shellfile_text, no_echo=True) except Exception as ext: print("ERROR: ", ext) @@ -3703,209 +3699,6 @@ class App(QtCore.QObject): else: self.defaults['global_stats'][resource] = 1 - 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 execudted in old instance of TCL - pass - else: - self.tcl = tk.Tcl() - self.setup_shell() - self.log.debug("TCL Shell has been initialized.") - - # 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 shell_message(self, msg, show=False, error=False, warning=False, success=False, selected=False): - """ - Shows a message on the FlatCAM Shell - - :param msg: Message to display. - :param show: Opens the shell. - :param error: Shows the message as an error. - :param warning: Shows the message as an warning. - :param success: Shows the message as an success. - :param selected: Indicate that something was selected on canvas - :return: None - """ - if show: - self.ui.shell_dock.show() - try: - if error: - self.shell.append_error(msg + "\n") - elif warning: - self.shell.append_warning(msg + "\n") - elif success: - self.shell.append_success(msg + "\n") - elif selected: - self.shell.append_selected(msg + "\n") - else: - self.shell.append_output(msg + "\n") - 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) - - def exec_command(self, text, no_echo=False): - """ - Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. - Also handles execution in separated threads - - :param text: FlatCAM TclCommand with parameters - :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it - will create crashes of the _Expandable_Edit widget - :return: output if there was any - """ - - self.report_usage('exec_command') - - result = self.exec_command_test(text, False, no_echo=no_echo) - - # MS: added this method call so the geometry is updated once the TCL command is executed - # if no_plot is None: - # self.plot_all() - - return result - - def exec_command_test(self, text, reraise=True, no_echo=False): - """ - Same as exec_command(...) with additional control over exceptions. - Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. - - :param text: Input command - :param reraise: Re-raise TclError exceptions in Python (mostly for unittests). - :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it - will create crashes of the _Expandable_Edit widget - :return: Output from the command - """ - - tcl_command_string = str(text) - - try: - if no_echo is False: - self.shell.open_processing() # Disables input box. - - result = self.tcl.eval(str(tcl_command_string)) - if result != 'None' and no_echo is False: - self.shell.append_output(result + '\n') - - except tk.TclError as e: - - # This will display more precise answer if something in TCL shell fails - result = self.tcl.eval("set errorInfo") - self.log.error("Exec command Exception: %s" % (result + '\n')) - if no_echo is False: - self.shell.append_error('ERROR: ' + result + '\n') - # Show error in console and just return or in test raise exception - if reraise: - raise e - finally: - if no_echo is False: - self.shell.close_processing() - 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 info(self, msg): """ Informs the user. Normally on the status bar, optionally @@ -10464,9 +10257,9 @@ class App(QtCore.QObject): with open(filename, "r") as tcl_script: cmd_line_shellfile_content = tcl_script.read() if self.cmd_line_headless != 1: - self.shell._sysShell.exec_command(cmd_line_shellfile_content) + self.shell.exec_command(cmd_line_shellfile_content) else: - self.shell._sysShell.exec_command(cmd_line_shellfile_content, no_echo=True) + self.shell.exec_command(cmd_line_shellfile_content, no_echo=True) if silent is False: self.inform.emit('[success] %s' % _("TCL script file opened in Code Editor and executed.")) @@ -11865,231 +11658,6 @@ class App(QtCore.QObject): """ self.ui.progress_bar.setValue(int(percentage)) - def setup_shell(self): - """ - Creates shell functions. Runs once at startup. - - :return: None - """ - - self.log.debug("setup_shell()") - - def shelp(p=None): - if not p: - cmd_enum = _("Available commands:\n") - - displayed_text = [] - try: - # find the maximum length of a command name - max_len = 0 - for cmd_name in commands: - curr_len = len(cmd_name) - if curr_len > max_len: - max_len = curr_len - max_tabs = math.ceil(max_len / 8) - - for cmd_name in sorted(commands): - cmd_description = commands[cmd_name]['description'] - - curr_len = len(cmd_name) - tabs = '\t' - - # make sure to add the right number of tabs (1 tab = 8 spaces) so all the commands - # descriptions are aligned - if curr_len == max_len: - cmd_line_txt = ' %s%s%s' % (str(cmd_name), tabs, cmd_description) - else: - nr_tabs = 0 - - for x in range(max_tabs): - if curr_len <= (x*8): - nr_tabs += 1 - - # nr_tabs = 2 if curr_len <= 8 else 1 - cmd_line_txt = ' %s%s%s' % (str(cmd_name), nr_tabs*tabs, cmd_description) - - displayed_text.append(cmd_line_txt) - except Exception as err: - log.debug("App.setup_shell.shelp() when run as 'help' --> %s" % str(err)) - displayed_text = [' %s' % cmd for cmd in sorted(commands)] - - cmd_enum += '\n'.join(displayed_text) - cmd_enum += '\n\n%s\n%s' % (_("Type help for usage."), _("Example: help open_gerber")) - return cmd_enum - - if p not in commands: - return "Unknown command: %s" % p - - return commands[p]["help"] - - # --- 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.raiseTclError(str(ex[0])) - - if status['timed_out']: - raise Exception('Timed out!') - - def make_docs(): - output = '' - import collections - od = collections.OrderedDict(sorted(commands.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. - - ''' - - 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, commands) - - # Add commands to the tcl interpreter - for cmd in commands: - self.tcl.createcommand(cmd, commands[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 setup_recent_items(self): """ Setup a dictionary with the recent files accessed, organized by type @@ -13040,6 +12608,295 @@ 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 execudted 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 shell_message(self, msg, show=False, error=False, warning=False, success=False, selected=False): + """ + Shows a message on the FlatCAM Shell + + :param msg: Message to display. + :param show: Opens the shell. + :param error: Shows the message as an error. + :param warning: Shows the message as an warning. + :param success: Shows the message as an success. + :param selected: Indicate that something was selected on canvas + :return: None + """ + if show: + self.ui.shell_dock.show() + try: + if error: + self.shell.append_error(msg + "\n") + elif warning: + self.shell.append_warning(msg + "\n") + elif success: + self.shell.append_success(msg + "\n") + elif selected: + self.shell.append_selected(msg + "\n") + else: + self.shell.append_output(msg + "\n") + 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/FlatCAMTool.py b/FlatCAMTool.py index 3343c06c..3b7f8d0f 100644 --- a/FlatCAMTool.py +++ b/FlatCAMTool.py @@ -252,3 +252,11 @@ class FlatCAMTool(QtWidgets.QWidget): (_("Edited value is out of range"), minval, maxval)) else: self.app.inform.emit('[success] %s' % _("Edited value is within limits.")) + + def sizeHint(self): + """ + I've overloaded this just in case I will need to make changes in the future to enforce dimensions + :return: + """ + default_hint_size = super(FlatCAMTool, self).sizeHint() + return QtCore.QSize(default_hint_size.width(), default_hint_size.height()) diff --git a/flatcamGUI/FlatCAMGUI.py b/flatcamGUI/FlatCAMGUI.py index 0b7c44cb..ded1d7c2 100644 --- a/flatcamGUI/FlatCAMGUI.py +++ b/flatcamGUI/FlatCAMGUI.py @@ -42,13 +42,24 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # Divine icon pack by Ipapun @ finicons.com - # ################################## ## - # ## BUILDING THE GUI IS DONE HERE # ## - # ################################## ## + # ####################################################################### + # ############ BUILDING THE GUI IS EXECUTED HERE ######################## + # ####################################################################### - # ######### ## - # ## Menu # ## - # ######### ## + # ####################################################################### + # ####################### TCL Shell DOCK ################################ + # ####################################################################### + self.shell_dock = QtWidgets.QDockWidget("FlatCAM TCL Shell") + self.shell_dock.setObjectName('Shell_DockWidget') + self.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) + self.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable | + QtWidgets.QDockWidget.DockWidgetFloatable | + QtWidgets.QDockWidget.DockWidgetClosable) + self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.shell_dock) + + # ####################################################################### + # ###################### Menu BUILDING ################################## + # ####################################################################### self.menu = self.menuBar() self.menu_toggle_nb = QtWidgets.QAction( @@ -735,7 +746,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow): # ######################################################################## # IMPORTANT # - # The order: SPITTER -> NOTEBOOK -> SNAP TOOLBAR is important and without it the GUI will not be initialized as + # The order: SPLITTER -> NOTEBOOK -> SNAP TOOLBAR is important and without it the GUI will not be initialized as # desired. self.splitter = QtWidgets.QSplitter() self.setCentralWidget(self.splitter) diff --git a/flatcamTools/ToolDblSided.py b/flatcamTools/ToolDblSided.py index 50ca4632..202e9d98 100644 --- a/flatcamTools/ToolDblSided.py +++ b/flatcamTools/ToolDblSided.py @@ -159,7 +159,7 @@ class DblSidedTool(FlatCAMTool): self.param_label = QtWidgets.QLabel("%s:" % _("Mirror Parameters")) self.param_label.setToolTip('%s.' % _("Parameters for the mirror operation")) - grid_lay1.addWidget(self.param_label, 0, 0, 1, 3) + grid_lay1.addWidget(self.param_label, 0, 0, 1, 2) # ## Axis self.mirax_label = QtWidgets.QLabel('%s:' % _("Mirror Axis")) diff --git a/flatcamTools/ToolShell.py b/flatcamTools/ToolShell.py index 92c3f45c..78f328d5 100644 --- a/flatcamTools/ToolShell.py +++ b/flatcamTools/ToolShell.py @@ -14,6 +14,8 @@ from flatcamGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit import html import sys +import tkinter as tk + import gettext import FlatCAMTranslation as fcTranslate import builtins @@ -227,6 +229,12 @@ class TermWidget(QWidget): class FCShell(TermWidget): def __init__(self, sysShell, version, *args): + """ + + :param sysShell: 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) self._sysShell = sysShell @@ -246,4 +254,94 @@ class FCShell(TermWidget): return True def child_exec_command(self, text): - self._sysShell.exec_command(text) + self.exec_command(text) + + def exec_command(self, text, no_echo=False): + """ + Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. + Also handles execution in separated threads + + :param text: FlatCAM TclCommand with parameters + :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it + will create crashes of the _Expandable_Edit widget + :return: output if there was any + """ + + self._sysShell.report_usage('exec_command') + + return self.exec_command_test(text, False, no_echo=no_echo) + + def exec_command_test(self, text, reraise=True, no_echo=False): + """ + Same as exec_command(...) with additional control over exceptions. + Handles input from the shell. See FlatCAMApp.setup_shell for shell commands. + + :param text: Input command + :param reraise: Re-raise TclError exceptions in Python (mostly for unittests). + :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it + will create crashes of the _Expandable_Edit widget + :return: Output from the command + """ + + tcl_command_string = str(text) + + try: + if no_echo is False: + self.open_processing() # Disables input box. + + result = self._sysShell.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')) + if no_echo is False: + self.append_error('ERROR: ' + result + '\n') + # Show error in console and just return or in test raise exception + if reraise: + raise e + finally: + if no_echo is False: + self.close_processing() + 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") diff --git a/flatcamTools/ToolTransform.py b/flatcamTools/ToolTransform.py index 69a1189b..416de316 100644 --- a/flatcamTools/ToolTransform.py +++ b/flatcamTools/ToolTransform.py @@ -33,8 +33,6 @@ class ToolTransform(FlatCAMTool): FlatCAMTool.__init__(self, app) self.decimals = self.app.decimals - self.transform_lay = QtWidgets.QVBoxLayout() - self.layout.addLayout(self.transform_lay) # ## Title title_label = QtWidgets.QLabel("%s" % self.toolName) title_label.setStyleSheet(""" @@ -44,12 +42,12 @@ class ToolTransform(FlatCAMTool): font-weight: bold; } """) - self.transform_lay.addWidget(title_label) - self.transform_lay.addWidget(QtWidgets.QLabel('')) + self.layout.addWidget(title_label) + self.layout.addWidget(QtWidgets.QLabel('')) # ## Layout grid0 = QtWidgets.QGridLayout() - self.transform_lay.addLayout(grid0) + self.layout.addLayout(grid0) grid0.setColumnStretch(0, 0) grid0.setColumnStretch(1, 1) grid0.setColumnStretch(2, 0) @@ -206,7 +204,7 @@ class ToolTransform(FlatCAMTool): self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False) grid0.addWidget(self.scale_link_cb, 10, 0) - grid0.addWidget(self.scale_zero_ref_cb, 10, 1) + grid0.addWidget(self.scale_zero_ref_cb, 10, 1, 1, 2) separator_line = QtWidgets.QFrame() separator_line.setFrameShape(QtWidgets.QFrame.HLine) @@ -395,7 +393,7 @@ class ToolTransform(FlatCAMTool): grid0.addWidget(QtWidgets.QLabel(''), 26, 0, 1, 3) - self.transform_lay.addStretch() + self.layout.addStretch() # ## Reset Tool self.reset_button = QtWidgets.QPushButton(_("Reset Tool")) @@ -408,7 +406,7 @@ class ToolTransform(FlatCAMTool): font-weight: bold; } """) - self.transform_lay.addWidget(self.reset_button) + self.layout.addWidget(self.reset_button) # ## Signals self.rotate_button.clicked.connect(self.on_rotate) diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py index 9971eeaf..72bb48c5 100644 --- a/tclCommands/TclCommand.py +++ b/tclCommands/TclCommand.py @@ -3,8 +3,7 @@ import re import FlatCAMApp import abc import collections -from PyQt5 import QtCore, QtGui -from PyQt5.QtCore import Qt +from PyQt5 import QtCore from contextlib import contextmanager diff --git a/tclCommands/TclCommandHelp.py b/tclCommands/TclCommandHelp.py new file mode 100644 index 00000000..a1af436e --- /dev/null +++ b/tclCommands/TclCommandHelp.py @@ -0,0 +1,119 @@ +# ########################################################## +# FlatCAM: 2D Post-processing for Manufacturing # +# File Author: Marius Adrian Stanciu (c) # +# Content was borrowed from FlatCAM proper # +# Date: 4/22/2020 # +# MIT Licence # +# ########################################################## + +from tclCommands.TclCommand import TclCommand + +import collections +import math + +import gettext +import FlatCAMTranslation as fcTranslate +import builtins + +fcTranslate.apply_language('strings') +if '_' not in builtins.__dict__: + _ = gettext.gettext + + +class TclCommandHelp(TclCommand): + """ + Tcl shell command to get the value of a system variable + + example: + get_sys excellon_zeros + """ + + # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['help'] + + description = '%s %s' % ("--", "PRINTS to TCL the HELP.") + + # Dictionary of types from Tcl command, needs to be ordered + arg_names = collections.OrderedDict([ + ('name', str) + ]) + + # Dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value + option_types = collections.OrderedDict([ + + ]) + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = [] + + # structured help for current command, args needs to be ordered + help = { + 'main': "Returns to TCL the value for the entered system variable.", + 'args': collections.OrderedDict([ + ('name', 'Name of a Tcl Command for which to display the Help.'), + ]), + 'examples': ['help add_circle'] + } + + def execute(self, args, unnamed_args): + """ + + :param args: + :param unnamed_args: + :return: + """ + print(self.app.tcl_commands_storage) + + if 'name' in args: + name = args['name'] + if name not in self.app.tcl_commands_storage: + return "Unknown command: %s" % name + + print(self.app.tcl_commands_storage[name]["help"]) + else: + if args is None: + cmd_enum = _("Available commands:\n") + + displayed_text = [] + try: + # find the maximum length of a command name + max_len = 0 + for cmd_name in self.app.tcl_commands_storage: + curr_len = len(cmd_name) + if curr_len > max_len: + max_len = curr_len + max_tabs = math.ceil(max_len / 8) + + for cmd_name in sorted(self.app.tcl_commands_storage): + cmd_description = self.app.tcl_commands_storage[cmd_name]['description'] + + curr_len = len(cmd_name) + tabs = '\t' + + cmd_name_colored = "" + cmd_name_colored += str(cmd_name) + cmd_name_colored += "" + + # make sure to add the right number of tabs (1 tab = 8 spaces) so all the commands + # descriptions are aligned + if curr_len == max_len: + cmd_line_txt = ' %s%s%s' % (cmd_name_colored, tabs, cmd_description) + else: + nr_tabs = 0 + + for x in range(max_tabs): + if curr_len <= (x * 8): + nr_tabs += 1 + + # nr_tabs = 2 if curr_len <= 8 else 1 + cmd_line_txt = ' %s%s%s' % (cmd_name_colored, nr_tabs * tabs, cmd_description) + + displayed_text.append(cmd_line_txt) + except Exception as err: + self.app.log.debug("App.setup_shell.shelp() when run as 'help' --> %s" % str(err)) + displayed_text = [' %s' % cmd for cmd in sorted(self.app.tcl_commands_storage)] + + cmd_enum += '\n'.join(displayed_text) + cmd_enum += '\n\n%s\n%s' % (_("Type help for usage."), _("Example: help open_gerber")) + + print(cmd_enum) \ No newline at end of file diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py index e34cd733..9a5b22cf 100644 --- a/tclCommands/__init__.py +++ b/tclCommands/__init__.py @@ -93,6 +93,7 @@ def register_all_commands(app, commands): tcl_modules = {k: v for k, v in list(sys.modules.items()) if k.startswith('tclCommands.TclCommand')} for key, mod in list(tcl_modules.items()): + print(key) if key != 'tclCommands.TclCommand': class_name = key.split('.')[1] class_type = getattr(mod, class_name)