From 3a97cd3880ab579b7472fe7364ca7ca08e5b27f1 Mon Sep 17 00:00:00 2001 From: Marius Stanciu Date: Sun, 20 Dec 2020 16:56:14 +0200 Subject: [PATCH] - modified the way the status bar icon is set - Drilling Tool - fixed missing feedrate code when the toolchange is Off - AppTextEditor - working on syntax highlighting - App - trying to speed up the new project creation - Tcl Shell - Browser Edit - added Undo/Redo, Cut and Delete selection - replace all the exec_() calls with exec() (except one situation in Tcl where I'm not sure of the effect) --- CHANGELOG.md | 11 +- appEditors/AppTextEditor.py | 6 +- appGUI/GUIElements.py | 348 ++++++++++++++++++--- appGUI/MainGUI.py | 10 +- appGUI/preferences/PreferencesUIManager.py | 4 +- appObjects/FlatCAMScript.py | 2 +- appTranslation.py | 4 +- app_Main.py | 213 ++++++++----- assets/examples/cutout_a_gerber.FlatScript | 2 +- assets/examples/isolate_gerber.FlatScript | 2 +- assets/examples/open_file.FlatScript | 2 +- camlib.py | 1 + tclCommands/TclCommandNew.py | 2 +- 13 files changed, 467 insertions(+), 140 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc03f8d..cce3b54a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ CHANGELOG for FlatCAM beta ================================================= +20.12.2020 + +- modified the way the status bar icon is set +- Drilling Tool - fixed missing feedrate code when the toolchange is Off +- AppTextEditor - working on syntax highlighting +- App - trying to speed up the new project creation +- Tcl Shell - Browser Edit - added Undo/Redo, Cut and Delete selection +- replace all the exec_() calls with exec() (except one situation in Tcl where I'm not sure of the effect) + 18.12.2020 - fixed the Tcl command export_gcode to return the actual gcode @@ -5433,7 +5442,7 @@ saving an Excellon object from editor to FlatCAM, selecting drills by left click 19.09.2018 -- optimized loading FlatCAM project by double clicking on project file; there is no need to clean up everything by using the function not Thread Safe: on_file_new() because there is nothing to clean since FlatCAM just started. +- optimized loading FlatCAM project by double clicking on project file; there is no need to clean up everything by using the function not Thread Safe: on_file_new_project() because there is nothing to clean since FlatCAM just started. - added a workspace delimitation with sizes A3, A4 and landscape or portrait format - The Workspace checkbox in Preferences GUI is doing toggle on the workspace diff --git a/appEditors/AppTextEditor.py b/appEditors/AppTextEditor.py index 8de4f807..387ceaea 100644 --- a/appEditors/AppTextEditor.py +++ b/appEditors/AppTextEditor.py @@ -25,9 +25,11 @@ if '_' not in builtins.__dict__: class AppTextEditor(QtWidgets.QWidget): - def __init__(self, app, text=None, plain_text=None, parent=None): + def __init__(self, app, text=None, plain_text=None, color_dict=None, parent=None): super().__init__(parent=parent) + if color_dict is None: + color_dict = {} self.app = app self.plain_text = plain_text self.callback = lambda x: None @@ -51,7 +53,7 @@ class AppTextEditor(QtWidgets.QWidget): # CODE Editor if self.plain_text: - self.editor_class = FCTextAreaLineNumber() + self.editor_class = FCTextAreaLineNumber(color_dict=color_dict) self.code_editor = self.editor_class.edit stylesheet = """ diff --git a/appGUI/GUIElements.py b/appGUI/GUIElements.py index ebb07b16..53c4952d 100644 --- a/appGUI/GUIElements.py +++ b/appGUI/GUIElements.py @@ -1901,7 +1901,7 @@ class FCPlainTextAreaExtended(QtWidgets.QPlainTextEdit): self.menu.addSeparator() # CUT - self. cut_action = QAction('%s\t%s' % (_("Cut"), _('Ctrl+X')), self) + self.cut_action = QAction('%s\t%s' % (_("Cut"), _('Ctrl+X')), self) self.menu.addAction(self.cut_action) self.cut_action.triggered.connect(self.cut_text) @@ -2368,7 +2368,7 @@ class FCInputDoubleSpinner(QtWidgets.QDialog): self.wdg.set_value(val) def get_value(self): - if self.exec_() == QtWidgets.QDialog.Accepted: + if self.exec() == QtWidgets.QDialog.Accepted: return self.wdg.get_value(), True else: return None, False @@ -2435,7 +2435,7 @@ class FCInputSpinner(QtWidgets.QDialog): self.wdg.spinner.set_step(val) def get_value(self): - if self.exec_() == QtWidgets.QDialog.Accepted: + if self.exec() == QtWidgets.QDialog.Accepted: return [self.wdg.get_value(), True] else: return [None, False] @@ -2496,7 +2496,7 @@ class FCInputDialogSlider(QtWidgets.QDialog): self.wdg.spinner.set_step(val) def get_results(self): - if self.exec_() == QtWidgets.QDialog.Accepted: + if self.exec() == QtWidgets.QDialog.Accepted: return self.wdg.get_value(), True else: return None, False @@ -2567,7 +2567,7 @@ class FCInputDialogSpinnerButton(QtWidgets.QDialog): self.wdg.spinner.set_value(val) def get_results(self): - if self.exec_() == QtWidgets.QDialog.Accepted: + if self.exec() == QtWidgets.QDialog.Accepted: return self.wdg.get_value(), True else: return None, False @@ -3751,8 +3751,8 @@ class FCTable(QtWidgets.QTableWidget): elif rect.bottom() - pos.y() < margin: return True # noinspection PyTypeChecker - return rect.contains(pos, True) and not (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) and \ - pos.y() >= rect.center().y() + drop_enabled = int(self.model().flags(index)) & Qt.ItemIsDropEnabled + return rect.contains(pos, True) and not drop_enabled and pos.y() >= rect.center().y() class SpinBoxDelegate(QtWidgets.QItemDelegate): @@ -3898,7 +3898,7 @@ class DialogBoxRadio(QtWidgets.QDialog): self.readyToEdit = True - if self.exec_() == QtWidgets.QDialog.Accepted: + if self.exec() == QtWidgets.QDialog.Accepted: self.ok = True self.location = self.lineEdit.text() self.reference = self.ref_radio.get_value() @@ -3915,46 +3915,104 @@ class _BrowserTextEdit(QTextEdit): self.app = app self.find_text = lambda: None - def contextMenuEvent(self, event): - # self.menu = self.createStandardContextMenu(event.pos()) + self.isUndoAvailable = False + self.isRedoAvailable = False + + # Context Menu Construction self.menu = QtWidgets.QMenu() - tcursor = self.textCursor() - txt = tcursor.selectedText() - copy_action = QAction('%s\t%s' % (_("Copy"), _('Ctrl+C')), self) - self.menu.addAction(copy_action) - copy_action.triggered.connect(self.copy_text) - if txt == '': - copy_action.setDisabled(True) + # UNDO + self.undo_action = QAction('%s\t%s' % (_("Undo"), _('Ctrl+Z')), self) + self.menu.addAction(self.undo_action) + self.undo_action.triggered.connect(self.undo) + + # REDO + self.redo_action = QAction('%s\t%s' % (_("Redo"), _('Ctrl+Y')), self) + self.menu.addAction(self.redo_action) + self.redo_action.triggered.connect(self.redo) self.menu.addSeparator() - sel_all_action = QAction('%s\t%s' % (_("Select All"), _('Ctrl+A')), self) - self.menu.addAction(sel_all_action) - sel_all_action.triggered.connect(self.selectAll) + # CUT + self.cut_action = QAction('%s\t%s' % (_("Cut"), _('Ctrl+X')), self) + self.menu.addAction(self.cut_action) + self.cut_action.triggered.connect(self.cut_text) - find_action = QAction('%s\t%s' % (_("Find"), _('Ctrl+F')), self) - self.menu.addAction(find_action) - find_action.triggered.connect(self.find_text) + # Copy + self.copy_action = QAction('%s\t%s' % (_("Copy"), _('Ctrl+C')), self) + self.menu.addAction(self.copy_action) + self.copy_action.triggered.connect(self.copy_text) + + # Delete + self.delete_action = QAction('%s\t%s' % (_("Delete"), _('Del')), self) + self.menu.addAction(self.delete_action) + self.delete_action.triggered.connect(self.delete_text) self.menu.addSeparator() + # Select All + self.sel_all_action = QAction('%s\t%s' % (_("Select All"), _('Ctrl+A')), self) + self.menu.addAction(self.sel_all_action) + self.sel_all_action.triggered.connect(self.selectAll) + + # Find + self.find_action = QAction('%s\t%s' % (_("Find"), _('Ctrl+F')), self) + self.menu.addAction(self.find_action) + self.find_action.triggered.connect(self.find_text) + + self.menu.addSeparator() + + # Save if self.app: - save_action = QAction('%s\t%s' % (_("Save Log"), _('Ctrl+S')), self) + self.save_action = QAction('%s\t%s' % (_("Save Log"), _('Ctrl+S')), self) # save_action.setShortcut(QKeySequence(Qt.Key_S)) - self.menu.addAction(save_action) - save_action.triggered.connect(lambda: self.save_log(app=self.app)) + self.menu.addAction(self.save_action) + self.save_action.triggered.connect(lambda: self.save_log(app=self.app)) - clear_action = QAction('%s\t%s' % (_("Clear All"), _('Del')), self) + # Clear + self.clear_action = QAction('%s\t%s' % (_("Clear All"), _('Shift+Del')), self) # clear_action.setShortcut(QKeySequence(Qt.Key_Delete)) - self.menu.addAction(clear_action) - clear_action.triggered.connect(self.clear) + self.menu.addAction(self.clear_action) + self.clear_action.triggered.connect(self.clear) + # Close the Dock # if self.app: # close_action = QAction(_("Close"), self) # self.menu.addAction(close_action) # close_action.triggered.connect(lambda: self.app.ui.shell_dock.hide()) + # Signals + self.undoAvailable.connect(self.on_undo_available) + self.redoAvailable.connect(self.on_redo_available) + + def on_undo_available(self, state): + self.isUndoAvailable = state + + def on_redo_available(self, state): + self.isRedoAvailable = state + + def contextMenuEvent(self, event): + # self.menu = self.createStandardContextMenu(event.pos()) + tcursor = self.textCursor() + txt = tcursor.selectedText() + + if self.isUndoAvailable is True: + self.undo_action.setDisabled(False) + else: + self.undo_action.setDisabled(True) + + if self.isRedoAvailable is True: + self.redo_action.setDisabled(False) + else: + self.redo_action.setDisabled(True) + + if txt == '': + self.cut_action.setDisabled(True) + self.copy_action.setDisabled(True) + else: + self.cut_action.setDisabled(False) + self.copy_action.setDisabled(False) + self.menu.exec_(event.globalPos()) def keyPressEvent(self, event) -> None: @@ -3968,18 +4026,34 @@ class _BrowserTextEdit(QTextEdit): # Copy Text elif key == QtCore.Qt.Key_C: self.copy_text() - # Copy Text + # Find Text elif key == QtCore.Qt.Key_F: self.find_text() # Save Log elif key == QtCore.Qt.Key_S: if self.app: self.save_log(app=self.app) + # Cut Text + elif key == QtCore.Qt.Key_X: + self.cut_text() + # Undo Text + elif key == QtCore.Qt.Key_Z: + if self.isUndoAvailable: + self.undo() + # Redo Text + elif key == QtCore.Qt.Key_Y: + if self.isRedoAvailable: + self.redo() + + if modifiers == QtCore.Qt.ShiftModifier: + # Clear all + if key == QtCore.Qt.Key_Delete: + self.clear() elif modifiers == QtCore.Qt.NoModifier: # Clear all if key == QtCore.Qt.Key_Delete: - self.clear() + self.delete_text() # Shell toggle if key == QtCore.Qt.Key_S: self.app.ui.toggle_shell_ui() @@ -3992,6 +4066,26 @@ class _BrowserTextEdit(QTextEdit): clipboard.clear() clipboard.setText(txt) + def cut_text(self): + tcursor = self.textCursor() + clipboard = QtWidgets.QApplication.clipboard() + + txt = tcursor.selectedText() + clipboard.clear() + clipboard.setText(txt) + + tcursor.removeSelectedText() + + def delete_text(self): + tcursor = self.textCursor() + txt = tcursor.selectedText() + if txt == '': + tcursor.deleteChar() + else: + tcursor.removeSelectedText() + + self.setTextCursor(tcursor) + def clear(self): QTextEdit.clear(self) @@ -4209,15 +4303,163 @@ class FCTextAreaLineNumber(QtWidgets.QFrame): and from here: https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html """ - def __init__(self, *args): + def __init__(self, *args, color_dict=None): FCPlainTextAreaExtended.__init__(self, *args) + self.color_storage = color_dict if color_dict else {} + # self.setFrameStyle(QFrame.NoFrame) self.setFrameStyle(QtWidgets.QFrame.NoFrame) self.highlight() # self.setLineWrapMode(QPlainTextEdit.NoWrap) self.cursorPositionChanged.connect(self.highlight) + self.highlighter = self.MyHighlighter(self.document()) + + class MyHighlighter(QtGui.QSyntaxHighlighter): + + def __init__(self, parent, highlight_rules=None): + QtGui.QSyntaxHighlighter.__init__(self, parent) + self.parent = parent + self.highlightingRules = [] + + if highlight_rules is None: + reservedClasses = QtGui.QTextCharFormat() + parameterOperator = QtGui.QTextCharFormat() + delimiter = QtGui.QTextCharFormat() + specialConstant = QtGui.QTextCharFormat() + boolean = QtGui.QTextCharFormat() + number = QtGui.QTextCharFormat() + string = QtGui.QTextCharFormat() + singleQuotedString = QtGui.QTextCharFormat() + + comment = QtGui.QTextCharFormat() + # comment + brush = QtGui.QBrush(Qt.gray, Qt.SolidPattern) + pattern = QtCore.QRegExp("\(.*\)") + comment.setForeground(brush) + rule = (pattern, comment) + self.highlightingRules.append(rule) + + # Marlin comment + brush = QtGui.QBrush(Qt.gray, Qt.SolidPattern) + pattern = QtCore.QRegExp("^;\s*.*$") + comment.setForeground(brush) + rule = (pattern, comment) + self.highlightingRules.append(rule) + + # Python comment + brush = QtGui.QBrush(Qt.gray, Qt.SolidPattern) + pattern = QtCore.QRegExp("^\#\s*.*$") + comment.setForeground(brush) + rule = (pattern, comment) + self.highlightingRules.append(rule) + + keyword = QtGui.QTextCharFormat() + # keyword + brush = QtGui.QBrush(Qt.blue, Qt.SolidPattern) + keyword.setForeground(brush) + keyword.setFontWeight(QtGui.QFont.Bold) + keywords = ["F", "G", "M", "T"] + for word in keywords: + # pattern = QtCore.QRegExp("\\b" + word + "\\b") + pattern = QtCore.QRegExp("\\b" + word + "\d+(\.\d*)?\s?" + "\\b") + rule = (pattern, keyword) + self.highlightingRules.append(rule) + + # reservedClasses + reservedClasses.setForeground(brush) + reservedClasses.setFontWeight(QtGui.QFont.Bold) + keywords = ["array", "character", "complex", "data.frame", "double", "factor", + "function", "integer", "list", "logical", "matrix", "numeric", "vector"] + for word in keywords: + pattern = QtCore.QRegExp("\\b" + word + "\\b") + rule = (pattern, reservedClasses) + self.highlightingRules.append(rule) + + # parameter + brush = QtGui.QBrush(Qt.darkBlue, Qt.SolidPattern) + pattern = QtCore.QRegExp("\-[0-9a-zA-Z]*\s") + parameterOperator.setForeground(brush) + parameterOperator.setFontWeight(QtGui.QFont.Bold) + rule = (pattern, parameterOperator) + self.highlightingRules.append(rule) + + # delimiter + pattern = QtCore.QRegExp("[\)\(]+|[\{\}]+|[][]+") + delimiter.setForeground(brush) + delimiter.setFontWeight(QtGui.QFont.Bold) + rule = (pattern, delimiter) + self.highlightingRules.append(rule) + + # specialConstant + brush = QtGui.QBrush(Qt.green, Qt.SolidPattern) + specialConstant.setForeground(brush) + keywords = ["Inf", "NA", "NaN", "NULL"] + for word in keywords: + pattern = QtCore.QRegExp("\\b" + word + "\\b") + rule = (pattern, specialConstant) + self.highlightingRules.append(rule) + + # boolean + boolean.setForeground(brush) + keywords = ["TRUE", "True","FALSE", "False"] + for word in keywords: + pattern = QtCore.QRegExp("\\b" + word + "\\b") + rule = (pattern, boolean) + self.highlightingRules.append(rule) + + # number + # pattern = QtCore.QRegExp("[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?") + # pattern.setMinimal(True) + # number.setForeground(brush) + # rule = (pattern, number) + # self.highlightingRules.append(rule) + + # string + brush = QtGui.QBrush(Qt.red, Qt.SolidPattern) + pattern = QtCore.QRegExp("\".*\"") + pattern.setMinimal(True) + string.setForeground(brush) + rule = (pattern, string) + self.highlightingRules.append(rule) + + # singleQuotedString + pattern = QtCore.QRegExp("\'.*\'") + pattern.setMinimal(True) + singleQuotedString.setForeground(brush) + rule = (pattern, singleQuotedString) + self.highlightingRules.append(rule) + else: + self.highlightingRules = highlight_rules + + def highlightBlock(self, text): + """ + + :param text: string + :type text: str + :return: + :rtype: + """ + + # go in reverse from the last element to the first + for rule in self.highlightingRules[::-1]: + expression = QtCore.QRegExp(rule[0]) + index = expression.indexIn(text) + while index >= 0: + length = expression.matchedLength() + self.setFormat(index, length, rule[1]) + index = expression.indexIn(text, index + length) + self.setCurrentBlockState(0) + + # def format_text_color(self): + # cursor = self.textCursor() + # c_format = cursor.charFormat() + # old_color = c_format.foreground() + # + # for key in self.color_storage: + # pass + def highlight(self): hi_selection = QTextEdit.ExtraSelection() @@ -4272,12 +4514,12 @@ class FCTextAreaLineNumber(QtWidgets.QFrame): painter.end() - def __init__(self, *args): + def __init__(self, *args, color_dict=None): QtWidgets.QFrame.__init__(self, *args) self.setFrameStyle(QtWidgets.QFrame.StyledPanel | QtWidgets.QFrame.Sunken) - self.edit = self.PlainTextEdit() + self.edit = self.PlainTextEdit(color_dict=color_dict) self.number_bar = self.NumberBar(self.edit) hbox = QtWidgets.QHBoxLayout(self) @@ -4683,6 +4925,12 @@ class FlatCAMInfoBar(QtWidgets.QWidget): layout.addWidget(self.icon) + self.red_pmap = QtGui.QPixmap(self.app.resource_location + '/redlight12.png') + self.green_pmap = QtGui.QPixmap(self.app.resource_location + '/greenlight12.png') + self.yellow_pamap = QtGui.QPixmap(self.app.resource_location + '/yellowlight12.png') + self.blue_pamap = QtGui.QPixmap(self.app.resource_location + '/bluelight12.png') + self.gray_pmap = QtGui.QPixmap(self.app.resource_location + '/graylight12.png') + self.text = QtWidgets.QLabel(self) self.text.setText(_("Application started ...")) self.text.setToolTip(_("Hello!")) @@ -4700,23 +4948,27 @@ class FlatCAMInfoBar(QtWidgets.QWidget): level = str(level) if self.lock_pmaps is not True: - self.pmap.fill() - if level == "ERROR" or level == "ERROR_NOTCL": - self.pmap = QtGui.QPixmap(self.app.resource_location + '/redlight12.png') - elif level.lower() == "success": - self.pmap = QtGui.QPixmap(self.app.resource_location + '/greenlight12.png') - elif level == "WARNING" or level == "WARNING_NOTCL": - self.pmap = QtGui.QPixmap(self.app.resource_location + '/yellowlight12.png') - elif level.lower() == "selected": - self.pmap = QtGui.QPixmap(self.app.resource_location + '/bluelight12.png') - else: - self.pmap = QtGui.QPixmap(self.app.resource_location + '/graylight12.png') + # self.pmap.fill() + + try: + if level == "ERROR" or level == "ERROR_NOTCL": + self.icon.setPixmap(self.red_pmap) + elif level.lower() == "success": + self.icon.setPixmap(self.green_pmap) + elif level == "WARNING" or level == "WARNING_NOTCL": + self.icon.setPixmap(self.yellow_pamap) + elif level.lower() == "selected": + self.icon.setPixmap(self.blue_pamap) + else: + self.icon.setPixmap(self.gray_pmap) + + except Exception as e: + log.debug("FlatCAMInfoBar.set_status() set Icon --> %s" % str(e)) try: self.set_text_(text) - self.icon.setPixmap(self.pmap) except Exception as e: - log.debug("FlatCAMInfoBar.set_status() --> %s" % str(e)) + self.app.log.debug("FlatCAMInfoBar.set_status() set Text --> %s" % str(e)) class FlatCAMSystemTray(QtWidgets.QSystemTrayIcon): @@ -4806,7 +5058,7 @@ def message_dialog(title, message, kind="info", parent=None): "error": QtWidgets.QMessageBox.Critical}[str(kind)] dlg = QtWidgets.QMessageBox(icon, title, message, parent=parent) dlg.setText(message) - dlg.exec_() + dlg.exec() def rreplace(s, old, new, occurrence): diff --git a/appGUI/MainGUI.py b/appGUI/MainGUI.py index 6fe4e467..569eb3a8 100644 --- a/appGUI/MainGUI.py +++ b/appGUI/MainGUI.py @@ -2175,7 +2175,7 @@ class MainGUI(QtWidgets.QMainWindow): bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole) msgbox.setDefaultButton(bt_no) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if forced_clear is True or response == bt_yes: @@ -3045,7 +3045,7 @@ class MainGUI(QtWidgets.QMainWindow): messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok) messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok) - messagebox.exec_() + messagebox.exec() return # SHIFT elif modifiers == QtCore.Qt.ShiftModifier: @@ -3205,7 +3205,7 @@ class MainGUI(QtWidgets.QMainWindow): messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok) messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok) - messagebox.exec_() + messagebox.exec() # Paint if key == QtCore.Qt.Key_I or key == 'I': @@ -3252,7 +3252,7 @@ class MainGUI(QtWidgets.QMainWindow): messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok) messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok) - messagebox.exec_() + messagebox.exec() # Add Text Tool if key == QtCore.Qt.Key_T or key == 'T': @@ -3274,7 +3274,7 @@ class MainGUI(QtWidgets.QMainWindow): messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok) messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok) - messagebox.exec_() + messagebox.exec() # Flip on X axis if key == QtCore.Qt.Key_X or key == 'X': diff --git a/appGUI/preferences/PreferencesUIManager.py b/appGUI/preferences/PreferencesUIManager.py index 04eef80c..cb32883b 100644 --- a/appGUI/preferences/PreferencesUIManager.py +++ b/appGUI/preferences/PreferencesUIManager.py @@ -953,7 +953,7 @@ class PreferencesUIManager: msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.NoRole) msgbox.setDefaultButton(bt_yes) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if theme_new_val != theme: @@ -1201,7 +1201,7 @@ class PreferencesUIManager: msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole) msgbox.setDefaultButton(bt_yes) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if response == bt_yes: diff --git a/appObjects/FlatCAMScript.py b/appObjects/FlatCAMScript.py index cd3107f9..b4a5f32e 100644 --- a/appObjects/FlatCAMScript.py +++ b/appObjects/FlatCAMScript.py @@ -178,7 +178,7 @@ class ScriptObject(FlatCAMObj): self.app.shell.open_processing() # Disables input box. # make sure that the pixmaps are not updated when running this as they will crash - # TODO find why the pixmaps load crash when run from this object (perhaps another thread?) + # TODO find why the pixmaps for the whole app load crash when run from this object (perhaps another thread?) self.app.ui.fcinfo.lock_pmaps = True self.script_code = self.script_editor_tab.code_editor.toPlainText() diff --git a/appTranslation.py b/appTranslation.py index 61ada23d..9b2a49ad 100644 --- a/appTranslation.py +++ b/appTranslation.py @@ -112,7 +112,7 @@ def on_language_apply_click(app, restart=False): bt_no = msgbox.addButton(_("No"), QtWidgets.QMessageBox.NoRole) msgbox.setDefaultButton(bt_yes) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if response == bt_no: @@ -214,7 +214,7 @@ def restart_program(app, ask=None): msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole) msgbox.setDefaultButton(bt_yes) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if response == bt_yes: diff --git a/app_Main.py b/app_Main.py index 969c9726..d7bad8fe 100644 --- a/app_Main.py +++ b/app_Main.py @@ -219,6 +219,17 @@ class App(QtCore.QObject): # File type and filename file_saved = QtCore.pyqtSignal(str, str) + # close app signal + close_app_signal = pyqtSignal() + + # will perform the cleanup operation after a Graceful Exit + # usefull for the NCC Tool and Paint Tool where some progressive plotting might leave + # graphic residues behind + cleanup = pyqtSignal() + + # emitted when the new_project is created in a threaded way + new_project_signal = pyqtSignal() + # Percentage of progress progress = QtCore.pyqtSignal(int) @@ -250,14 +261,6 @@ class App(QtCore.QObject): # signal emitted when jumping locate_signal = pyqtSignal(tuple, str) - # close app signal - close_app_signal = pyqtSignal() - - # will perform the cleanup operation after a Graceful Exit - # usefull for the NCC Tool and Paint Tool where some progressive plotting might leave - # graphic residues behind - cleanup = pyqtSignal() - proj_selection_changed = pyqtSignal(object, object) def __init__(self, qapp, user_defaults=True): @@ -1973,14 +1976,15 @@ class App(QtCore.QObject): for act in self.ui.menutool.actions(): self.ui.menutool.removeAction(act) - def init_tools(self): + def init_tools(self, init_tcl=True): """ Initialize the Tool tab in the notebook side of the central widget. Remove the actions in the Tools menu. Instantiate again the FlatCAM tools (plugins). All this is required when changing the layout: standard, compact etc. - :return: None + :param init_tcl: Bool. If True will init the Tcl Shell + :return: None """ self.log.debug("init_tools()") @@ -1992,12 +1996,12 @@ class App(QtCore.QObject): self.ui.notebook.removeTab(2) # rebuild the Tools Tab - self.ui.tool_tab = QtWidgets.QWidget() - self.ui.tool_tab_layout = QtWidgets.QVBoxLayout(self.ui.tool_tab) - self.ui.tool_tab_layout.setContentsMargins(2, 2, 2, 2) - self.ui.notebook.addTab(self.ui.tool_tab, _("Tool")) - self.ui.tool_scroll_area = VerticalScrollArea() - self.ui.tool_tab_layout.addWidget(self.ui.tool_scroll_area) + # self.ui.tool_tab = QtWidgets.QWidget() + # self.ui.tool_tab_layout = QtWidgets.QVBoxLayout(self.ui.tool_tab) + # self.ui.tool_tab_layout.setContentsMargins(2, 2, 2, 2) + # self.ui.notebook.addTab(self.ui.tool_tab, _("Tool")) + # self.ui.tool_scroll_area = VerticalScrollArea() + # self.ui.tool_tab_layout.addWidget(self.ui.tool_scroll_area) # reinstall all the Tools as some may have been removed when the data was removed from the Tools Tab # first remove all of them @@ -2010,7 +2014,7 @@ class App(QtCore.QObject): # third install all of them try: - self.install_tools(init_tcl=True) + self.install_tools(init_tcl=init_tcl) except AttributeError: pass @@ -2169,7 +2173,7 @@ class App(QtCore.QObject): self.ui.popmenu_new_geo.triggered.connect(self.app_obj.new_geometry_object) self.ui.popmenu_new_grb.triggered.connect(self.app_obj.new_gerber_object) self.ui.popmenu_new_exc.triggered.connect(self.app_obj.new_excellon_object) - self.ui.popmenu_new_prj.triggered.connect(self.f_handlers.on_file_new) + self.ui.popmenu_new_prj.triggered.connect(self.f_handlers.on_file_new_project) self.ui.zoomfit.triggered.connect(self.on_zoom_fit) self.ui.clearplot.triggered.connect(self.clear_plots) @@ -2243,7 +2247,7 @@ class App(QtCore.QObject): # Toolbar # File Toolbar Signals - # ui.file_new_btn.triggered.connect(self.on_file_new) + # ui.file_new_btn.triggered.connect(self.on_file_new_project) self.ui.file_open_btn.triggered.connect(self.f_handlers.on_file_openproject) self.ui.file_save_btn.triggered.connect(self.f_handlers.on_file_saveproject) self.ui.file_open_gerber_btn.triggered.connect(self.f_handlers.on_fileopengerber) @@ -2285,7 +2289,7 @@ class App(QtCore.QObject): def on_layout(self, index=None, lay=None, connect_signals=True): """ - Set the toolbars layout (location) + Set the toolbars layout (location). :param connect_signals: Useful when used in the App.__init__(); bool :param index: @@ -2584,7 +2588,8 @@ class App(QtCore.QObject): """ Transfers the Geometry or Excellon from it's editor to the current object. - :return: None + :param cleanup: if True then we closed the app when the editor was open so we close first the editor + :return: None """ self.defaults.report_usage("editor2object()") @@ -2609,7 +2614,7 @@ class App(QtCore.QObject): bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.RejectRole) msgbox.setDefaultButton(bt_yes) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if response == bt_yes: @@ -2843,10 +2848,10 @@ class App(QtCore.QObject): @QtCore.pyqtSlot(str, bool) def info(self, msg, shell_echo=True): """ - Informs the user. Normally on the status bar, optionally - also on the shell. + Informs the user. Normally on the status bar, optionally also on the shell. - :param msg: Text to write. + :param msg: Text to write. Composed from a first part between brackets which is the level and the rest + which is the message. The level part will control the text color and the used icon :type msg: str :param shell_echo: Control if to display the message msg in the Shell :type shell_echo: bool @@ -2890,13 +2895,28 @@ class App(QtCore.QObject): self.shell_message(msg) def info_shell(self, msg, new_line=True): + """ + A handler for a signal that call for printing directly on the Tcl Shell without printing in status bar. + + :param msg: The message to be printed + :type msg: str + :param new_line: if True then after printing the message add a new line char + :type new_line: bool + :return: None + :rtype: None + """ self.shell_message(msg=msg, new_line=new_line) def save_to_file(self, content_to_save, txt_content): """ Save something to a file. - :return: None + :param content_to_save: text when is in HTML + :type content_to_save: str + :param txt_content: text that is not HTML + :type txt_content: str + :return: None + :rtype: None """ self.defaults.report_usage("save_to_file") self.log.debug("save_to_file()") @@ -2966,8 +2986,8 @@ class App(QtCore.QObject): Will register the files opened into record dictionaries. The FlatCAM projects has it's own dictionary. - :param kind: type of file that was opened - :param filename: the path and file name for the file that was opened + :param kind: type of file that was opened + :param filename: the path and file name for the file that was opened :return: """ self.log.debug("register_recent()") @@ -3339,7 +3359,7 @@ class App(QtCore.QObject): closebtn.clicked.connect(self.accept) - AboutDialog(app=self, parent=self.ui).exec_() + AboutDialog(app=self, parent=self.ui).exec() def on_howto(self): """ @@ -3516,14 +3536,14 @@ class App(QtCore.QObject): # BUTTONS section closebtn.clicked.connect(self.accept) - HowtoDialog(app=self, parent=self.ui).exec_() + HowtoDialog(app=self, parent=self.ui).exec() def install_bookmarks(self, book_dict=None): """ Install the bookmarks actions in the Help menu -> Bookmarks - :param book_dict: a dict having the actions text as keys and the weblinks as the values - :return: None + :param book_dict: a dict having the actions text as keys and the weblinks as the values + :return: None """ if book_dict is None: @@ -3582,7 +3602,8 @@ class App(QtCore.QObject): def on_bookmarks_manager(self): """ - Adds the bookmark manager in a Tab in Plot Area + Adds the bookmark manager in a Tab in Plot Area. + :return: """ for idx in range(self.ui.plot_tab_area.count()): @@ -3590,7 +3611,7 @@ class App(QtCore.QObject): # there can be only one instance of Bookmark Manager at one time return - # BookDialog(app=self, storage=self.defaults["global_bookmarks"], parent=self.ui).exec_() + # BookDialog(app=self, storage=self.defaults["global_bookmarks"], parent=self.ui).exec() self.book_dialog_tab = BookmarkManager(app=self, storage=self.defaults["global_bookmarks"], parent=self.ui) self.book_dialog_tab.setObjectName("bookmarks_tab") @@ -3609,6 +3630,12 @@ class App(QtCore.QObject): self.ui.plot_tab_area.setCurrentWidget(self.book_dialog_tab) def on_backup_site(self): + """ + Called when the user click on the menu entry Help -> Bookmarks -> Backup Site + + :return: None + :rtype: None + """ msgbox = QtWidgets.QMessageBox() msgbox.setText(_("This entry will resolve to another website if:\n\n" "1. FlatCAM.org website is down\n" @@ -3624,7 +3651,7 @@ class App(QtCore.QObject): bt_yes = msgbox.addButton(_('Close'), QtWidgets.QMessageBox.YesRole) msgbox.setDefaultButton(bt_yes) - msgbox.exec_() + msgbox.exec() # response = msgbox.clickedButton() def final_save(self): @@ -3653,7 +3680,7 @@ class App(QtCore.QObject): bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.RejectRole) msgbox.setDefaultButton(bt_yes) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if response == bt_yes: @@ -4585,7 +4612,7 @@ class App(QtCore.QObject): msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.RejectRole) msgbox.setDefaultButton(bt_ok) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if response == bt_ok: @@ -4748,7 +4775,7 @@ class App(QtCore.QObject): bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole) msgbox.setDefaultButton(bt_ok) - msgbox.exec_() + msgbox.exec() # work only if the notebook tab on focus is the Tools_Tab if notebook_widget_name == 'tool_tab': @@ -4838,7 +4865,7 @@ class App(QtCore.QObject): msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.RejectRole) msgbox.setDefaultButton(bt_ok) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if self.defaults["global_delete_confirmation"] is False or force_deletion is True: @@ -5137,7 +5164,7 @@ class App(QtCore.QObject): self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) - if self.exec_() == QtWidgets.QDialog.Accepted: + if self.exec() == QtWidgets.QDialog.Accepted: self.ok = True self.location_point = self.ref_radio.get_value() else: @@ -5401,7 +5428,7 @@ class App(QtCore.QObject): self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) - if self.exec_() == QtWidgets.QDialog.Accepted: + if self.exec() == QtWidgets.QDialog.Accepted: self.ok = True self.location_point = self.ref_radio.get_value() else: @@ -6350,7 +6377,7 @@ class App(QtCore.QObject): msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole) msgbox.setDefaultButton(bt_yes) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if response == bt_yes: @@ -7045,10 +7072,12 @@ class App(QtCore.QObject): def selection_area_handler(self, start_pos, end_pos, sel_type): """ - :param start_pos: mouse position when the selection LMB click was done - :param end_pos: mouse position when the left mouse button is released - :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection - :return: + Called when the mouse selects by dragging left mouse button on canvas. + + :param start_pos: mouse position when the selection LMB click was done + :param end_pos: mouse position when the left mouse button is released + :param sel_type: if True it's a left to right selection (enclosure), if False it's a 'touch' selection + :return: None """ poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])]) @@ -7097,8 +7126,8 @@ class App(QtCore.QObject): """ Will select objects clicked on canvas - :param key: for future use in cumulative selection - :return: + :param key: a keyboard key. for future use in cumulative selection + :return: None """ # list where we store the overlapped objects under our mouse left click position @@ -7225,27 +7254,35 @@ class App(QtCore.QObject): self.log.error("[ERROR] Something went bad in App.select_objects(). %s" % str(e)) def selected_message(self, curr_sel_obj): + """ + Will print a colored message on status bar when the user selects an object on canvas. + + :param curr_sel_obj: Application object that have geometry: Geometry, Gerber, Excellon, CNCJob + :type curr_sel_obj: + :return: None + :rtype: None + """ if curr_sel_obj: if curr_sel_obj.kind == 'gerber': - self.inform.emit('[selected]{name} {tx}'.format( + self.inform.emit('[selected] {name} {tx}'.format( color='green', name=str(curr_sel_obj.options['name']), tx=_("selected")) ) elif curr_sel_obj.kind == 'excellon': - self.inform.emit('[selected]{name} {tx}'.format( + self.inform.emit('[selected] {name} {tx}'.format( color='brown', name=str(curr_sel_obj.options['name']), tx=_("selected")) ) elif curr_sel_obj.kind == 'cncjob': - self.inform.emit('[selected]{name} {tx}'.format( + self.inform.emit('[selected] {name} {tx}'.format( color='blue', name=str(curr_sel_obj.options['name']), tx=_("selected")) ) elif curr_sel_obj.kind == 'geometry': - self.inform.emit('[selected]{name} {tx}'.format( + self.inform.emit('[selected] {name} {tx}'.format( color='red', name=str(curr_sel_obj.options['name']), tx=_("selected")) @@ -8627,6 +8664,8 @@ class MenuFileHandlers(QtCore.QObject): self.pagesize = {} + self.app.new_project_signal.connect(self.on_new_project_house_keeping) + def on_fileopengerber(self, signal, name=None): """ File menu callback for opening a Gerber. @@ -8874,7 +8913,7 @@ class MenuFileHandlers(QtCore.QObject): msgbox.setInformativeText(msg) bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole) msgbox.setDefaultButton(bt_ok) - msgbox.exec_() + msgbox.exec() return name = obj.options["name"] @@ -9222,7 +9261,7 @@ class MenuFileHandlers(QtCore.QObject): msgbox.setInformativeText(msg) bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole) msgbox.setDefaultButton(bt_ok) - msgbox.exec_() + msgbox.exec() return @@ -9333,31 +9372,33 @@ class MenuFileHandlers(QtCore.QObject): bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.RejectRole) msgbox.setDefaultButton(bt_yes) - msgbox.exec_() + msgbox.exec() response = msgbox.clickedButton() if response == bt_yes: - self.on_file_saveprojectas() + self.on_file_saveprojectas(threaded=True) elif response == bt_cancel: return elif response == bt_no: - self.on_file_new() + self.on_file_new_project(threaded=True) else: - self.on_file_new() - self.inform.emit('[success] %s...' % _("New Project created")) + self.on_file_new_project(threaded=True) - def on_file_new(self, cli=None): + def on_file_new_project(self, cli=None, threaded=None): """ Returns the application to its startup state. This method is thread-safe. - :param cli: Boolean. If True this method was run from command line - :return: None + :param cli: Boolean. If True this method was run from command line + :param threaded: Bool. If True some part of the initialization are done threaded + :return: None """ - self.defaults.report_usage("on_file_new") + self.defaults.report_usage("on_file_new_project") # Remove everything from memory - self.log.debug("on_file_new()") + self.log.debug("on_file_new_project()") + + t0 = time.time() # close any editor that might be open if self.app.call_source != 'app': @@ -9367,9 +9408,6 @@ class MenuFileHandlers(QtCore.QObject): self.app.exc_editor = AppExcEditor(self.app) self.app.grb_editor = AppGerberEditor(self.app) - # Clear pool - self.app.clear_pool() - for obj in self.app.collection.get_list(): # delete shapes left drawn from mark shape_collections, if any if isinstance(obj, GerberObject): @@ -9393,9 +9431,6 @@ class MenuFileHandlers(QtCore.QObject): # delete the exclusion areas self.app.exc_areas.clear_shapes() - # tcl needs to be reinitialized, otherwise old shell variables etc remains - self.app.shell.init_tcl() - # delete any selection shape on canvas self.app.delete_selection_shape() @@ -9414,8 +9449,17 @@ class MenuFileHandlers(QtCore.QObject): # Re-fresh project options self.app.on_options_app2project() - # Init FlatCAMTools - self.app.init_tools() + if threaded is True: + self.app.new_project_signal.emit() + else: + # Clear pool + self.app.clear_pool() + + # Init FlatCAMTools + self.app.init_tools(init_tcl=True) + + # tcl needs to be reinitialized, otherwise old shell variables etc remains + # self.app.shell.init_tcl() # Try to close all tabs in the PlotArea but only if the appGUI is active (CLI is None) if cli is None: @@ -9427,7 +9471,7 @@ class MenuFileHandlers(QtCore.QObject): try: self.app.ui.plot_tab_area.closeTab(index) except Exception as e: - self.log.debug("App.on_file_new() --> %s" % str(e)) + self.log.debug("App.on_file_new_project() --> %s" % str(e)) # # And then add again the Plot Area self.app.ui.plot_tab_area.insertTab(0, self.app.ui.plot_tab, _("Plot Area")) @@ -9436,8 +9480,27 @@ class MenuFileHandlers(QtCore.QObject): # take the focus of the Notebook on Project Tab. self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab) + self.log.debug('%s: %s %s.' % (_("Project created in"), str(time.time() - t0), _("seconds"))) self.app.ui.set_ui_title(name=_("New Project - Not saved")) + self.inform.emit('[success] %s...' % _("New Project created")) + + def on_new_project_house_keeping(self): + """ + Do dome of the new project initialization in a threaded way + + :return: + :rtype: + """ + + # Clear pool + self.log.debug("New Project: cleaning multiprocessing pool.") + self.app.clear_pool() + + # Init FlatCAMTools + self.log.debug("New Project: initializing the Tools and Tcl Shell.") + self.app.init_tools(init_tcl=True) + def on_filenewscript(self, silent=False): """ Will create a new script file and open it in the Code Editor @@ -10862,7 +10925,7 @@ class MenuFileHandlers(QtCore.QObject): 1) Loads and parses file 2) Registers the file as recently opened. - 3) Calls on_file_new() + 3) Calls on_file_new_project() 4) Updates options 5) Calls app_obj.new_object() with the object's from_dict() as init method. 6) Calls plot_all() if plot=True @@ -10938,7 +11001,7 @@ class MenuFileHandlers(QtCore.QObject): elif cli is True: self.app.delete_selection_shape() else: - self.on_file_new() + self.on_file_new_project() # Project options self.app.options.update(d['options']) diff --git a/assets/examples/cutout_a_gerber.FlatScript b/assets/examples/cutout_a_gerber.FlatScript index 169ef912..0f439102 100644 --- a/assets/examples/cutout_a_gerber.FlatScript +++ b/assets/examples/cutout_a_gerber.FlatScript @@ -3,7 +3,7 @@ # Will cut a PCB piece with a pattern (Gerber file) out of the surrounding PCB material # ##################################################################################### -puts "**************** RUNNING an EXAMPLE SCRIPT = Cutout a Gerber file *******************" +puts "\n**************** RUNNING an EXAMPLE SCRIPT = Cutout a Gerber file *******************\n" # ----------- START: This is needed only for the examples ---------------- # first set the default location where to search for the files to be open and store it to the ROOT_FOLDER variable diff --git a/assets/examples/isolate_gerber.FlatScript b/assets/examples/isolate_gerber.FlatScript index d2d0478c..46ada146 100644 --- a/assets/examples/isolate_gerber.FlatScript +++ b/assets/examples/isolate_gerber.FlatScript @@ -3,7 +3,7 @@ # Will isolate copper features in a Gerber file by creating surrounding paths around # ##################################################################################### -puts "**************** RUNNING an EXAMPLE SCRIPT = Isolate a Gerber file *******************" +puts "\n**************** RUNNING an EXAMPLE SCRIPT = Isolate a Gerber file *******************\n" # ----------- START: This is needed only for the examples ---------------- # first set the default location where to search for the files to be open and store it to the ROOT_FOLDER variable diff --git a/assets/examples/open_file.FlatScript b/assets/examples/open_file.FlatScript index ed84b7ef..3ea84664 100644 --- a/assets/examples/open_file.FlatScript +++ b/assets/examples/open_file.FlatScript @@ -3,7 +3,7 @@ # Will open a Gerber (and Excellon) file in FlatCAM # ##################################################################################### -puts "**************** RUNNING an EXAMPLE SCRIPT = Open a file *******************" +puts "\n**************** RUNNING an EXAMPLE SCRIPT = Open a file *******************\n" # ----------- START: This is needed only for the examples ---------------- # first set the default location where to search for the files to be open and store it to the ROOT_FOLDER variable diff --git a/camlib.py b/camlib.py index 771261ff..bc161051 100644 --- a/camlib.py +++ b/camlib.py @@ -3306,6 +3306,7 @@ class CNCjob(Geometry): t_gcode += self.doformat(p.toolchange_code, toolchangexy=(temp_locx, temp_locy)) t_gcode += self.doformat(p.z_feedrate_code) else: + t_gcode += self.doformat(p.z_feedrate_code) t_gcode += self.doformat(p.lift_code) # Spindle start diff --git a/tclCommands/TclCommandNew.py b/tclCommands/TclCommandNew.py index 9a2af1f2..bea2945a 100644 --- a/tclCommands/TclCommandNew.py +++ b/tclCommands/TclCommandNew.py @@ -39,4 +39,4 @@ class TclCommandNew(TclCommand): :return: None or exception """ - self.app.on_file_new(cli=True) + self.app.on_file_new_project(cli=True)