Selaa lähdekoodia

Merge remote-tracking branch 'upstream/Beta' into Beta

Robert Niemöller 5 vuotta sitten
vanhempi
commit
84e8d3b8c3
100 muutettua tiedostoa jossa 51819 lisäystä ja 19662 poistoa
  1. 391 0
      Bookmark.py
  2. 5573 0
      CHANGELOG.md
  3. 42 9
      FlatCAM.py
  4. 0 9071
      FlatCAMApp.py
  5. 0 48
      FlatCAMCommon.py
  6. 0 6017
      FlatCAMObj.py
  7. 0 92
      FlatCAMTool.py
  8. 50 0
      Makefile
  9. 0 771
      ObjectCollection.py
  10. 108 2725
      README.md
  11. 18 0
      Utils/remove_bad_profiles_from_pictures.py
  12. 195 0
      Utils/vispy_example.py
  13. 981 0
      appCommon/Common.py
  14. 119 0
      appCommon/bilinear.py
  15. 126 0
      appCommon/bilinearInterpolator.py
  16. 3554 0
      appDatabase.py
  17. 4526 0
      appEditors/AppExcEditor.py
  18. 841 851
      appEditors/AppGeoEditor.py
  19. 6729 0
      appEditors/AppGerberEditor.py
  20. 373 0
      appEditors/AppTextEditor.py
  21. 0 0
      appEditors/__init__.py
  22. 783 0
      appEditors/appGCodeEditor.py
  23. 181 0
      appGUI/ColumnarFlowLayout.py
  24. 4703 0
      appGUI/GUIElements.py
  25. 4983 0
      appGUI/MainGUI.py
  26. 2943 0
      appGUI/ObjectUI.py
  27. 587 0
      appGUI/PlotCanvas.py
  28. 1656 0
      appGUI/PlotCanvasLegacy.py
  29. 72 20
      appGUI/VisPyCanvas.py
  30. BIN
      appGUI/VisPyData/data/fonts/opensans-regular.ttf
  31. BIN
      appGUI/VisPyData/data/freetype/freetype253.dll
  32. BIN
      appGUI/VisPyData/data/freetype/freetype253_x64.dll
  33. 12 9
      appGUI/VisPyPatches.py
  34. 5 4
      appGUI/VisPyTesselators.py
  35. 236 45
      appGUI/VisPyVisuals.py
  36. 0 0
      appGUI/__init__.py
  37. 327 0
      appGUI/preferences/OptionUI.py
  38. 77 0
      appGUI/preferences/OptionsGroupUI.py
  39. 41 0
      appGUI/preferences/PreferencesSectionUI.py
  40. 1208 0
      appGUI/preferences/PreferencesUIManager.py
  41. 16 0
      appGUI/preferences/__init__.py
  42. 215 0
      appGUI/preferences/cncjob/CNCJobAdvOptPrefGroupUI.py
  43. 79 0
      appGUI/preferences/cncjob/CNCJobEditorPrefGroupUI.py
  44. 240 0
      appGUI/preferences/cncjob/CNCJobGenPrefGroupUI.py
  45. 81 0
      appGUI/preferences/cncjob/CNCJobOptPrefGroupUI.py
  46. 36 0
      appGUI/preferences/cncjob/CNCJobPreferencesUI.py
  47. 0 0
      appGUI/preferences/cncjob/__init__.py
  48. 62 0
      appGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py
  49. 306 0
      appGUI/preferences/excellon/ExcellonEditorPrefGroupUI.py
  50. 168 0
      appGUI/preferences/excellon/ExcellonExpPrefGroupUI.py
  51. 468 0
      appGUI/preferences/excellon/ExcellonGenPrefGroupUI.py
  52. 122 0
      appGUI/preferences/excellon/ExcellonOptPrefGroupUI.py
  53. 53 0
      appGUI/preferences/excellon/ExcellonPreferencesUI.py
  54. 0 0
      appGUI/preferences/excellon/__init__.py
  55. 472 0
      appGUI/preferences/general/GeneralAPPSetGroupUI.py
  56. 401 0
      appGUI/preferences/general/GeneralAppPrefGroupUI.py
  57. 316 0
      appGUI/preferences/general/GeneralAppSettingsGroupUI.py
  58. 409 0
      appGUI/preferences/general/GeneralGUIPrefGroupUI.py
  59. 43 0
      appGUI/preferences/general/GeneralPreferencesUI.py
  60. 0 0
      appGUI/preferences/general/__init__.py
  61. 349 0
      appGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py
  62. 67 0
      appGUI/preferences/geometry/GeometryEditorPrefGroupUI.py
  63. 193 0
      appGUI/preferences/geometry/GeometryGenPrefGroupUI.py
  64. 267 0
      appGUI/preferences/geometry/GeometryOptPrefGroupUI.py
  65. 46 0
      appGUI/preferences/geometry/GeometryPreferencesUI.py
  66. 0 0
      appGUI/preferences/geometry/__init__.py
  67. 120 0
      appGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py
  68. 248 0
      appGUI/preferences/gerber/GerberEditorPrefGroupUI.py
  69. 118 0
      appGUI/preferences/gerber/GerberExpPrefGroupUI.py
  70. 229 0
      appGUI/preferences/gerber/GerberGenPrefGroupUI.py
  71. 100 0
      appGUI/preferences/gerber/GerberOptPrefGroupUI.py
  72. 54 0
      appGUI/preferences/gerber/GerberPreferencesUI.py
  73. 0 0
      appGUI/preferences/gerber/__init__.py
  74. 301 0
      appGUI/preferences/tools/Tools2CThievingPrefGroupUI.py
  75. 138 0
      appGUI/preferences/tools/Tools2CalPrefGroupUI.py
  76. 231 0
      appGUI/preferences/tools/Tools2EDrillsPrefGroupUI.py
  77. 135 0
      appGUI/preferences/tools/Tools2FiducialsPrefGroupUI.py
  78. 75 0
      appGUI/preferences/tools/Tools2InvertPrefGroupUI.py
  79. 56 0
      appGUI/preferences/tools/Tools2OptimalPrefGroupUI.py
  80. 89 0
      appGUI/preferences/tools/Tools2PreferencesUI.py
  81. 233 0
      appGUI/preferences/tools/Tools2PunchGerberPrefGroupUI.py
  82. 195 0
      appGUI/preferences/tools/Tools2QRCodePrefGroupUI.py
  83. 242 0
      appGUI/preferences/tools/Tools2RulesCheckPrefGroupUI.py
  84. 103 0
      appGUI/preferences/tools/Tools2sidedPrefGroupUI.py
  85. 153 0
      appGUI/preferences/tools/ToolsCalculatorsPrefGroupUI.py
  86. 108 0
      appGUI/preferences/tools/ToolsCornersPrefGroupUI.py
  87. 245 0
      appGUI/preferences/tools/ToolsCutoutPrefGroupUI.py
  88. 453 0
      appGUI/preferences/tools/ToolsDrillPrefGroupUI.py
  89. 322 0
      appGUI/preferences/tools/ToolsFilmPrefGroupUI.py
  90. 347 0
      appGUI/preferences/tools/ToolsISOPrefGroupUI.py
  91. 357 0
      appGUI/preferences/tools/ToolsNCCPrefGroupUI.py
  92. 311 0
      appGUI/preferences/tools/ToolsPaintPrefGroupUI.py
  93. 156 0
      appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py
  94. 111 0
      appGUI/preferences/tools/ToolsPreferencesUI.py
  95. 246 0
      appGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py
  96. 48 0
      appGUI/preferences/tools/ToolsSubPrefGroupUI.py
  97. 261 0
      appGUI/preferences/tools/ToolsTransformPrefGroupUI.py
  98. 0 0
      appGUI/preferences/tools/__init__.py
  99. 83 0
      appGUI/preferences/utilities/AutoCompletePrefGroupUI.py
  100. 102 0
      appGUI/preferences/utilities/FAExcPrefGroupUI.py

+ 391 - 0
Bookmark.py

@@ -0,0 +1,391 @@
+from PyQt5 import QtGui, QtCore, QtWidgets
+from appGUI.GUIElements import FCTable, FCEntry, FCButton, FCFileSaveDialog
+
+import sys
+import webbrowser
+
+from copy import deepcopy
+from datetime import datetime
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class BookmarkManager(QtWidgets.QWidget):
+
+    # mark_rows = QtCore.pyqtSignal()
+
+    def __init__(self, app, storage, parent=None):
+        super(BookmarkManager, self).__init__(parent)
+
+        self.app = app
+
+        assert isinstance(storage, dict), "Storage argument is not a dictionary"
+
+        self.bm_dict = deepcopy(storage)
+
+        # Icon and title
+        # self.setWindowIcon(parent.app_icon)
+        # self.setWindowTitle(_("Bookmark Manager"))
+        # self.resize(600, 400)
+
+        # title = QtWidgets.QLabel(
+        #     "<font size=8><B>FlatCAM</B></font><BR>"
+        # )
+        # title.setOpenExternalLinks(True)
+
+        # layouts
+        layout = QtWidgets.QVBoxLayout()
+        self.setLayout(layout)
+
+        table_hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(table_hlay)
+
+        self.table_widget = FCTable(drag_drop=True, protected_rows=[0, 1])
+        self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        table_hlay.addWidget(self.table_widget)
+
+        self.table_widget.setColumnCount(3)
+        self.table_widget.setColumnWidth(0, 20)
+        self.table_widget.setHorizontalHeaderLabels(
+            [
+                '#',
+                _('Title'),
+                _('Web Link')
+            ]
+        )
+        self.table_widget.horizontalHeaderItem(0).setToolTip(
+            _("Index.\n"
+              "The rows in gray color will populate the Bookmarks menu.\n"
+              "The number of gray colored rows is set in Preferences."))
+        self.table_widget.horizontalHeaderItem(1).setToolTip(
+            _("Description of the link that is set as an menu action.\n"
+              "Try to keep it short because it is installed as a menu item."))
+        self.table_widget.horizontalHeaderItem(2).setToolTip(
+            _("Web Link. E.g: https://your_website.org "))
+
+        # pal = QtGui.QPalette()
+        # pal.setColor(QtGui.QPalette.Background, Qt.white)
+
+        # New Bookmark
+        new_vlay = QtWidgets.QVBoxLayout()
+        layout.addLayout(new_vlay)
+
+        new_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("New Bookmark"))
+        new_vlay.addWidget(new_title_lbl)
+
+        form0 = QtWidgets.QFormLayout()
+        new_vlay.addLayout(form0)
+
+        title_lbl = QtWidgets.QLabel('%s:' % _("Title"))
+        self.title_entry = FCEntry()
+        form0.addRow(title_lbl, self.title_entry)
+
+        link_lbl = QtWidgets.QLabel('%s:' % _("Web Link"))
+        self.link_entry = FCEntry()
+        self.link_entry.set_value('http://')
+        form0.addRow(link_lbl, self.link_entry)
+
+        # Buttons Layout
+        button_hlay = QtWidgets.QHBoxLayout()
+        layout.addLayout(button_hlay)
+
+        add_entry_btn = FCButton(_("Add Entry"))
+        remove_entry_btn = FCButton(_("Remove Entry"))
+        export_list_btn = FCButton(_("Export List"))
+        import_list_btn = FCButton(_("Import List"))
+        # closebtn = QtWidgets.QPushButton(_("Close"))
+
+        # button_hlay.addStretch()
+        button_hlay.addWidget(add_entry_btn)
+        button_hlay.addWidget(remove_entry_btn)
+
+        button_hlay.addWidget(export_list_btn)
+        button_hlay.addWidget(import_list_btn)
+        # button_hlay.addWidget(closebtn)
+        # ##############################################################################
+        # ######################## SIGNALS #############################################
+        # ##############################################################################
+
+        add_entry_btn.clicked.connect(self.on_add_entry)
+        remove_entry_btn.clicked.connect(self.on_remove_entry)
+        export_list_btn.clicked.connect(self.on_export_bookmarks)
+        import_list_btn.clicked.connect(self.on_import_bookmarks)
+        self.title_entry.returnPressed.connect(self.on_add_entry)
+        self.link_entry.returnPressed.connect(self.on_add_entry)
+        # closebtn.clicked.connect(self.accept)
+
+        self.ui_connect()
+        self.build_bm_ui()
+
+    def ui_connect(self):
+        self.table_widget.drag_drop_sig.connect(self.mark_table_rows_for_actions)
+
+    def ui_disconnect(self):
+        try:
+            self.table_widget.drag_drop_sig.connect(self.mark_table_rows_for_actions)
+        except (TypeError, AttributeError):
+            pass
+
+    def build_bm_ui(self):
+
+        self.table_widget.setRowCount(len(self.bm_dict))
+
+        nr_crt = 0
+        sorted_bookmarks = sorted(list(self.bm_dict.items()), key=lambda x: int(x[0]))
+        for entry, bookmark in sorted_bookmarks:
+            row = nr_crt
+            nr_crt += 1
+
+            title = bookmark[0]
+            weblink = bookmark[1]
+
+            id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt))
+            # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.table_widget.setItem(row, 0, id_item)  # Tool name/id
+
+            title_item = QtWidgets.QTableWidgetItem(title)
+            self.table_widget.setItem(row, 1, title_item)
+
+            weblink_txt = QtWidgets.QTextBrowser()
+            weblink_txt.setOpenExternalLinks(True)
+            weblink_txt.setFrameStyle(QtWidgets.QFrame.NoFrame)
+            weblink_txt.document().setDefaultStyleSheet("a{ text-decoration: none; }")
+
+            weblink_txt.setHtml('<a href=%s>%s</a>' % (weblink, weblink))
+
+            self.table_widget.setCellWidget(row, 2, weblink_txt)
+
+            vertical_header = self.table_widget.verticalHeader()
+            vertical_header.hide()
+
+            horizontal_header = self.table_widget.horizontalHeader()
+            horizontal_header.setMinimumSectionSize(10)
+            horizontal_header.setDefaultSectionSize(70)
+            horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+            horizontal_header.resizeSection(0, 20)
+            horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
+            horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+
+        self.mark_table_rows_for_actions()
+
+        self.app.defaults["global_bookmarks"].clear()
+        for key, val in self.bm_dict.items():
+            self.app.defaults["global_bookmarks"][key] = deepcopy(val)
+
+    def on_add_entry(self, **kwargs):
+        """
+        Add a entry in the Bookmark Table and in the menu actions
+        :return: None
+        """
+        if 'title' in kwargs:
+            title = kwargs['title']
+        else:
+            title = self.title_entry.get_value()
+        if title == '':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Title entry is empty."))
+            return 'fail'
+
+        if 'link' in kwargs:
+            link = kwargs['link']
+        else:
+            link = self.link_entry.get_value()
+
+        if link == 'http://':
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Web link entry is empty."))
+            return 'fail'
+
+        # if 'http' not in link or 'https' not in link:
+        #     link = 'http://' + link
+
+        for bookmark in self.bm_dict.values():
+            if title == bookmark[0] or link == bookmark[1]:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Either the Title or the Weblink already in the table."))
+                return 'fail'
+
+        # for some reason if the last char in the weblink is a slash it does not make the link clickable
+        # so I remove it
+        if link[-1] == '/':
+            link = link[:-1]
+        # add the new entry to storage
+        new_entry = len(self.bm_dict) + 1
+        self.bm_dict[str(new_entry)] = [title, link]
+
+        # add the link to the menu but only if it is within the set limit
+        bm_limit = int(self.app.defaults["global_bookmarks_limit"])
+        if len(self.bm_dict) < bm_limit:
+            act = QtWidgets.QAction(parent=self.app.ui.menuhelp_bookmarks)
+            act.setText(title)
+            act.setIcon(QtGui.QIcon(self.app.resource_location + '/link16.png'))
+            act.triggered.connect(lambda: webbrowser.open(link))
+            self.app.ui.menuhelp_bookmarks.insertAction(self.app.ui.menuhelp_bookmarks_manager, act)
+
+        self.app.inform.emit('[success] %s' % _("Bookmark added."))
+
+        # add the new entry to the bookmark manager table
+        self.build_bm_ui()
+
+    def on_remove_entry(self):
+        """
+        Remove an Entry in the Bookmark table and from the menu actions
+        :return:
+        """
+        index_list = []
+        for model_index in self.table_widget.selectionModel().selectedRows():
+            index = QtCore.QPersistentModelIndex(model_index)
+            index_list.append(index)
+            title_to_remove = self.table_widget.item(model_index.row(), 1).text()
+
+            if title_to_remove == 'FlatCAM' or title_to_remove == _('Backup Site'):
+                self.app.inform.emit('[WARNING_NOTCL] %s.' % _("This bookmark can not be removed"))
+                self.build_bm_ui()
+                return
+            else:
+                for k, bookmark in list(self.bm_dict.items()):
+                    if title_to_remove == bookmark[0]:
+                        # remove from the storage
+                        self.bm_dict.pop(k, None)
+
+                        for act in self.app.ui.menuhelp_bookmarks.actions():
+                            if act.text() == title_to_remove:
+                                # disconnect the signal
+                                try:
+                                    act.triggered.disconnect()
+                                except TypeError:
+                                    pass
+                                # remove the action from the menu
+                                self.app.ui.menuhelp_bookmarks.removeAction(act)
+
+        # house keeping: it pays to have keys increased by one
+        new_key = 0
+        new_dict = {}
+        for k, v in self.bm_dict.items():
+            # we start with key 1 so we can use the len(self.bm_dict)
+            # when adding bookmarks (keys in bm_dict)
+            new_key += 1
+            new_dict[str(new_key)] = v
+
+        self.bm_dict = deepcopy(new_dict)
+        new_dict.clear()
+
+        self.app.inform.emit('[success] %s' % _("Bookmark removed."))
+
+        # for index in index_list:
+        #     self.table_widget.model().removeRow(index.row())
+        self.build_bm_ui()
+
+    def on_export_bookmarks(self):
+        self.app.defaults.report_usage("on_export_bookmarks")
+        self.app.log.debug("on_export_bookmarks()")
+
+        date = str(datetime.today()).rpartition('.')[0]
+        date = ''.join(c for c in date if c not in ':-')
+        date = date.replace(' ', '_')
+
+        filter__ = "Text File (*.TXT);;All Files (*.*)"
+        filename, _f = FCFileSaveDialog.get_saved_filename(
+            caption=_("Export Bookmarks"),
+            directory='{l_save}/{n}_{date}'.format(l_save=str(self.app.get_last_save_folder()),
+                                                   n=_("Bookmarks"),
+                                                   date=date),
+            ext_filter=filter__)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+            return
+        else:
+            try:
+                f = open(filename, 'w')
+                f.close()
+            except PermissionError:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                return
+            except IOError:
+                self.app.log.debug('Creating a new bookmarks file ...')
+                f = open(filename, 'w')
+                f.close()
+            except Exception:
+                e = sys.exc_info()[0]
+                self.app.log.error("Could not load the file.")
+                self.app.log.error(str(e))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load the file."))
+                return
+
+            # Save Bookmarks to a file
+            try:
+                with open(filename, "w") as f:
+                    for title, link in self.bm_dict.items():
+                        line2write = str(title) + ':' + str(link) + '\n'
+                        f.write(line2write)
+            except Exception:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write bookmarks to file."))
+                return
+        self.app.inform.emit('[success] %s: %s' % (_("Exported bookmarks to"), filename))
+
+    def on_import_bookmarks(self):
+        self.app.log.debug("on_import_bookmarks()")
+
+        filter_ = "Text File (*.txt);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Bookmarks"), filter=filter_)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+        else:
+            try:
+                with open(filename) as f:
+                    bookmarks = f.readlines()
+            except IOError:
+                self.app.log.error("Could not load bookmarks file.")
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load the file."))
+                return
+
+            for line in bookmarks:
+                proc_line = line.replace(' ', '').partition(':')
+                self.on_add_entry(title=proc_line[0], link=proc_line[2])
+
+            self.app.inform.emit('[success] %s: %s' % (_("Imported Bookmarks from"), filename))
+
+    def mark_table_rows_for_actions(self):
+        for row in range(self.table_widget.rowCount()):
+            item_to_paint = self.table_widget.item(row, 0)
+            if row < self.app.defaults["global_bookmarks_limit"]:
+                item_to_paint.setBackground(QtGui.QColor('gray'))
+                # item_to_paint.setForeground(QtGui.QColor('black'))
+            else:
+                item_to_paint.setBackground(QtGui.QColor('white'))
+                # item_to_paint.setForeground(QtGui.QColor('black'))
+
+    def rebuild_actions(self):
+        # rebuild the storage to reflect the order of the lines
+        self.bm_dict.clear()
+        for row in range(self.table_widget.rowCount()):
+            title = self.table_widget.item(row, 1).text()
+            wlink = self.table_widget.cellWidget(row, 2).toPlainText()
+
+            entry = int(row) + 1
+            self.bm_dict.update(
+                {
+                    str(entry): [title, wlink]
+                }
+            )
+
+        self.app.install_bookmarks(book_dict=self.bm_dict)
+
+    # def accept(self):
+    #     self.rebuild_actions()
+    #     super().accept()
+
+    def closeEvent(self, QCloseEvent):
+        self.rebuild_actions()
+        self.ui_disconnect()
+        super().closeEvent(QCloseEvent)

+ 5573 - 0
CHANGELOG.md

@@ -0,0 +1,5573 @@
+FlatCAM BETA (c) 2019 - by Marius Stanciu
+Based on FlatCAM: 
+2D Computer-Aided PCB Manufacturing by (c) 2014-2016 Juan Pablo Caram
+=================================================
+
+CHANGELOG for FlatCAM beta
+
+=================================================
+
+7.11.2020
+
+- fixed a small issue in Excellon Editor that reset the delta coordinates on right mouse button click too, which was incorrect. Only left mouse button click should reset the delta coordinates.
+- In Gerber Editor upgraded the UI
+- in Gerber Editor made sure that trying to add a Circular Pad array with null radius will fail
+- in Gerber Editor when the radius is zero the utility geometry is deleted
+- in Excellon Editor made sure that trying to add a Circular Drill/Slot array with null radius will fail
+- in Excellon Editor when the radius is zero the utility geometry is deleted
+- in Gerber Editor fixed an error in the Eraser tool trying to disconnect the Jump signal
+- small UI change in the Isolation Tool for the Reference Object selection
+- small UI changes in NCC Tool and in Paint Tool for the Reference Object selection
+- language strings recompiled to make sure that the .MO files are well optimized
+RELEASE 8.994
+
+6.11.2020
+
+- in Gerber Editor made the selection multithreaded in a bid to get more performance but until Shapely will start working on vectorized geometry this don't yield too much improvement
+- in Gerber Editor, for selection now the intersection of the click point and the geometry is determined for chunks of the original geometry, each chunk gets done in a separate process
+- updated the French translation (by Olivier Cornet)
+- fixed the new InputDialog widget to set its passed values in the constructor
+- in Gerber Editor fixed the Add circular array capability
+- in Gerber Editor remade the utility geometry generation for Circular Pad Array to show the array updated in real time and also fixed the adding of array in negative quadrants
+- in Excellon Editor remade the utility geometry generation for Circular Drill/Slot Array to show the array updated in real time and also fixed the adding of array in negative quadrants
+- Turkish language strings updated (by Mehmet Kaya)
+- both for Excellon and Gerber editor fixed the direction of slots/pads when adding a circular array
+- in Gerber editor added the G key shortcut to toggle the grid snapping
+- made some changes in the Region Tool from the Gerber Editor
+
+5.11.2020
+
+- fixed the annotation plotting in the CNCJob object
+- created a new InputDialog widget that has the buttons and the context menu translated and replaced the old widget throughout the app
+- updated the translation strings
+- Turkish language strings updated
+- set some policy rules back the way they were for the combo boxes in Geometry Object properties
+- updated the Italian translation (by Massimiliano Golfetto)
+- finished the Google-translation of the German language strings
+
+4.11.2020
+
+- updated all the translation files
+- fixed issue with arrays of items could not be added in the Gerber/Excellon Editor when a translation is used
+- fixed issue in the Excellon Editor where the Space key did not toggle the direction of the array of drills
+- combed the application strings all over the app and trimmed them up until those starting with letter 'O'
+- updated the translation strings
+- fixed the UI layout in Excellon Editor and made sure that after changing a value in the Notebook side after the mouse is inside the canvas, the canvas takes the focus allowing the key shortcuts to work
+- Turkish language strings updated (by Mehmet Kaya)
+- in Gerber Editor added the shortcut key 'Space' to change the direction of the array of pads
+- updated all the translation languages. Translated by Google the Spanish, Russian. Romanian translation updated.
+- refactored the name of the classes from the Gerber Editor
+- added more icons in the Gerber and Excellon Editors for the buttons
+
+3.11.2020
+
+- fixed an issue in Tool Isolation used with tools from the Tools Database: the set offset value was not used
+- updated the Tools Database to include all the Geometry keys in the every tool from database
+- made sure that the Operation Type values ('Iso', 'Rough' and 'Finish') are not translated as this may create issues all over the application
+- fix an older issue that made that only the Custom choice created an effect when changing the Offset in the Geometry Object Tool Table
+- trying to optimize Gerber Editor selection with the mouse
+- optimized some of the strings
+- fixed the project context save functionality to work in the new program configuration
+- updated Turkish translation (by Mehmet Kaya)
+- in NCC and Isolation Tools, the Validity Checking of the tools is now multithreaded when the Check Validity UI control is checked
+- translation strings updated
+- fixed an error in Gerber parser, when it encounter a pen-up followed by pen-down move while in a region
+- trimmed the application strings
+- updated the Italian translation (by Massimiliano Golfetto)
+- fixed a series of issues in Gerber Editor tools when the user is trying to use the tools by preselecting a aperture without size (aperture macro)
+- moved all the UI stuff out of the Gerber Editor class in its own class
+- in the Excellon Editor, added shortcut keys Space and Ctrl+Space for toggling the direction of the Slots, respectively for the Array of Slots
+- updated the translation strings to the latest changes in the app strings
+
+2.11.2020
+
+- fixed the Tcl Command AlignDrill
+- fixed the Tcl Command AlignDrillGrid
+- fixed the Tcl COmmand Panelize, Excellon panelization section
+- Fixed an issue in Tool Calibration export_excellon method call
+- PEP8 corrections all over the app
+- made sure that the object selection will not work while in Editors or in the App Tools
+- some minor changes to strings and icons
+- in Corner Markers Tool - the new Gerber object will have also follow_geometry
+- upgraded the Fiducials Tool to create new objects instead of updating in place the source objects
+- upgraded the Copper Thieving Tool to create new objects instead of updating in place the source objects
+- in Copper Thieving Tool added a new parameter to filter areas too small to be desired in the copper thieving; added it to Preferences too
+- Copper Thieving Tool added a new parameter to select what extra geometry to include in the Pattern Plating Mask; added it to the Preferences
+- made a wide change on the spinners GUI ranges: from 9999.9999 all values to 10000.0000
+- fixed some late issues in Corner Markers Tool new feature (messages)
+- upgraded Calculator Tool and added the new parameter is the Preferences
+- updated translation strings
+- fixed borderline bug in Gerber editor when the edited Gerber object last aperture is a aperture without size (Aperture Macro)
+- improved the loading of a Gerber object in the Gerber Editor
+- updated translation strings
+
+1.11.2020
+
+- updated the French Translation (by Olivier Cornet)
+- fixed issue in Corner Markers Tool that crashed the app if only one corner was checked
+- fixed issue in Isolation Tool where Area Isolation selection was not working 
+- added to the translatable strings the category labels in the Project Tab and also updated the translations
+- fixed a small issue (messages) in Corner Markers Tool
+- in Corners Markers Tool added a new feature: possibility to use cross shape markers
+- in Corner Marker Tool add new feature: ability to create an Excellon object with drill holes in the corner markes
+- in Corner Marker Tool, will no longer update the current object with the marker geometry but create a new Gerber object
+- in Join Excellon functionality made sure that the new Combo Exellon object will have copied the data from source objects and not just references, therefore will survive the delete of its parents
+- updated Turkish translation (by Mehmet Kaya)
+- updated all the languages except Turkish
+- in the Tool PDF fixed the creation of Excellon objects to the current Excellon object data structure
+
+31.10.2020
+
+- adapted HPGL importer to work within the new app
+- in Gerber Editor fixed an error when using the Distance Tool with "Snap to center" option active: if clicking not on a pad Distance Tool was not working
+- updated the Turkish translation strings (by Mehmet Kaya)
+- typo fixed in Copper Thieving Tool (due of recent changes)
+- fixed issue #457; wrong reference when saving a project
+- fixed issue in Excellon Editor that crashed the app if using the Resize Drill feature by clicking in menu/toolbar
+- fixed issue in Excellon Editor when using the menu links to Move or Copy Drills/Slots
+- updated the strings 
+- updated the Turkish translation strings (by Mehmet Kaya)
+- added a parent to some of the FCInputDialog widgets used in the app such that those pop-up windows will b displayed in the center of the app main window as opposed to the center of the screen
+- finished the Google-translation of not translated strings in Russian language
+
+30.10.2020
+
+- fixed the Punch Gerber Tool bug that did not allowed the projects to be loaded or to create a new project. Fixed issue #456
+- in Tool Subtract added an option to delete the source objects after a successful operation. Fixed issue #455
+- when entering into an Editor now the Project tab is disabled and the Properties tab where the Editor is installed change the text to 'Editor' and the color is set in Red. After exiting the Tab text is reverted to previous state.
+- fixed and issue where the Tab color that was changed in various states of the app was reverted back to a default color 'black'. Now it reverts to whatever color had before therefore being compatible with an usage of black theme
+- fixed bug that did not allow joining of any object to a Geometry object
+- working on solving the lost triggered signals for the Editor Toolbars buttons after changing the layout
+- fixed issue #454; trigger signals for Editor Toolbars lost after changing the layout
+- updated the translation strings
+- more bugs that were introduced by recent changes done to solve other bugs and so on: fixed issues with the Editors and Delete shortcut
+- fixed an error in the Gerber Editor
+
+29.10.2020
+
+- added icons in most application Tools
+- updated Punch Gerber Tool such that the aperture table is updated upon clicking of the checboxes in Processed Pads Type
+- updated Punch Gerber Tool: the Excellon method now takes into consideration the pads choice 
+- minor change for the FCComboBox UI element by setting its size policy as ignored so it will not expand the notebook when the name of one of its items is very long
+- added a protection on opening the tools database UI if the tools DB file is not loaded
+- fixed NCC Tool not working with the new changes; the check for not having complete isolation is just a Warning
+- fixed the sizePolicy for the FCComboBox widgets in the Preferences that holds the preprocessors
+- fixed issue with how the preamble / postamble GCode were inserted into the final GCode
+- fixed a small issue in GCode Editor where the signals for the buttons were attached again at each launch of the GCode Editor
+- fixed issues in the Tools Database due of recent changes in how the data structure is created
+- made sure that the right tools go only to the intended use, in Tools Database otherwise an error status message is created and Tools DB is closed on adding a wrong tool
+- fixed the usage for Tools Database in Unix-like OS's; fixed issue #453
+- done some modest refactoring
+- fixed the Search and Add feature in Geometry Object UI
+- fixed issue with preamble not being inserted when used alone
+- modified the way that the start GCode is stored such that now the bug in GCode Editor that did not allowed selection of the first tool is now solved
+- in Punch Gerber Tool added a column in the apertures table that allow marking of the selected aperture so the user can see what apertures are selected
+- improvements in the Punch Gerber Tool aperture markings
+- improved the Geometry Object functionality in regards of Tools DB, deleting a tool and adding a tool
+- when using the 'T' shortcut key with Properties Tab in focus and populated with the properties of a Geometry Object made the popped up spinner to have the value autoselected
+- optimized the UI in Extract Drills Tool
+- added some more icons for buttons
+
+28.10.2020
+
+- a series of PEP8 corrections in the FlatCAMGeometry.py
+- in Geometry UI finished a very basic way for the Polish feature (this will be upgraded in the future, for now is very rough)
+- added some new GUI elements by subclassing some widgets for the dialog pop-ups
+- in NCC Tool and Isolation Tool, pressing the shortcut key 'T' will bring the add new tool pop up in which now it is included the button to get the optimal diameter
+- in Geometry UI and for Solderpaste Tool replaced the pop up window that is launched when using shortcut key with one that has the context menu translated
+- some UI cleanup in the Geometry UI
+- updated the translation strings except Russian which could be in the works
+- fixed an error that did not allowed for the older preferences to be deleted when installing a different version of the software
+- in Legacy Mode fixed a small issue: the status bar icon for the Grid axis was not colored on app start
+- added a new string to the translatable strings
+- fixed an error that sometime showed in Legacy Mode when moving the mouse outside canvas
+- reactivated the shortcut key 'S' in TCL Shell, to close the shell dock when it was open (of course the focus has to be not on the command line)
+- brought up-to-date and fixed the Tcl Command Drillcncjob and Cncjob
+- fixed Tcl command Isolate to not print messages on message bar in case it is run headless
+- fixed Tcl command Copper Clear (NCC)
+- fixed Tcl command Paint
+- temporary fix for comboboxes not finding the the value in the items when setting themselves with a value by defaulting to the first item in the list
+- fix in Tool Subtract where there was a typo
+- upgraded the punch Gerber Tool
+- updated the Turkish translation strings (by Mehmet Kaya)
+- fixed an issue in Isolation Tool when running the app in Basic mode;
+- fixed Paint, Isolation and NCC Tools such the translated comboboxes values are now stored as indexes instead of translated words as before
+- in Geometry Object made sure that the widgets in the Tool Table gets populated regardless of encountering non-recognizable translated values
+- in Paint Tool found a small bug and fixed it
+- fixed the Tool Subtractor algorithms
+
+27.10.2020
+
+- created custom classes derived from TextEdit and from LineEdit where I overloaded the context menu and I made all the other classes that were inheriting from them to inherit from those new classes
+- minor fix in ToolsDB2UI
+- updated the Turkish translation strings (by Mehmet Kaya)
+- fixed a bug in conversion of any to Gerber in the section of Excellon conversion
+- some PEP8 fixes
+- fixed a bug due of recent chagnes in FileMenuHandlers class
+- fixed an issue in Tools Database (ToolsDB2 class) that did not made the Tab name in Red color when adding/deleting a tool by using the context menu
+- optimized the Tools Database
+- small string change
+
+26.10.2020
+
+- added a new menu entry and functionality in the View category: enable all non-selected (shortcut key ALT+3)
+- fixed shortcut keys for a number of functionality and in some cases added some new
+- fixed the enable/disable all plots functionality
+- fixed issue with the app window restored in a shifted position after doing Fullscreen
+- fixed issue with coords, delta_coords and status toolbars being disabled when entering fullscreen mode and remaining disabled after restore to normal mode
+- changed some of the strings (added a few in the How To section)
+- more strings updated
+- modified the shortcut strings and the way the shortcuts were listed in the Shortcut keys list such that it will allow a future Shortcuts Manager
+- updated all the language strings according to the modifications done above
+- fixed a small issue with using the shortcut key for toggling the display of Properties tab
+- fixed issue with not updating the model view on the model used in the Project tab when using the shortcut keys 1, 2, 3 which made the color of the tree element not reflect the change in status
+- minor string fix; removed the 'Close' menu entry on the context menu for the TCL Shell
+- overloaded the context menu in the Tcl Shell and added key shortcuts; the menu entries are now translatable
+- overloaded the context menu in several classes from GUI Elements such that the menus are now translated
+- fixed a formatting issue in the MainGUI.py file
+- updated the translations for the new strings that were added
+- another GUI element for which I've overloaded the context menu to make it translatable: _ExpandableTextEdit
+- overloaded the context menu for FCSpinner and for FCDoubleSpinner
+- added new strings and therefore updated the translation strings
+- fixed some minor issues when doing a project save
+
+25.10.2020
+
+- updated the Italian translation (by Massimiliano Golfetto)
+- finished the update of the Spanish translation (Google translate)
+
+24.10.2020
+
+- added a new GUI element, an InputDialog made out of FCSliderWithSpinner named FCInputDialogSlider
+- replaced the InputDialog in the Opacity pop menu for the objects in the Project Tab with a FCInputDialogSlider
+- minor changes
+- UI changes in the AppTextEditor and in CNCJob properties tab and in GCoe Editor
+- some changes in strings; updated all the translation strings to the latest changes
+- finished the Romanian translation
+- created two new preprocessors (from 'default' and from 'grbl_11') that will have no toolchange commands regardless of the settings in the software
+- updated the Turkish translation (by Mehmet Kaya)
+- the methods of the APP class that were the handlers for the File menu are now moved to their own class
+- fixed some of the Tcl Commands that depended on the methods refactored above
+- reverted the preprocessors with no toolchange commands to the original but removed the M6 toolchange command
+- fixed newly introduced issue when doing File -> Print(PDF)
+- fixed newly introduced issues with SysTray and Splash
+- added ability for the app to detect the current DPI used on the screen; applied this information in the Film Tool when exporting PNG files
+- found that Pillow v >= 7.2 breaks Reportlab 3.5.53 (latest version) and creates an error in Film Tool when exporting PNG files. Pillow 7.2 still works.
+
+23.10.2020
+
+- updated Copper Thieving Tool to work with the updated program
+- updated Rules Check Tool - Hole Size rule to work with the new data structure for the Excellon objects
+- updated Rules Check Tool - added an activity message
+- updated some strings, updated the translation strings
+- commented the ToolsDB class since it is not used currently
+- some minor changes in the AppTextEditor.py file
+- removed Hungarian language since it's looking like is no longer being translated
+- added a default properties tab which will hold a set of information's about the application
+- minor changes in the Properties Tool
+- Excellon UI: fixed a small issue with toggling all rows in Tools Table not toggling off and also the milling section in Utilities was not updated
+- some refactoring in the keys of the defaults dictionary
+- fixed an ambiguity in the Tools Database GUI elements
+
+22.10.2020
+
+- added  a message to show if voronoi_diagram method can be used (require Shapely >= 1.8)
+- modified behind the scene the UI for Tool Subtract
+- modified some strings and updated the translation strings
+- in NCC Tool added a check for the validity of the used tools; its only informative
+- in NCC Tool done some refactoring
+- in NCC Tool fixed a bug when using Rest Machining; optimizations
+- in NCC Tool fixed a UI issue
+- updated the Turkish translation (by Mehmet Kaya)
+- small change in the CNCJob UI by replacing the AL checkbox with a checkable QButton
+- disabled the Autolevelling feature in CNCJob due of being not finished and missing also a not-yet-released package: Shapely v 1.8
+- added some new strings for translation and updated the translation strings
+- in ToolsDB2UI class made the vertical layouts have a preferred minimum dimension as opposed to the previous fixed one
+- in Geometry Object made sure that the Tools Table second column is set to Resize to contents
+- fixed a bug in Tool PunchGerber when using an Excellon to punch holes in the Gerber apertures
+
+21.10.2020
+
+- in Geometry Object fixed the issue with not using the End X-Y value and also made some other updates here
+- in NCC and Paint Tool fixed some issues with missing keys in the tool data dictionary
+- In Excellon Object UI fixed the enable/disable for the Milling section according to the Tools Table row that is selected
+- In Excellon Object UI fixed the milling geometry generation
+- updated the translations strings to the changes in the source code
+- some strings changed
+- made the Properties checkbox in the Object UI into a checkable button and added to it an icon
+- fixed crash on using shortcut for creating a new Document Object
+- fixed Cutout Tool to work with the endxy parameter
+- added the exclusion parameters for Drilling Tool to the Preferences area
+- cascaded_union() method will be deprecated in Shapely 1.8 in favor of unary_union; replaced the usage of cascaded_union with unary_union in all the app
+- added some strings to the translatable strings and updated the translation strings
+- merged in the Autolevelling branch and made some PEP8 changes to the bilinearInterpolator.py file
+- added a button in Gerber UI that will hide/show the bounding box generation and the non-copper region section
+- compacted the UI for the 2Sided Tool
+- added a button in Excellon UI that will hide/show the milling section
+- optimized a bit the UI for Gerber/Excellon/Geometry objects
+- optimized FlatCAMObj.add_properties_items() method
+- updated the Turkish translation (by Mehmet Kaya)
+- added ability to run a callback function with callback_parameters after a new FlatCAM object is created
+
+20.10.2020
+
+- finished to add the Properties data to the Object Properties (former Selected Tab)
+
+19.10.2020
+
+- added a check (and added to Preferences too) for the verification of tools validity in the Isolation Tool
+- fixed QrCode Tool
+- updated the Turkish translation (by Mehmet Kaya)
+
+18.10.2020
+
+- fixed issue with calling the inform signal in the FlatCAMDefaults.load method
+- fixed macro parsing in Gerber files generated by KiCAD 4.99 (KiCAD 5.0)
+
+17.10.2020
+
+- updated Turkish translation (by Mehmet Kaya)
+
+8.10.2020
+
+- small change in the NCC Tool UI
+- some strings are changed and therefore the translation strings source are updated
+- Isolation Tool - added a check for having a complete isolation
+
+7.10.2020
+
+- working on adding DPI setting for PNG export in Film Tool - update
+- finished updating DPI setting feature for PNG export in Film Tool
+
+5.10.2020
+
+- working on adding DPI setting for PNG export in the Film Tool
+- finished working in adding DPI settings for PNG export in Film Tool although there are some limitations due of Reportlab
+- small change in TclCommandExportSVG
+
+26.09.2020
+
+- the Selected Tab is now Properties Tab for FlatCAM objects
+- modified the Properties Tab for various FlatCAM objects preparing the move of Properties Tool data into the Properties Tab
+- if the Properties tab is in focus (selected) when a new object is created then it is automatically selected therefore it's properties will be populated
+
+25.09.2020
+
+- minor GUI change in Isolation Tool
+
+24.09.2020
+
+- fixed a bug where end_xy parameter in Drilling Tool was not used
+- fixed an issue in Delete All method in the app_Main.py
+
+23.09.2020
+
+- added support for virtual units in SVG parser; warning: it may require the support for units which is not implemented yet
+- fixed canvas selection such that when selecting shape fails to be displayed with rounded corners a square selection shape is used
+- fixed canvas selection for the case when the selected object is a single line or a line made from multiple segments
+
+22.09.2020
+
+- fixed an error in importing SVG that has a single line
+- updated the POT file and the PO/MO files for Turkish language
+- working to add virtual units to SVG parser
+
+20.09.2020
+
+- in CNCJob UI Autolevelling: on manual add of probe points, only voronoi diagram is calculated
+- in SVG parser: made sure that the minimum number of steps to approximate an arc/circle/bezier is 10
+
+19.09.2020
+
+- removed some brackets in the GRBL laser preprocessor due of GRBL firmware interpreting the first closing bracket as the comment end
+
+3.09.2020
+
+- in CNCJob UI Autolevelling: changed the UI a bit
+- added a bilinear interpolation calculation class from: https://github.com/pmav99/interpolation
+- in CNCJob UI Autolevelling: made sure that the grid can't have less than 2 rows and 2 columns when using the bilinear interpolation or 1 row and 1 column when using the Voronoi polygons
+- in CNCJob UI Autolevelling: prepared the app for bilinear interpolation
+- in CNCJob UI Autolevelling: fixes in the UI
+
+2.09.2020
+
+- in CNCJob UI Autolevelling: solved some small errors: when manual adding probe points dragging the mouse with left button pressed created selection rectangles; detection of click inside the solid geometry was failing
+- in CNCJob UI Autolevelling: in manual adding of probe points make sure you always add a first probe point in origin
+- in CNCJob UI Autolevelling: first added point when manual adding of probe points is auto added in origin before adding first point
+- in CNCJob UI Autolevelling: temp geo for adding points in manual mode is now painted in solid black color and with a smaller diameter
+- in CNCJob UI Autolevelling - GRBL controller - added a way to save a GRBL height map
+- in CNCJob UI Autolevelling: added the UI for choosing the method used for the interpolation used in autolevelling
+
+31.08.2020
+
+- updated the Italian translation files by Massimiliano Golfetto
+- in CNCJob UI Autolevelling: made sure that plotting a Voronoi polygon is done only for non-None polygons
+- in CNCJob UI Autolevelling: in manual mode, Points can be chosen only when clicking inside the object to be probed
+- in CNCJob UI Autolevelling: made sure that plotting a Voronoi polygon is done only for non-None polygons
+- in CNCJob UI Autolevelling: remade the probing points generation so they could allow bilinear interpolation
+
+29.08.2020
+
+- 2Sided Tool - fixed newly introduced issues in the Alignment section
+- 2Sided Tool - modified the UI such that some of the fields will allow only numbers and some special characters ([,],(,),/,*,,,+,-,%)
+- Cutout Tool - working on adding mouse bites for the Freeform cutout
+- updated the translation files to the current state of the app
+- Cutout Tool - rectangular and freeform cutouts are done in a threaded way
+- Cutout Tool - added the Mouse Bites feature for the Rectangular and Freeform cutouts and right now it fails in case of using a Geometry object and Freeform cutout (weird result)
+- some changes in camlib due of warnings for future changes in Shapely 2.0
+- Cutout Tool - fixed mouse bites feature in case of using a Geometry object and Freeform cutout
+- Cutout Tool - can do cutouts on multigeo Geometry objects: it will automatically select the geometry of first tool
+- Geometry Editor - fixed exception raised when trying to move and there is no shape to move
+- Cutout Tool - finished adding the Mouse Bites feature by adding mouse bites for manual cutouts
+
+28.08.2020
+
+- Paint Tool - upgraded the UI and added the functionality that now adding a new tool is done by first searching the Tools DB for a suitable tool and if fails then it adds an default tool
+- Paint Tool - on start will attempt to search in the Tools DB for the default tools and if found will load them from the DB
+- NCC Tool - upgraded the UI and added the functionality that now adding a new tool is done by first searching the Tools DB for a suitable tool and if fails then it adds an default tool
+- NCC Tool - on start will attempt to search in the Tools DB for the default tools and if found will load them from the DB
+- fixes in NCC, Paint and Isolation Tool due of recent changes
+- modified the Tools Database and Preferences with the new parameters from CutOut Tool
+- changes in Tool Cutout: now on Cutout Tool start the app will look into Tools Database and search for a tool with same diameter (or within the set tolerance) as the one from Preferences and load it if found or load a default tool if not
+- Tool Cutout - this Tool can now load tools from Tools Database through buttons in the Cutout Tool
+
+27.08.2020
+
+- fixed the Tcl commands AddCircle, AddPolygon, AddPolyline and AddRectangle to have stored bounds therefore making them movable/selectable on canvas
+- in Tool Cutout, when using the Thin Gaps feature, the resulting geometry loose the extra color by toggling tool plot in Geometry UI Tools Table- fixed
+- in Tool Cutout fixed manual adding of gaps with thin gaps and plotting
+- in Tool Cutout, when using fix gaps made sure that this feature is not activated if the value is zero
+- in Tool Cutout: modified the UI in preparation for adding the Mouse Bites feature
+- Turkish translation strings were updated by the translator, Mehmet Kaya
+- Film Tool - moved the Tool UI in its own class
+- in Tools: Image, InvertGerber, Optimal, PcbWizard - moved the Tool UI in its own class
+- Tool Isolation - made sure that the app can load from Tools Database only tools marked for Isolation tool
+- Tool Isolation - on Tool start it will attempt to load the Preferences set tools by diameter from Tools Database. If it can't find one there it will add a default tool.
+- in Tools: Transform, SUb, RulesCheck, DistanceMin, Distance - moved the Tool UI in its own class
+- some small fixes
+- fixed a borderline issue in CNCJob UI Autolevelling - Voronoi polygons calculations
+
+26.08.2020
+
+- fix for issue nr 2 in case of Drilling Tool. Need to check Isolation Tool, Paint Tool, NCC Tool
+- Drilling Tool - UI changes
+- Geometry object - now plotting color for an individual tool can be specified
+- in CutOut Tool - when using  'thin gaps' option then the cut parts are colored differently than the rest of the geometry in the Geometry object
+- solved some deprecation warnings (Shapely module)
+- Drilling Tool - when replacing Tools if more than one tool for one diameter is found, the application exit the process and display an error in status bar; some minor fixes
+- Isolation Tool - remade the UI
+- Isolation Tool - modified the add new tool method to search first in Tools Database  for a suitable tool
+- Isolation Tool - added ability to find the tool diameter that will guarantee total isolation of the currently selected Gerber object
+- NCC Tool - UI change: if the operation is Isolation then some of the tool parameters are disabled
+- fixed issue when plotting a CNCJob object with multiple tools and annotations on by plotting annotations after all the tools geometries are plotted
+- fixed crash in Properties Tool, when applied on a CNCJob object made out of an Excellon object (fixed issue #444)
+- in Properties Tool, for CNCJob objects made out of Excellon objects, added the information's from Tool Data
+- in Properties Tool made sure that the set color for the Tree parents depends on the fact if the gray icons set are used (dark theme) or not
+- Properties Tool - properties for a Gerber objects has the Tool Data now at the end of the information's
+- in Gerber UI done some optimizations
+
+25.08.2020
+
+- in CNCJob UI Autolevelling - made the Voronoi calculations work even in the scenarios that previously did not work; it need a newer version of Shapely, currently I installed the GIT version
+- in CNCJob UI Autolevelling - Voronoi polygons are now plotted
+- in CNCJob UI Autolevelling - adding manual probe points now show some geometry (circles) for the added points until the adding is finished
+- 2Sided Tool - finished the feature that allows picking an Excellon drill hole center as a Point mirror reference
+- Tool Align Objects - moved the Tool Ui into its own class
+- for Tools: Calculators, Calibration, Copper Thieving, Corners, Fiducials - moved the Tool UI in its own class
+
+24.08.2020
+
+- fixed issues in units conversion
+- in CNCJob UI Autolevelling - changed how the probing code is generated and when
+- changed some strings in CNCJob UI Autolevelling
+- made sure that when doing units conversion keep only the decimals specified in the application decimals setting (should differentiate between values and display?)
+- in CNCJob UI Autolevelling - some UI changes
+- in CNCJob UI Autolevelling - GRBL controller - added the probing method
+- in CNCJob UI Autolevelling - GRBL controller - fixed the send_grbl_command() method
+
+23.08.2020
+
+- in CNCJob UI Autolevelling - autolevelling is made to be not available for cnc code generated with Roland or HPGL preprocessors
+- in CNCJob UI Autolevelling - added a save dialog for the probing GCode
+- added a new GUI element, a DoubleSlider
+- in CNCJob UI Autolevelling - GRBL controller - Control: trying to add DoubleSlider + DoubleSpinner combo controls
+- in GUI element FCDoubleSpinner fixed an range issue
+
+21.08.2020
+
+- in CNCJob UI Autolevelling - GRBL controller - Control: added a Origin button; changed the UI to have rounded rectangles 
+- in CNCJob UI Autolevelling - GRBL controller - Control: added feedrate and step size controls and added them in Preferences
+- in CNCJob UI Autolevelling - GRBL controller - added handlers for the Zeroing and for Homing and for Pause/Resume; some UI optimizations
+
+19.08.2020
+
+- in CNCJob UI Autolevelling - sending GCode/GRBL commands is now threaded
+- in CNCJob UI Autolevelling - Grbl Connect tab colors will change with the connection status
+- in CNCJob UI Autolevelling - GRBL Control and Sender tabs are disabled when the serial port is disconnected
+- in CNCJob UI Autolevelling - GRBL Sender - now only a single command can be sent 
+- in CNCJob UI Autolevelling - GRBL controller - changed the UI
+- in CNCJob UI Autolevelling - added some VOronoi poly calculations
+
+18.08.2020
+
+- in Doublesided Tool added some UI for Excellon hole snapping
+- in Doublesided Tool cleaned up the UI
+- in CNCJob UI Autolevelling - in COntrol section added  buttons for Jog an individual axes zeroing
+- in CNCJob UI Autolevelling - added handlers for: jogging, reset, sending commands
+- in CNCJob UI Autolevelling - added handlers for GRBL report and for getting GRBL parameters
+
+17.08.2020
+
+- in CNCJob UI Autolevelling - GRBL GUI controls are now organized in a tab widget
+
+16.08.2020
+
+- in CNCJob UI Autolevelling - updated the UI with controls for probing GCode parameters and added signals and slots for the UI
+- in CNCJob UI Autolevelling - added a mini gcode sender for the GRBL to be able to send the probing GCode and get the height map (I may make a small and light app for that so it does not need to have FlatCAM on the GCode sender PC)
+- in CNCJob UI Autolevelling finished the probing GCode generation for MACH/LinuxCNC controllers; this GCode can also be viewed
+- in CNCJob UI Autolevelling - Probing GCode has now a header
+- in CNCJob UI Autolevelling - Added entries in Preferences
+- in CNCJob UI Autolevelling - finished the Import Height Map method
+- in CNCJob UI Autolevelling - made autolevelling checkbox state persistent between app restarts
+
+14.08.2020
+
+- in CNCJob UI worked on the UI for the Autolevelling
+- in CNCJob UI finished working on adding test points in Grid mode
+- in CNCJob UI finished working on adding test points in Manual mode
+
+13.08.2020
+
+- in CNCJob UI added GUI for an eventual Autolevelling feature 
+- in CNCJob UI updated the GUI for Autolevelling
+- Cutout Tool - finished handler for gaps thickness control for the manual gaps
+- CNCJob object - working in generating Voronoi diagram for autolevelling
+
+11.08.2020
+
+- CutOut Tool - finished handler for gaps thickness control for the free form cutout
+
+9.08.2020
+
+- small fix so the cx_freeze 6.2 module will work in building a frozen version of FlatCAM
+
+7.08.2020
+
+- all Geometry objects resulted from Isolation Tool are now of type multi-geo
+- fixed minor glitch in the Isolation Tool UI
+- added an extra check when doing selection on canvas
+- fixed an UI problem in Gerber Editor
+
+5.08.2020
+
+- Tool Cutout - more work in gaps thickness control feature
+- Tool Cutout - added some icons to buttons
+- Tool Cutout - done handling the gaps thickness control for the rectangular cutout; TODO: check all app for the usage of geometry_spindledir and geometry_optimization_type defaults in tools and in options
+- Tool Cutout - some work in gaps thickness control for the free form cutout
+
+4.08.2020
+
+- removed the Toolchange Macro feature (in the future it will be replaced by full preprocessor customization)
+- modified GUI in Preferences
+- Tool Cutout - working in adding gaps thickness control feature; added the UI in the Tool
+
+3.08.2020
+
+- GCode Editor - GCode tool selection when clicking on tool in Tools table is working. The only issue is that the first tool gcode includes the start gcode which confuse the algorithm
+- GCode Editor - can not delete objects while in the Editor; can not close the Code Editor Tab except on Editor exit; activated the shortcut keys (for now only CTRL+S is working)
+- added a way to remember the old state of Tools toolbar before and after entering an Editor
+- GCode Editor - modified the UI
+
+2.08.2020
+
+- GCode Editor - closing the Editor will close also the Code Editor Tab
+- cleanup of the CNCJob UI; added a checkbox to signal if any append/prepend gcode was set in Preferences (unchecking it will override and disable the usage of the append/prepend GCode)
+- the start Gcode is now stored in the CNCJob object attribute gc_start
+- GCode Editor - finished adding the ability to select a row in the Tools table and select the related GCode
+- GCode Editor - working on GCode tool selection - not OK
+
+1.08.2020
+
+- Tools Database: added a Cutout Tool Parameters section
+- GCode Editor - work in the UI
+
+31.07.2020
+
+- minor work in GCode Editor
+
+29.07.2020
+
+- fixed an exception that was raised in Geometry object when using an Offset
+
+27.07.2020
+
+- Gerber parser - a single move with pen up D2 followed by a pen down D1 at the same location is now treated as a Flash; fixed issue #441
+
+25.07.2020
+
+- Tools Tab is hidden when entering into a Editor and showed on exit (this needs to be remade such that the toolbars state should be restored to whatever it was before entering in the Editor)
+
+22.07.2020
+
+- working on a proper GCode Editor
+- wip in the GCode Editor
+- added a Laser preprocessor named 'Z_laser' which will change the Z to the Travel Z on each ToolChange event allowing therefore control of the dot size
+- by default now a new blank Geometry object created by FlatCAM is of type multigeo
+- made sure that optimizations of lines when importing SVG or DXF as lines will not encounter polygons but only LinesStrings or LinearRings, otherwise having crashes
+- fixed the import SVG and import DXF, when importing as Geometry to be imported as multigeo tool
+- fixed the import SVG and import DXF, the source files will be saved as loaded into the source_file attribute of the resulting object (be it Geometry or Gerber)
+- in import SVG and import DXF methods made sure that any polygons that are imported as polygons will survive and only the lines are optimized (changed the behavior of the above made modification)
+
+21.07.2020
+
+- updated the FCRadio class with a method that allow disabling certain options
+- the Path optimization options for Excellon and Geometry objects are now available depending on the OS platform used (32bit vs 64bit)
+- fixed MultiColor checkbox in Excellon Object to work in Legacy Mode (2D)
+- modified the visibility change in Excellon UI to no longer do plot() when doing visibility toggle for one of the tools but only a visibility change in the shapes properties
+- Excellon UI in Legacy Mode (2D): fixed the Solid checkbox functionality
+- Excellon UI: fixed plot checkbox performing an extra plot function which was not required
+- Excellon UI: added a column which will color each row/tool of that column in the color used when checking Multicolor checkbox
+- Excellon UI: made sure that when the Multicolor checkbox is unchecked, the color is updated in the Color column of the tools table
+- made sure that the Preferences files are deleted on new version install, while the application is in Beta status
+- fixed issues with detecting older Preferences files
+- fixed some issues in Excellon Editor due of recent changes
+- moved the Gerber colors fill in the AppObject.on_object_created() slot and fixed some minor issues here
+- made sure there are no issues when plotting the Excellon object in one thread and trying to build the UI in another by using a signal
+
+20.07.2020
+
+- fixed a bug in the FlatCAMGerber.on_mark_cb_click_table() method when moving a Gerber object
+- added a way to remember the colors set for the Gerber objects; it will remember the order that they were loaded and set a color previously given
+- added a control in Preferences -> Gerber Tab for Gerber colors storage usage
+- made sure that the defaults on first install will set the number of workers to half the number of CPU's on the system but no less than 2
+
+18.07.2020
+
+- added some icons in the Code Editor
+- replaced some icons in the app
+- in Code Editor, when changing text, the Save Code button will change color (text and icon) to red and after save it will revert the color to the default one
+- in Code Editor some methods rework
+
+16.07.2020
+
+- added a new method for GCode generation for Geometry objects
+- added multiple algorithms for path optimization when generating GCode from an Geometry object beside the original Rtree algorithm: TSA, OR-Tools Basic, OR-Tools metaheuristics
+- added controls for Geometry object path optimization in Preferences
+
+15.07.2020
+
+- added icons to some of the push buttons
+- Tool Drilling - automatically switch to the Selected Tab after job finished
+- added Editor Push buttons in Geometry and CNCJob UI's
+- Tool Drilling - brushing through code and solved the report on estimation of execution time
+- Tool Drilling - more optimizations regarding of using Toolchange as opposed to not using it
+- modified the preprocessors to work with the new properties for Excellon objects
+- added to preprocessors information regarding the X,Y position at the end of the job
+- Tool Drilling made sure that on Toolchange event after toolchange event the tool feedrate is set
+- added some icons to more push buttons inside the app
+- a change of layout in Tools Database
+- a new icon for Search in DB
+
+14.07.2020
+
+- Drilling Tool - now there is an Excellon preference that control the autoload of tools from the Tools Database
+- Tools Database - remade the UI
+- made sure that the serializable attributes are added correctly and only once (self.ser_attrs)
+- Tools Database - some fixes in the UI (some of the widgets had duplicated names)
+- Tools Database - made sure the on save the tools are saved only with the properties that relate to their targeted area of the app
+- Tools Database - changes can be done only for one tool at a time
+- Tool Database - more changes to the UI
+
+13.07.2020
+
+- fixed a bug in Tools Database: due of not disconnecting the signals it created a race that was concluded into a RuntimeError exception (an dict changed size during iteration)
+- Drilling Tool - working in adding tools auto-load from Tools DB
+- some updates to the Excellon Object options
+- Drilling Tool - manual add from Tools DB is working
+- Drilling Tool - now slots are converted to drills if the checkbox is ON for the tool investigated
+- Drilling Tool - fixes due of changes in properties (preferences)
+- fixed the Drillcncjob TCL command
+- Multiple Tools fix - fixed issue with converting slots to drills selection being cleared when toggling all rows by clicking on the header
+- Multiple Tools fix - fixes for when having multiple tools selected which created issues in tool tables for many tools
+
+12.07.2020
+
+- when creating a new FlatCAM object, the options will be updated with FlatCAM tools properties that relate to them
+- updated the Tools DB class by separating the Tools DB UI into it's own class
+- Tools DB - added the parameters for Drilling Tool
+
+11.07.2020
+
+- moved all Excellon Advanced Preferences to Drilling Tool Preferences
+- updated Drilling Tool to use the new settings
+- updated the Excellon Editor: the default_data dict is populated now on Editor entry
+- Excellon Editor: added a new functionality: conversion of slots to drills
+- Excellon UI: added a new feature that is grouped in Advanced Settings: a toggle tools table visibility checkbox 
+- Drilling Tool - minor fixes
+- Drilling Tool - changes in UI
+- Isolation Tool - modified the UI; preparing to add new feature of polishing at the end of the milling job
+- Tool Paint - fixed an issue when launching the tool and an object other than Geometry or Excellon is selected
+- Geometry UI - moved the UI for polishing from Isolation Tool to Geometry UI (actually in the future Milling Tool) where it belongs
+- Gerber UI - optimized the mark shapes to use only one ShapeCollection
+
+10.07.2020
+
+- Tool Drilling - moved some of the Excellon Preferences related to drilling operation to it's own group Drilling Tool Options
+- optimized the CNCJob UI to look like other parts of the app 
+- in Gerber and Excellon UI added buttons to start the Editor
+- in all Editors Selected Tab added a button to Exit the Editor
+- Tool Drilling - fixed incorrect annotations in CNCJob objects generated; one drawback is that now each tool (when Toolchange is ON) has it's own annotation order which lead to overlapping in the start point of one tool and the end of previous tool
+- Tool Drilling - refactoring methods and optimizations
+
+9.07.2020
+
+- Tool Drilling - remade the methods used to generate GCode from Excellon, to parse the GCode. Now the GCode and GCode_parsed are stored individually for each tool and also they are plotted individually
+- Tool Drilling now works - I still need to add the method for converting slots to drill holes
+- CNCJob object - now it is possible for CNCJob objects originated from Excellon objects, to toggle the plot for a selection of tools
+- working in cleaning up the Excellon UI (Selected Tab)
+- finished the clean-up in Excellon UI
+- Tool Drilling - added new feature to drill the slots
+
+8.07.2020
+
+- Tool Drilling - working on the UI
+- Tool Drilling - added more tool parameters; laying the ground for adding "Drilling Slots" feature
+- added as ToolTip for the the Preprocessor combobox items, the actual name of the items
+- working on Tool Drilling - remaking the way that the GCode is stored, each tool will store it's own GCode
+- working on Tool Drilling
+
+7.07.2020
+
+- updated the Panelize Tool to save the source code for the panelized Excellon objects so it can be saved from the Save project tab context menu entry
+- updated the Panelize Tool to save the source code for the panelized Geometry objects as DXF file
+- fixed the Panelize Tool so the box object stay as selected on new objects are loaded; any selection shape on canvas is deleted when clicking Panelize
+
+6.07.2020
+
+- Convert Any to Excellon. Finished Gerber object conversion to Excellon. Flash's are converted to drills. Traces in the form of a linear LineString (no changes in direction) are converted to slots.
+- Turkish translation updated by Mehmet Kaya for the 8.993 version of strings
+
+2.07.2020
+
+- trying to optimize the resulting geometry in DXF import (and in SVG import) by merging contiguous lines; reduced the lines to about one third of the original
+- fixed importing DXF file as Gerber method such that now the resulting Gerber object is correctly created having the geometry attributes like self.apertures and self.follow_geometry
+- added Turkish translation - courtesy of Mehmet Kaya
+- modified the Gerber export method to take care of the situation where the exported Gerber file is a SVG/DXF file imported as Gerber
+- working in making a new functionality: Convert Any to Excellon. Finished Geometry object conversion to Excellon.
+
+30.06.2020
+
+- fixed the SVG parser so the SVG files with no information regarding the 'height' can be opened in FlatCAM; fixed issue #433
+
+29.06.2020
+
+- fixed the DXF parser to work with the latest version of ezdxf module (issues for the ellipse entity and modified attribute name for the knots_values to knots)
+- fixed the DXF parser to parse correctly the b-splines by not adding automatically a knot value 0f (0, 0) when the spline is not closed
+
+27.06.2020
+
+- Drilling Tool - UI is working as expected; I will have to propagate the changes to other tools too, to increase likeness between different parts of the app
+
+25.06.2020
+
+- made sure that when trying to view the source but no object is selected, the messages are correct
+- wip for Tool Drilling
+
+23.06.2020
+
+- working on Tool Drilling
+
+21.06.2020
+
+- wip
+
+18.06.2020
+
+- fixed bug in the Cutout Tool that did not allowed the manual cutous to be added on a Geometry created in the Tool
+- fixed bug in Cutout Tool that made the selection box show in the stage of adding manual gaps
+- updated Cutout Tool UI
+- Cutout Tool - in manual gap adding there is now an option to automatically turn on the big cursor which could help
+- Cutout Tool - fixed errors when trying to add a manual gap without having a geometry object selected in the combobox
+- Cutout Tool - made sure that all the paths generated by this tool are contiguous which means that two lines that meet at one end will become only one line therefore reducing unnecessary Z moves
+- Panelize Tool - added a new option for the panels of type Geometry named Path Optimization. If the checkbox is checked then all the LineStrings that are overlapped in the resulting multigeo Geometry panel object will keep only one of the paths thus minimizing the tool cuts.
+- Panelize Tool - fixed to work for panelizing Excellon objects with the new data structure storing drills and tools in the obj.tools dictionary
+- put the bases for a new Tool: Milling Holes Tool
+
+17.06.2020
+
+- added the multi-save capability if multiple CNCJob objects are selected in Project tab but only if all are of type CNCJob
+- added fuse tools control in Preferences UI for the Excellon objects: if checked the app will try to see if there are tools with same diameter and merge the drills for those tools; if not the tools will just be added to the new combined Excellon
+- modified generate_from_excellon_by_tool() method in camlib.CNCJob() such that when Toolchange option is False, since the drills will be drilled with one tool only, all tools will be optimized together
+
+16.06.2020
+
+- changed the data structure for the Excellon object; modified the Excellon parser and the Excellon object class
+- fixed partially the Excellon Editor to work with the new data structure
+- fixed Excellon export to work with the new data structure
+- fixed all transformations in the Excellon object attributes; still need to fix the App Tools that creates or use Excellon objects
+- fixed some problems (typos, missing data) generated by latest changes
+- more typos fixed in Excellon parser, slots processing
+- fixed Extract Drills Tool to work with the new Excellon data format
+- minor fix in App Tools that were updated to have UI in a separate class
+- Tool Punch Gerber - updated the UI
+- Tool Panelize - updated the UI
+- Tool Extract Drills - updated the UI
+- Tool QRcode - updated the UI
+- Tool SolderPaste - updated the UI
+- Tool DblSided - updated the UI
+
+15.06.2020
+
+- in Paint Tool and NCC Tool updated the way the selected tools were processed and made sure that the Tools Table rows are counted only once in the processing
+- modified the UI in Paint Tool such that in case of using rest machining the offset will apply for all tools
+- Paint Tool - made the rest machining function for the paint single polygon method
+- Paint Tool - refurbished the 'rest machining' for the entire tool
+- Isolation Tool - fixed to work with selection of tools in the Tool Table (previously it always used all the tools in the Tool Table)
+- Tools Database - added a context menu action to Save the changes to the database even if it's not in the Administration mode
+- Tool Isolation - fixed a UI minor issue: 'forced rest' checkbox state at startup was always enabled
+- started working in moving the Excellon drilling in its own Application Tool
+- created a new App Tool named Drilling Tool where I will move the drilling out of the Excellon UI
+- working on the Drilling Tool - started to create a new data structure that will hold the Excellon object data
+
+14.06.2020
+
+- made sure that clicking the icons in the status bar works only for the left mouse click
+- if clicking the activity icon in the status bar and there is no object selected then the effect will be a plot_all with fit_view
+- modified the FCLabel GUI element
+- NCC Tool - remade and optimized the copper clearing with rest machining: now it works as expected with a reasonable performance
+- fixed issue #428 - Cutout Tool -> Freeform geometry was not generated due of trying to get the bounds of the solid_geometry before it was available
+- NCC Tool - now the tools can be reordered (if the order UI radio is set to 'no')
+- remade the UI in Paint Tool and the tools in tools table ca now be reordered (if the order UI radio is set to 'no')
+- some updates in NCC Tool using code from Paint Tool
+- in Paint and NCC Tools made sure that using the key ESCAPE to cancel the tool will not create mouse events issues
+- some updates in Tcl commands Paint and CopperClear data dicts
+- modified the Isolation Tool UI: now the tools can be reordered (if the order UI radio is set to 'no')
+- modified the Paint, NCC and Isolation Tools that when no tools is selected in the Tools Table, a message will show that no Tool is selected and the Geometry generation button is disabled
+
+13.06.2020
+
+- modified the Tools Database such that there is now a way to mark a tool as meant to be used in a certain part of the application; it will disable or enable parts of the parameters of the tool
+- updated the FCTable GUI element to work correctly when doing drag&drop for the rows
+- updated the Geometry UI to work with the new FCTable
+- made the coordinates / delta coordinates / grid toolbar / actions toolbar visibility an option, controlled from the infobar (Status bar) context menu. How it's at app shutdown it's restored at the next application start
+- moved the init of activity view in the MainGUI file from the APP.__init__()
+- added a new string in the tooltip for the button that adds tool from database specifying the tools database administration is done in the menu
+- when opening a new tab in the PlotTabArea the coordinates toolbars will be hidden and shown after the tab is closed
+
+12.06.2020
+
+- NCC Tool optimization - moved the UI in its own class
+- NCC Tool optimization - optimized the Tool edit method
+- NCC Tool - allow no tool at NCC Tool start (the Preferences have no tool)
+- NCC Tool - optimized tool reset code
+- NCC Tool - fixed the non-rest copper clearing to work as expected: each tool in the tool table will make it's own copper clearing without interference from the rest of the tools 
+- Geometry UI - made again the header clickable and first click selects all rows, second click will deselect all rows.
+- Geometry UI - minor updates in the layout; moved the warning text to the tooltip of the generate_cncjob button
+- Geometry UI - working in making the modification of tool parameters such that if there is a selection of tools the modification in the Tool parameters will be applied to all selected
+
+11.06.2020
+
+- finished tool reordering in Geometry UI
+
+10.06.2020
+
+- fixed bug in the Isolation Tool that in certain cases an empty geometry was present in the solid_geometry which mae the CNCJob object generation to fail. It happen for Gerber objects created in the Gerber Editor
+- working on the tool reordering in the Geometry UI
+- continue - work in tool reordering in Geometry UI
+
+9.06.2020
+
+- fixed a possible problem in generating bounds value for a solid_geometry that have empty geo elements
+- added ability to merge tools when merging Geometry objects if they share the same attributes like: diameter, tool_type or type
+- added a control in Edit -> Preferences -> Geometry to control if to merge/fuse tools during Geometry merging
+
+8.06.2020
+
+- minor changes in the way that the tools are installed and connected
+- renamed the GeoEditor class/file to AppGeoEditor from FlatCAMGeoEditor making it easier to see in the IDE tree structure
+- some refactoring that lead to a working solution when using the Python 3.8 + PyQt 5.15
+- more refactoring in the app Editors
+- added a protection when trying to edit a Geometry object that have multiple tools but no tool is selected
+
+7.06.2020
+
+- refactoring in camlib.py. Made sure that some conditions are met, if some of the parameters are None then return failure. Modifications in generate_from_geometry_2 and generate_from_multitool_geometry methods
+- fixed issue with trying to access GUI from different threads by adding a new signal for printing to shell messages
+- fixed a small issue in Gerber file opener filter that did not see the *.TOP extension or *.outline extension
+- in Excellon parser added a way to "guestimate" the units if no units are detected in the header. I may need to make it optional in Preferences
+- changed the Excellon defaults for zeros suppression to TZ (assumed that most Excellon without units in header will come out of older Eagle) and the Excellon export default is now with coordinates in decimal
+- made sure that the message that exclusion areas are deleted is displayed only if there are shapes in the exclusion areas storage
+- fixed bug: on first ever usage of FlatCAM beta the last loaded language (alphabetically) is used instead of English (in current state is Russian)
+- made sure the the GUI settings are cleared on each new install
+- added a new signal that is triggered by change in visibility for the Shell Dock and will change the status of the shell label in the status bar. In this way the label will really be changed each time the shell is toggled
+- optimized the GUI in Film Tool
+- optimized GUI in Alignment Tool
+
+6.06.2020
+
+- NCC Tool - added a message to warn the user that he needs at least one tool with clearing operation
+- added a GUI element in the Preferences to control the possibility to edit with mouse cursor objects in the Project Tab. It is named: "Allow Edit"
+
+5.06.2020
+
+- fixed a small issue in the Panelization Tool that blocked the usage of a Geometry object as panelization reference
+- in Tool Calculators fixed an application crash if the user typed letters instead of numbers in the boxes. Now the boxes accept only numbers, dots, comma, spaces and arithmetic operators
+- NumericalEvalEntry allow the input of commas now
+- Tool Calculators: allowed comma to be used as decimal separator
+- changed how the import of svg.path module is done in the ParseSVG.py file
+- Tool Isolation - new feature that allow to isolate interiors of polygons (holes in polygons). It is possible that the isolation to be reported as successful (internal limitations) but some interiors to not be isolated. This way the user get to fix the isolation by doing an extra isolation.
+- added mouse events disconnect in the quit_application() method
+- remade the ReadMe tab
+- Tool Isolation - added a GUI element to control if the isolation of a polygon, when done with rest, should be done with the current tool even if its interiors (holes in it) could not be isolated or to be left for the next tool
+- updated all the translation strings to the latest changes
+- small fix
+- fixed the color set for the application objects
+- made some reverts regarding the mods in the quit_application() method - problems when freezed
+RELEASE 8.993
+
+4.06.2020
+
+- improved the Isolation Tool - rest machining: test if the isolated polygon has interiors (holes) and if those can't be isolated too then mark the polygon as a rest geometry to be isolated with the next tool and so on
+- updated the French translation strings - from @micmac (Michel Maciejewski)
+
+3.06.2020
+
+- updated Transform Tool to have a selection of possible references for the transformations that are now selectable in the GUI
+- Transform Tool - compacted the UI
+- minor issue in Paint Tool
+- added a new feature for Gerber parsing: if the NO buffering is chosen in the Gerber Advanced Preferences there is now a checkbox to activate delayed buffering which will do the buffering in background allowing the user to work in between. I hope that this can be useful in case of large Gerber files.
+- made the delayed Gerber buffering to use multiprocessing but I see not much performance increase
+- made sure that the status bar label for preferences is updated also when the Preferences Tab is opened from the Edit -> Preferences
+- remade file names in the app
+- fixed the issue with factory_defaults being saved every time the app start
+- fixed the preferences not being saved to a file when the Save button is pressed in Edit -> Preferences
+- fixed and updated the Transform Tools in the Editors
+- updated the language translation strings (and Google_Translated some of them)
+- made sure that if the user closes the app with an editor open, before the exit the editor is closed and signals disconnected
+- updated the Italian translation - contribution by Golfetto Massimiliano
+- made the timing for the object creation to be displayed in the shell
+
+2.06.2020
+
+- Tcl Shell - added a button to delete the content of the active line
+- Tcl Command Isolate - fixed to work in the new configuration
+- Tcl Command Follow - fixed to work in the new configuration
+- Etch Compensation Tool - added a new etchant: alkaline baths
+- fixed spacing in the status toolbar icons
+- updated the translation files to the latest changes
+- modified behavior of object comboboxes in Paint, NCC and CutOut Tools: now if an object is selected in Project Tab and is of the supported kind in the Tool, it will be auto-selected
+- fixed some more strings
+- updated the Google-translations for the German, Spanish, French
+- updated the Romanian translation
+- replaced the icon for the Editor in Toolbar (both for the normal icons and for icons in dark theme)
+
+1.06.2020
+
+- made the Distance Tool display the angle in values between 0 and 359.9999 degrees
+- changed some strings
+- fixed the warning that old preferences found even for new installation
+- in Paint Tool fixed the message to select a polygon when using the Selection: Single Polygon being overwritten by the "Grid disabled" message
+- more changes in strings throughout the app
+- made some minor changes in the GUI of the FlatCAM Tools
+- in Tools Database made sure that each new tool added has a unique name
+- in AppTool made some methods to be class methods
+- reverted the class methods in AppTool
+- added a button for Transformations Tool in the lower side (common) of the Object UI
+- some other UI changes
+- after using Isolation Tool it will switch automatically to the Geometry UI
+- in Preferences replaced some widgets with a new one that combine a Slider with a Spinner (from David Robertson)
+- in Preferences replaced the widgets that sets colors with a compound one (from David Robertson)
+- made Progressive plotting work in Isolation Tool
+- fix an issue with progressive plotted shapes not being deleted on the end of the job
+- some fixed due of recent changes and some strings changed
+- added a validator for the FCColorEntry GUI element such that only the valid chars are accepted
+- changed the status bar label to have an icon instead of text
+- added a label in status bar that will toggle the Preferences tab
+- made some changes such that that the label in status bar for toggling the Preferences Tab will be updated in various cases of closing the tab
+- changed colors for the status bar labels and added some of the new icons in the gray version
+- remade visibility as threaded - it seems that I can't really squeeze more performance from this
+
+31.05.2020
+
+- structural changes in Preferences from David Robertson
+- made last filter selected for open file to be used next time when opening files (for Excellon, GCode and Gerber files, for now)
+
+30.05.2020
+
+- made confirmation messages for the values that are modified not to be printed in the Shell
+- Isolation Tool: working on the Rest machining: almost there, perhaps I will use multiprocessing
+- Isolation Tool: removed the tools that have empty geometry in case of rest machining
+- Isolation Tool: solved some naming issues
+- Isolation Tool: updated the tools dict with the common parameters value on isolating
+- Fixed a recent change that made the edited Geometry objects in the Geometry Editor not to be plotted after saving changes
+- modified the Tool Database such that when a tool shape is selected as 'V' any change in the Vdia or Vangle or CutZ parameters will update the tool diameter value
+- In Tool Isolation made sure that the use of ESC key while some processes are active will disconnect the mouse events that may be connected, correctly
+- optimized the Gerber UI
+- added a Multi-color checkbox for the Geometry UI (will color differently tool geometry when the geometry is multitool)
+- added a Multi-color checkbox for the Excellon UI (this way colors for each tool are easier to differentiate especially when the diameter is close)
+- made the Shell Dock always show docked
+- fixed NCC Tool behavior when selecting tools for Isolation operation
+
+29.05.2020
+
+- fixed the Tool Isolation when using the 'follow' parameter
+- in Isolation Tool when the Rest machining is checked the combine parameter is set True automatically because the rest machining concept make sense only when all tools are used together
+- some changes in the UI; added in the status bar an icon to control the Shell Dock
+- clicking on the activity icon will replot all objects
+- optimized UI in Tool Isolation
+- overloaded the App inform signal to allow not printing to shell if a second bool parameter is given; modified some GUI messages to use this feature
+- fixed the shell status label status on shell dock close from close button
+- refactored some methods from App class and moved them to plotcanvas (plotcanvaslegacy) class
+- added an label with icon in the status bar, clicking it will toggle (show status) of the X-Y axis on cavnas
+- optimized the UI, added to status bar an icon to toggle the axis 
+- updated the Etch Compensation Tool by adding a new possibility to compensate the lateral etch (manual value)
+- updated the Etch Compensation Tool such that the resulting Gerber object will have the apertures attributes ('size', 'width', 'height') updated to the changes
+
+28.05.2020
+
+- made the visibility change (when using the Spacebar key in Project Tab) to be not threaded and to use the enabled property of the ShapesCollection which should be faster
+- updated the Tool Database class to have the Isolation Tool data
+- Isolation Tool - made to work the adding of tools from database
+- fixed some issues related to using the new Numerical... GUI elements
+- fixed issues in the Tool Subtract
+- remade Tool Subtract to use multiprocessing when processing geometry
+- the resulting Gerber file from Tool Subtract has now the attribute source_file populated
+
+27.05.2020
+
+- working on Isolation Tool: made to work the Isolation with multiple tools without rest machining
+
+26.05.2020
+
+- working on Isolation Tool: made to work the tool parameters data to GUI and GUI to data
+- Isolation Tool: reworked the GUI
+- if there is a Gerber object selected then in Isolation Tool the Gerber object combobox will show that object name as current
+- made the Project Tree items not editable by clicking on selected Tree items (the object rename can still be done in the Selected tab)
+- working on Isolation Tool: added a Preferences section in Edit -> Preferences and updated their usage within the Isolation tool
+- fixed milling drills not plotting the resulting Geometry object
+- all tuple entries in the Preferences UI are now protected against letter entry
+- all entries in the Preferences UI that have numerical entry are protected now against letters
+- cleaned the Preferences UI in the Gerber area
+- minor UI changes
+
+25.05.2020
+
+- updated the GUI fields for the Scale and Offset in the Object UI to allow only numeric values and operators in the list [/,*,+,-], spaces, dots and comma
+- modified the Etch Compensation Tool and added conversion utilities from Oz thickenss and mils to microns
+- added a Toggle All checkbox to Corner Markers Tool
+- added an Icon to the MessageBox that asks for saving if the user try to close the app and there is some unsaved work 
+- changed and added some icons
+- fixed the Shortcuts Tab to reflect the actual current shortcut keys
+- started to work on moving the Isolation Routing from the Gerber Object UI to it's own tool
+- created a new tool: Isolation Routing Tool: work in progress
+- some fixes in NCC Tool
+- added a dialog in Menu -> Help -> ReadMe?
+
+24.05.2020
+
+- changes some icons
+- added a new GUI element which is a evaluated LineEdit that accepts only float numbers and /,*,+,-,% chars
+- finished the Etch Compensation Tool
+- fixed unreliable work of Gerber Editor and optimized the App.editor2object() method
+- updated the Gerber parser such that it will parse correctly Gerber files that have only one solid polygon inside with multiple clear polygons (like those generated by the Invert Tool)
+- fixed a small bug in the Geometry UI that made updating the storage from GUI not to work
+- some small changes in Gerber Editor
+
+23.05.2020
+
+- fixed a issue when testing for Exclusion areas overlap over the Geometry object solid_geometry
+
+22.05.2020
+
+- fixed the algorithm for calculating closest points in the Exclusion areas
+- added the Exclusion zones processing to Geometry GCode generation
+
+21.05.2020
+
+- added the Exclusion zones processing to Excellon GCode generation
+- fixed a non frequent plotting problem for CNCJob objects made out of Excellon objects
+
+19.05.2020
+
+- updated the Italian language (translation incomplete)
+- updated all the language strings to the latest changes; updated the POT file
+- fixed a possible malfunction in Tool Punch Gerber
+
+18.05.2020
+
+- fixed the PDF Tool when importing as Gerber objects
+- moved all the parsing out of the PDF Tool to a new file ParsePDF in the flatcamParsers folder
+- trying to fix the pixmap load crash when running a FlatCAMScript
+- made the workspace label in the status bar clickable and also added a status bar message on status toggle for workspace
+- modified the GUI for Film and Panelize Tools
+- moved some of the GUI related methods from FlatCAMApp.App to the flatcamGUI.MainGUI class
+- moved Shortcuts Tab creation in it's own class
+- renamed classes to have shorter names and grouped
+- removed reference to postprocessors and replaced it with preprocessors
+- more refactoring class names
+- moved some of the methods from the App class to the ObjectCollection class
+- moved all the new_object related methods in their own class AppObjects.AppObject
+- more refactoring; solved some issues introduced by the refactoring
+- solved a circular import
+- updated the language translation files to the latest changes (no translation)
+- working on a new Tool: Etch Compensation Tool -> installed the tool and created the GUI and class template
+- moved more methods out of App_Main class
+- added confirmation messages for toggle of HUD, Grid, Grid Snap, Axis
+- added icon in status bar for HUD; clicking on it will toggle the HUD (heads up display)
+- fixes due of recent changes
+- fixed issue #417
+
+17.05.2020
+
+- added new FlatCAM Tool: Corner Markers Tool which will add line markers in the selected corners of the bounding box of the targeted Gerber object
+- added a menu entry in Menu -> View for Toggle HUD
+- solved the issue with the GUI in the Notebook being expanded too much in width due of the FCDoubleSpinner and FCSpinner sizeHint by setting the sizePolicy to Ignored value
+- fixed the workspace being always A4
+- added a label in the status bar to show if the workplace is active and what size it is
+- now the Edit command (either from Menu Edit ->Edit Object) or through the shortcut key (E key) or project tab context menu works also for the CNCJob objects (will open a text Editor with the GCode)
+- fixed the object collection methods that return a list of objects or names of objects such that they have a parameter now to allow adding to those lists (or not) for the objects of type Script or Document. Thus fixing some of the Tcl commands such Set Origin
+- reverted the previous changes to object collection; it is better to create empty methods in FlatCAMScript and FlatCAMDocument objects
+
+16.05.2020
+
+- worked on the NCC Tool; added a new clear method named 'Combo' which will go through all methods until the clear is done
+- added a Preferences parameter for font size used in HUD
+
+13.05.2020
+
+- updated the French translation strings, made by @micmac (Michel)
+
+12.05.2020
+
+- fixed recent issues introduced in Tcl command Drillcncjob
+- updated the Cncjob to use the 'endxy' parameter which dictates the x,y position at the end of the job
+- now the Tcl commands Drillcncjob and Cncjob can use the toolchangexy and endxy parameters with or without parenthesis (but no spaces allowed)
+- modified the Tcl command Paint "single" parameter. Now it's value is a tuple with the x,y coordinates of the single polygon to be painted.
+- the HUD display state is now persistent between app restarts
+- updated the Distance Tool such that the right click of the mouse will cancel the tool unless it was a panning move
+- modified the PlotCanvasLegacy to decide if there is a mouse drag based on the distance between the press event position and the release event position. If the distance is smaller than a delta distance then it is not a drag move.
+
+11.05.2020
+
+- removed the labels in status bar that display X,Y positions and replaced it with a HUD display on canvas (combo key SHIFT+H) will toggle the display of the HUD
+- made the HUD work in Legacy2D mode
+- fixed situation when the mouse cursor is outside of the canvas and no therefore returning None values
+- remade the Snap Toolbar presence; now it is always active and situated in the Status Bar
+- Snap Toolbar is now visible in Fullscreen
+- in Fullscreen now the Notebook is available but it will be hidden on Fullscreen launch
+- fixed some minor issues (in the HUD added a separating line, missing an icon in toolbars on first launch)
+- made sure that the corner snap buttons are shown only in Editors
+- changed the HUD color when using Dark theme 
+- fix issue in Legacy2D graphic mode where the snap function was not accessible when the PlotCanvasLegacy class was created
+- modified the HUD in Legacy2D when using Dark Theme to use different colors
+- modified how the graphic engine change act in Preferences: now only by clicking Apply(or Save) the change will happen. And there is also a message asking for confirmation
+- re-added the position labels in the status bar; they will be useful if HUD is Off (Altium does the same :) so learn from the best)
+- fixed the Tcl command Cncjob: there was a problem reported as issue #416. The command did not work due of the dpp parameter
+- modified the Tcl command Cncjob such that if some of the parameters are not used then the default values will be used (set with set_sys)
+- modified the Tcl command Drillcncjob to use the defaults when some of the parameters are not used
+
+10.05.2020
+
+- fixed the problem with using comma as decimal separator in Grid Snap fields
+
+9.05.2020
+
+- modified the GUI for Exclusion areas; now the shapes are displayed in a Table where they can be selected and deleted. Modification applied for Geometry Objects only (for now).
+- fixed an error when converting units, error that acted when in those fields that accept lists of tools only one tool was added
+- finished the GUI for exclusion areas both in the Excellon and Geometry Objects. Need to think if to make it visible only in Advanced Mode
+
+8.05.2020
+
+- added a parameter to the FlatCAMDefaults class, whenever a value in the self.defaults dict change it will call a callback function and send to it the modified key
+- optimized and fixed some issues in the self.on_toggle_units() method
+- the Exclusion areas will have all the orange color but the color of the outline will differ according to the type of the object from where it was added (cosmetic use only as the Exclusion areas will be applied globally)
+- removed the Apply theme button in the Preferences; it is now replaced by the more general buttons (either Save or Apply)
+- added a confirmation/warning message when applying a new theme
+
+7.05.2020
+
+- added a fix so the app close is now clean, with exit code 0 as set
+- added the ability to add exclusion areas from the Excellon object too. Now there is a difference in color to differentiate from which type of object the exclusion areas were added but they all serve the same purpose
+
+6.05.2020
+
+- wip in adding Exclusion areas in Geometry object; each Geometry object has now a storage for shapes (exclusion shapes, should I make them more general?)
+- changed the above: too many shapes collections and the performance will go down. Created a class ExclusionAreas that holds all the require properties and the Object UI elements will connect to it's methods. This way I can apply this feature to Excellon object too (who is a special type of Geometry Object)
+- handled the New project event and the object deletion (when all objects are deleted then the exclusion areas will be deleted too)
+- solved issue with new parameter end_xy when it is None
+- solved issue with applying theme and not making the change in the Preferences UI. In Preferences UI the theme radio is always Light (white)
+- now the annotations will invert the selected color in the Preferences, when selecting Dark theme 
+
+5.05.2020
+
+- fixed an issue that made the preprocessors combo boxes in Preferences not to load and display the saved value fro the file
+- some PEP8 corrections
+
+4.05.2020
+
+- in detachable tabs, Linux loose the reference of the detached tab and on close of the detachable tabs will gave a 'segmentation fault' error. Solved it by not deleting the reference in case of Unix-like systems
+- some strings added to translation strings
+
+3.05.2020
+
+- small changes to allow making the x86 installer that is made from a Python 3.5 run FlatCAM beta 
+- fixed multiple parameter 'outname' in the Tcl commands OpenGerber and OpenGcode 
+- added more examples in the scripts Examples: isolate and cutout examples
+- updated the Italian translation
+- updated the translation files
+- changed the line endings for Makefile and setup_ubuntu.sh files
+- protected a dict in VispyVisuals from issuing errors of keys changed while iterating through it
+
+2.05.2020
+
+- working on a new feature: adding interdiction area for Gcode generation. They will be added in the Geometry Object
+
+2.05.2020
+
+- changed the icons for the grid snap in the status bar
+- moved some of the methods from FlatCAMApp.App to flatcamGUI.MainGUI class
+- fixed bug in Gerber Editor in which the units conversion wasn't calculated correct
+- fixed bug in Gerber Editor in which the QThread that is started on object edit was not stopped at clean up stage
+- fixed bug in Gerber Editor that kept all the apertures (including the geometry) of a previously edited object that was not saved after edit
+- modified the Cutout Tool to generate multi-geo objects therefore the set geometry parameters will populate the Geometry Object UI
+- modified the Panelize Tool to optimize the output from Cutout Tool such that there are no longer overlapping cuts
+- some string corrections
+- updated the Italian translation done by user @pcb-hobbyst (Golfetto Massimiliano)
+- RELEASE 8.992
+
+01.05.2020
+
+- added some ToolTips (strings needed to be translated too) for the Cut Z entry in Geometry Object UI that explain why is sometime disabled and reason for it's value (sometime is zero)
+- solve parenting issues when trying to load a FlatScript from Menu -> File -> Scripting
+- added a first new example script and added some files to work with
+- added a new parameter that will store the home folder of the FlatCAM installation so we can access the example folder
+- added in Gerber editor a method for zoom fit that takes into consideration the current geometry of the edited object
+
+30.04.2020 
+
+- made some corrections - due of recent refactoring PyCharm reported errors all over (not correct but it made programming difficult)
+- modified the requirements.txt file to force svg.path module to be at least version 4.0
+- fixed bug in Tools DB that crashed when a tool is copied
+- in Tools Database added a Save Button whose color is changed in Red if the DB was modified and back to default when the DB is saved.
+- fixed bug in Tool DB that crashed the app when the Tool Name was modified but there was no tree item (a tool in the list) selected in the Tree widget (list of tools)
+- now on tool add and tool copy, the last item (tool, which is the one added) is autoselected; o tool delete always the first item (tool) is selected
+- fixed issue #409; problem was due of an assert I used in the handler of the Menu ->Options -> Flip X(Y) menu entry
+- activated and updated the editing in the Aperture Table in the Gerber Editor; not all parameters can be edited for every type of aperture
+- some strings updated
+- fixed a small issue in loading the Projects
+
+29.04.2020
+
+- added a try-except clause in the FlatCAMTranslation.restart_program() when closing the Listener and the thread that runs it to adjust to MacOS usage
+- more PEP8 changes
+- in PreferencesUI.PreferencesUIManager class I removed the need to pass reference to the App class since it this was available through the 'ui' parameter
+- some fixes due to recent refactoring
+- minor bugs fixed (not so visible)
+- promoted some methods to be static
+- set the default layout on first run to the 'minimal' value
+- modified the method that detects which tab was closed in the Plot Area so it will no longer depend on it's translated text but on it's objectName set on the QTab creation
+- fixed the merge methods for all FlatCAM objects
+- fixed a SyntaxError Exception when checking for types of found old preferences
+- updated the French, German and Spanish Google translations
+- updated the Romanian translation
+- fixed units conversion issue
+- updated the units conversion method to convert all the convertible parameters in the Preferences
+- solved the problem with not closing all the tabs in Plot Area when creating a New Project; the issue was that once a tab was removed the indexes are remade (when tab 0 is removed then tab 1 becomes tab 0 and so on)
+- some more strings changed -> updated the translations
+- replaced some FormLayouts with Gridlayouts in Tool Cutout.
+
+28.04.2020
+
+- handled a possible situation in App.load_defaults() method
+- fixed some issues in FlatCAMDB that may appear in certain scenarios
+- some minor changes in the Python version detection
+- added a new Tcl Command named SetPath which will set a path to be used by the Tcl commands. Once set will serve as a fallback path in case that the files fail to be opened first time. It will be persistent, saved in preferences.
+- added the GUI for the new Open Example in the FIle -> Scripting menu.
+- I am modifying all the open ... handlers to add a parameter that will flag if the method was launched from Tcl Shell. This way if the method will fail to open the filename (which include the path) it will try to open from a set fallback path.
+- fixed issue #406, bug introduced recently (leftover changes).
+- modified the ImportSVG Tcl command name to OpenSVG (open_svg alias)
+- added a new Tcl command named OpenDXF (open_dxf alias)
+- fixed some errors in Scripting features
+- added a new Tcl command named GetPath as a convenient way to get the current default path stored in App.defaults['global_tcl_path']
+- added a new package to be installed in Linux to make available the black theme for FlatCAM beta
+- moved all the 'share' resources (icons) to the 'assets/resources' folder
+- some more fixes to problems generated by latest changes in the open handlers
+- modified the make_freezed.py script for the new location of the icons
+- added a fix for the ConnectionRefusedError in Linux that is issued when first running after a FlatCAM crash
+- in SVG parser modified some imports to be one on each line
+- fixed the Tcl Command BBox (leftovers from recent global changes)
+- fixed some typos in strings reported by @pcb-hobbyst on FlatCAM forum
+- disabled a skip_quotes method in ToolShell.FCShell class so I can now use quotes to enclose file paths with spaces inside
+
+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
+- replaced the testing if instance of FlatCAMObj with testing the obj.kind attribute
+- removed the import of the whole FlatCAMApp file only for the usage of GracefulException
+- remove the import of FlatCAMApp and used alternate ways
+- optimized the imports in some files
+- moved the Bookmarksmanager and ToolDB classes into their own files
+- solved some bugs that were not so visible in the Editors and HPGL parser
+- split the FlatCAMObj file into multiple files located in the flatcamObjects folder and renamed the contained classes with names more suggestive
+- updated the Google Translation for the German language
+- added support for Hungarian language - no translation for now
+- minor changes
+- moved the ObjectCollection class to the flatcamObjects folder where it belongs
+- Linux Makefile 
+
+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
+- fixed links in Attributions tab in Help -> About FlatCAM to be able to open external links.
+- updated Google Translations for French and Spanish languages
+- added some '\n' chars in the Help Tcl command to make the help more readable
+
+24.04.2020
+
+- some PEP changes, some method descriptions updated
+- added a placeholder text to 2Sided Tool
+- added a new menu entry in the context menu of the Tcl Shell: 'Save Log' which will save the content of the Tcl Shell browser window to a file
+- the status bar messages that are echoed in the Tcl Shell will no longer have all text colored but only the identifier
+- some message strings cleanup
+- added possibility to save as text file the content in Tcl Shell browser window when clicking the Save log context menu entry
+- fixed an issue regarding the statusbar pixmap selection
+- update the language template strings.pot and updated the Romanian translation
+- updated the Readme file with the steps for installation for MacOS
+- updated the requirements.txt file
+- updated some of the icons in the dark_resources folder (some added, some modified)
+- updated Paint Tool for the new Tool DB
+- updated the Tcl commands CopperClear and Paint
+
+23.04.2020 
+
+- fixed the Tcl Command Help to work as expected; made the text of the commands to be colored in Red color and bold
+- added a 'Close' menu entry in the Tcl Shell context menu that will close (hide) the Tcl Shell Dock widget
+- on launching the Tcl Shell the Edit line will take focus immediately 
+- in App.on_mouse_move_over_plot() method no longer will be done a setFocus() on every move, only when it is needed
+- added an extra check if old preferences files are detected, a check if the type of the values is the same with the type in the current preferences file. If the type is not the same then the current type is preferred.
+- aligned the Tcl commands display when the Help Tcl command is run without parameters
+- fixed the Tcl command Plot_All that malfunctioned if there were any FlatCAM scripts (or FlatCAM documents) open
+- updated the shortcuts list
+
+22.04.2020 
+
+- 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
+
+- made the Grid icon in the status bar clickable and it will toggle the snap to grid function
+- some mods in the Distance Tool
+- added ability to use line width when adding shapes for both Legacy and OpenGL graphic engines
+- added the linewidth=2 parameter for the Tool Distance utility geometry
+- fixed a selection issue in Legacy graphic mode for single click
+- added a CHANGELOG file and changed the README file to contain the installation instructions
+- updated the README file
+- in Project Tab added tooltips for the loaded objects
+- fixed a bug in loading objects by drag&drop into the Project Tab where only one object in the selection was loaded
+
+19.04.2020 
+
+- fixed a bug that did not allow to edit GUI elements of type FCDoubleSpinner if it contained the percent symbol
+- some small optimizations in the GUI of Cutout Tool
+- fixed more issues (new) in NCC Tool
+- added a new layout named 'minimal'
+- some PEP8 changes in Geometry Editor
+
+15.04.2020 
+
+- made sure that the Tcl commands descriptions listed on help command are aligned
+
+14.04.2020 
+
+- lightened the hue of the color for 'success' messages printed in the Tcl Shell browser
+- modified the extensions all over such the names include also the extension name. For Linux who does not display the extensions in the native FileDialog.
+- added descriptions for some of the methods in the app.
+- added lightened icons for the dark theme from Leandro Heck 
+
+13.04.2020 
+
+- added the outname parameter for the geocutout Tcl command
+- multiple fixes in the Tcl commands (especially regarding the interchange between True/false and 1/0 values)
+- updated the help for all Tcl Commands
+- in Tcl Shell, the 'help' command will add also a brief description for each command in the list
+- updated the App.plot_all() method giving it the possibility to be run as threaded or not
+- updated the Tcl command PlotAll to be able to run threaded or not
+- updated the Tcl commands PlotAll and PlotObjects to have a parameter that control if the objects are to be plotted or not on canvas; it serve as a disable/enable
+- minor update to the autocomplete dictionary
+- the Show Shell in Edit -> Preferences will now toggle the Tcl shell based on the current status of the Tcl Shell
+- updated the Tcl command Isolate help for follow parameter 
+- updated DrillCncJob Tcl Command with new parameters and fixed it to work in the new format of the Excellon methods
+- fixed issue #399
+- changed CncJob Tcl Command parameter 'depthperpass' to a shorter 'dpp'
+
+11.04.2020 
+
+- fixed issue #394 - the saveDialog in Linux did not added the selected extension
+- when the Save button is clicked in the Edit -> Preferences the Preferences tab is closed.
+
+10.04.2020 
+
+- made sure that the timeout parameter used by some Tcl Commands is seen as an integer in all cases - fixed issue #389
+- minor changes in Paint Tool
+- minor changes in GUI (Save locations in Menu -> File) and the key shortcuts - fixed issue #391
+
+
+9.04.2020 
+
+- if FlatCAM is not run with Python version >= 3.5 it will exit.
+- modified all CTRL+ with Ctrl+ and all ALT+ with Alt+ and all SHIFT+ with Shift+. Fixed issue #387.
+- removed some packages from setup_ubuntu.sh as they are not needed in FlatCAM beta
+
+8.4.2020 
+
+- fixed the Tcl Command Delete to have an argument -f that will force deletion evading the popup (if the popup is enabled). The sme command without a name now will delete all objects
+- fixed the Tcl Command JoinExcellons
+- fixed the Tcl Command JoinGeometry
+- fixed the Tcl Command Mirror
+- updated the Tcl Command Mirror to use a (X,Y) origin parameter. Works if the -box parameter is not used.
+- updated the Tcl Command Offset. Now it can use only -x or -y parameter no longer is mandatory to have both. The one that is not present will be assumed 0.0
+- updated the Tcl Command Panelize. The -rows and -columns parameters are no longer both required. If one is not present then it is assumed to be zero.
+- updated the Tcl Command Scale. THe -origin parameter can now be a tuple of (x,y) coordinates.
+- updated the Tcl Command Skew. Now it can use only -x or -y parameter no longer is mandatory to have both. The one that is not present will be assumed 0.0
+- updated the help for all the Tcl Commands
+
+6.04.2020 
+
+- added key shortcuts (arrow up/down) that will select the objects in the Project tab if the focus is in that tab
+- added a minor change to the ListSys Tcl command
+- fixed an crash generated when running the Tool Database from the Menu -> Options menu entry
+- fixed a bug in handling the UP/DOWN key shortcuts that caused a crash when no object was selected in the Project Tab; also made sure that the said keys are handled only for the Project Tab
+- some PEP8 changes and other minor changes
+- updated the requirements file
+- updated the 2Sided Tool by not allowing the Gerber file to be mirrored without a valid reference and added some placeholder texts
+
+5.04.2020 
+
+- made sure that the HDPI scaling attribute is set before the QApplication is started
+- made sure that when saving a project, the app will try to update the active object from UI form only if there is an active object
+- fix for contextual menus on canvas when using PyQt versions > 5.12.1
+- decision on which mouse button to use for panning is done now once when setting the plotcanvas
+- fix to work with Python 3.8 (closing the application)
+- fixed bug in Gerber parser that allowed loading as Gerber of a file that is not a Gerber
+- fixed a bug in extension detection for Gerber files that allowed in the filtered list files that extension *.gb*
+- added a processEvents method in the Gerber parser parse_lines() method
+- fixed issue #386 - multiple Cut operation on a edited object created a crash due of the bounds() method
+- some changes in the Geometry UI
+
+4.04.2020 
+
+- fixed the Repeated code parsing in Excellon Parse
+
+1.04.2020 
+
+- updated the SVG parser to take into consideration the 'Close' svg element and paths that are made from a single line (we may need to switch to svgpathtools module)
+- minor changes to increase compatibility with Python 3.8
+- PEP8 changes
+
+30.03.2020
+
+- working to update the Paint Tool
+- fixed some issues in Paint Tool
+
+29.03.2020
+
+- modified the new database to accept data from NCC and Paint Tools
+- fixed issues in the new database when adding the tool in a Geometry object
+- fixed a bug in Geometry object that generated a change of dictionary while iterating over it
+- started to add the new database links in the NCC and Paint Tools
+- in the new Tools DB added ability to double click on the ID in the tree widget to execute adding a tool from DB
+- working in updating NCC Tool
+
+28.03.2020
+
+- finished the new database based on a QTreeWidget
+
+21.03.2020
+
+- fixed Cutout Tool to work with negative values for Margin parameter
+
+20.03.2020
+
+- updated the "re-cut" feature in Geometry object; now if the re-cut parameter is non zero it will cut half of the entered distance before the isolation end and half of it after the isolation end
+- added to Paint and NCC Tool a feature that allow polygon area selection when the reference is selected as Area Selection
+- in Paint Tool and NCC Tool added ability to use Escape Tool to cancel Area Selection and for Paint Tool to cancel Polygon Selection
+- fixed issue in "re-cut" feature when combined with multi-depth feature
+- fixed bugs in cncjob TclCommand
+
+13.03.2020
+
+- fixed a bug in CNCJob generation out of a Excellon object; the plot failed in case some of the geometry of the CNCJob was invalid
+- fixed Properties Tool due of recent changes to the FCTree widget
+
+12.03.2020
+
+- working on the new database
+- fix a bug in the TextInputTool in FlatCAM Geometry Editor that crashed the sw when some fonts are not loaded correctly
+
+4.03.2020
+
+- updated all the FlatCAM Tools and the Gerber UI FCComboBoxes to update the box value with the latest object loaded in the App
+- some fixes in the NCC Tool
+- modified some strings
+
+02.03.2020
+
+- added property that allow the FCComboBox to update the view with the last item loaded; updated the app to use this property
+
+01.03.2020
+
+- updated the CutOut Tool such that while adding manual gaps, the cutting geometry is updated on-the-fly if the gap size or tool diameter parameters are adjusted
+- updated the UI in Geometry Editor
+
+29.02.2020
+
+- compacted the NCC Tool UI by replacing some Radio buttons with Combo boxes due of too many elements
+- fixed error in CutOut Tool when trying to create a FreeFrom Cutout out of a Gerber object with the Convex Shape checked
+- working on a new type of database
+
+28.02.2020
+
+- some small changes in preprocessors
+- solved issue #381 where there was an error when trying to generate CNCJob out of an Excellon file that have a tool with only slots and no drills
+- solved some issues in the preprocessors regarding the newly introduced feature that allow control of the final move X,Y positions
+
+25.02.2020
+
+- fixed bug in Gerber parser: it tried to calculate a len() for a single element and not a list - a Gerber generated by Eagle exhibited this
+- added a new parameter named 'End Move X,Y' for the Geometry and Excellon objects. Adding a tuple of coordinates in this field will control the X,Y position of the final move; not entering a value there will cause not to make an end move
+
+20.02.2020
+
+- in Paint Tool replaced the Selection radio with a combobox GUI element that is more compact
+- in NCC Tool modified the UI
+
+19.02.2020
+
+- fixed some issues in the Geometry Editor; the jump signal disconnect was failing for repeated Editor tool operation
+- fixed an issue in Gerber Editor where the multiprocessing pool was reported as closed and an ValueError exception was raised in a certain scneraio
+- on Set Origin, Move to Origin and Move actions for Gerber and Excellon objects the source file will be also updated (the export functions will export an updated object)
+- in FlatCAMObj.export_gerber() method took into account the possibility of polygons of type 'clear' (the ones found in the Gerber files under the LPC command)
+
+17.02.2020
+
+- updated the Excellon UI to hold data for each tool
+- in Excellon UI removed the tools table column for Offset Z and used the UI form parameter
+- updated the Excellon Editor to add for each tool a 'data' dictionary
+- updated all FlatCAM tools to use the new confirmation message that show if the entered value is within range or outside
+- updated all FlatCAM tools to use the new confirmation message for QSpinBoxes, too
+- in Excellon UI protected the values that are common parameters from change on tool selection change
+- fixed some issues related to the usage of the new confirmation message in FlatCAM Tools
+- made sure that the FlatCAM Tools UI initialization is done only in set_tool_ui() method and not in the constructor
+- adapted the GCode generation from Excellon to work with multiple tools data and modified the preprocessors header
+- when multiple tools are selected in Excellon UI and parameters are modified it will applied to all selected
+- in Excellon UI, Paint Tool and NCC Tool finished the "Apply parameters to all tools" functionality
+- updated Paint Tool and NCC Tool in the UI functionality
+- fixed the Offset spinbox not being controller by offset checkbox in NCC Tool
+
+16.02.2020
+
+- small update to NCC Tool UI
+
+15.02.2020
+
+- in Paint Tool added a new method of painting named Combo who will pass through all the methods until the polygon is cleared
+- in Paint Tool attempting to add a new mode suitable for Laser usage
+- more work in the new Laser Mode in the Paint Tool
+- modified the Paint Tool UI
+
+14.02.2020
+
+- adjusted the UI for Excellon and Geometry objects
+- added a new FlatCAM Tool: Gerber Invert Tool. It will invert the copper features in a Gerber file: where is copper there will be empty and where is empty it will be copper
+- added the Preferences entries for the Gerber Invert Tool
+
+13.02.2020
+
+- finished Punch Gerber Tool
+- minor changes in the Tool Transform and Tool Calculators UI to bring them up2date with the other tools
+
+12.02.2020
+
+- working on fixing a bug in GeometryObject.merge() - FIXED issue #380
+- fixed bug: when deleting a FlatCAMCNCJob with annotations enabled, the annotations are not deleted from canvas; fixed issue #379
+- fixed bug: creating a new project while a project is open and it contain CNCJob annotations and/or Gerber mark shapes, did not delete them from canvas
+
+11.02.2020
+
+- working on Tool Punch; finished the geometry update with the clear geometry for the case of Excellon method
+- working on Tool Punch; finished the geometry update with the clear geometry for the case of Fixed Diameter method
+
+10.02.2020
+
+- optimized the Paint and NCC Tools. When the Lines type of painting/clearing is used, the lines will try to arrange themselves on the direction that the lines length clearing the polygon are bigger
+- solved bug that made drilling with Marlin preprocessor very slow
+- applied the fix for above bug to the TclCommand Drillcncjob too
+- started a new way to clear the Gerber polygons based on the 'follow' lines
+- some cleanup and bug fixes for the Paint Tool
+
+
+8.02.2020
+
+- added a new preprocessor for using laser on a Marlin 3D printer named 'Marlin_laser_use_Spindle_pin'
+- modified the Geometry UI when using laser preprocessors
+- added a new preprocessor file for using laser on a Marlin motion controller but with the laser connected to one of the FAN pins, named 'Marlin_laser_use_FAN_pin'
+- modified the Excellon GCode generation so now it can use multi depth drilling; modified the preprocessors to show the number of passes
+
+5.02.2020
+
+- Modified the Distance Tool such that the Measure button can't be clicked while measuring is in progress
+- optimized selection of drills in the Excellon Editor
+- fixed bugs in multiple selection in Excellon Editor
+- fixed selection problems in Gerber Editor
+- in Distance Tool, when run in the Excellon or Gerber Editor, added a new option to snap to center of the geometry (drill for Excellon, pad for Gerber)
+
+3.02.2020
+
+- modified Spinbox and DoubleSpinbox Custom UI elements such that they issue a warning status message when the typed value is out of range
+- fixed the preprocessors with 'laser' in the name to use the spindle direction set in the Preferences
+- increased the upper limit for feedrates by an order of magnitude
+
+2.02.2020
+
+- fixed issue #376 where the V-Shape parameters from Gerber UI are not transferred to the resulting Geometry object if the 'combine' checkbox is not checked in the Gerber UI
+- in Excellon UI, if Basic application mode is selected in Preferences, the Plot column 'P' is hidden now because some inexperienced users mistake this column checkboxes for tool selection
+- fixed an error in Gerber Parser; the initial values for current_x, current_y were None but should have been 0.0
+- limited the lower limit of angle of V-tip to a value of 1 because 0 makes no sense 
+- small changes in Gerber UI
+- in Geometry Editor make sure that after an edit is finished (correctly or forced) the QTree in the Editor UI is cleared of items
+
+31.01.2020
+
+- added a new functionality, a variation of Set Origin named Move to Origin. It will move a selection of objects to origin such as the bottom left corner of the bounding box that fit them all is in origin.
+- fixed some bugs
+- fixed a division by zero error: fixed #377
+
+30.01.2020
+
+- remade GUI in Tool Cutout, Tool Align Objects, Tool Panelize
+- some changed in the Excellon UI
+- some UI changes in the common object UI
+
+29.01.2020
+
+- changes in how the Editor exit is handled
+- small fix in some pywin32 imports
+- remade the GUI + small fixes in 2Sided Tool
+- updated 2Sided Tool
+
+28.01.2020
+
+- some changes in Excellon Editor
+
+27.01.2020
+
+- in Geometry Editor made sure that on final save, for MultiLineString geometry all the connected lines are merged into one LineString to minimize the number of vertical movements in GCode
+- more work in Punch Gerber Tool
+- the Jump To popup window will now autoselect the LineEdit therefore no more need for an extra click after launching the function
+- made some structural changes in Properties Tool
+- started to make some changes in Geometry Editor
+- finished adding in Geometry Editor a TreeWidget with the geometry shapes found in the edited object
+
+24.02.2020
+
+- small changes to the Toolchange manual preprocessor
+- fix for plotting Excellon objects if the color is changed and then the object is moved
+- laying the GUI for a new Tool: Punch Gerber Tool which will add holes in the Gerber apertures
+- fixed bugs in Minimum Distance Tool
+- update in the GUI for the Punch Gerber Tool
+
+22.01.2020
+
+- fixed a bug in the bounding box generation
+
+19.01.2020
+
+- fixed some bugs that are visible in Linux regarding the ArgsThread class: on app close we need to quit the QThread running the ArgsThread class and also close the opened Socket
+- make sure that the fixes above apply when rebooting app for theme change or for language change
+- fixed and issue that made setting colors for the Gerber file not possible if using a translation
+- made possible to set the colors for Excellon objects too
+- added to the possible colors the fundamentals: black and white
+- in the project context menu for setting colors added the option to set the transparency and also a default option which revert the color to the default value set in the Preferences
+
+17.01.2020
+
+- more changes to Excellon UI
+- changes to Geometry UI
+- more work in NCC Tool upgrade; each tool now work with it's own set of parameters
+- some updates in NCC Tool
+- optimized the object envelope generation in the redesigned NCC Tool
+
+16.01.2020
+
+- updated/optimized the GUI in Preferences for Paint Tool and for NCC Tool
+- work in Paint Tool to bring it up to date with NCC Tool
+- updated the GUI in preferences for Calculator Tool
+- a small change in the Excellon UI
+- updated the Excellon and Geometry UI to be similar
+- put bases for future changes to Excellon Object UI such that each tool will hold it's own parameters
+- in ParseExcellon.Excellon the self.tools dict has now a key 'data' which holds a dict with all the default values for Excellon and Geometry
+- Excellon and Geometry objects, when started with multiple tools selected, the parameters tool name reflect this situation
+- moved default_data data update from Excellon parser to the Excellon object constructor
+
+15.01.2020
+
+- added key shortcuts and toolbar icons for the new tools: Align Object Tool (Alt+A) and Extract Drills (Alt+I)
+- added new functionality (key shortcut Shift+J) to locate the corners of the bounding box (and center) in a selected object
+- modified the NCC Tool GUI to prepare for accepting a tool from a tool database
+- started to modify the Paint Tool to be similar to NCC Tool and to accept a tool from a database
+- work in Paint Tool GUI functionality
+
+14.01.2020
+
+- in Extract Drill Tool added a new method of drills extraction. The methods are: fixed diameter, fixed annular ring and proportional
+- in Align Objects Tool finished the Single Point method of alignment
+- working on the Dual Point option in Align Objects Tool - angle has to be recalculated
+- finished Dual Point option in Align Objects Tool
+
+13.01.2020
+
+- fixed a small GUI issue in Excellon UI when Basic mode is active
+- started the add of a new Tool: Align Objects Tool which will align (sync) objects of Gerber or Excellon type
+- fixed an issue in Gerber parser introduced recently due of changes made to make Gerber files produced by Sprint Layout
+- working on the Align Objects Tool
+
+12.01.2020
+
+- improved the circle approximation resolution
+- fixed an issue in Gerber parser with detecting old kind of units
+- if CTRL key is pressed during app startup the app will start in the Legacy(2D) graphic engine compatibility mode
+
+11.01.2020
+
+- fixed an issue in the Distance Tool
+- expanded the Extract Drills Tool to use a particular annular ring for each type of aperture flash (pad)
+- Extract Drills Tool: fixed issue with oblong pads and with pads made from aperture macros
+- Extract Drills Tool: added controls in Edit -> Preferences
+
+10.02.2020
+
+- working on a new tool: Extract Drills Tool who will create a Excellon object out of the apertures of a Gerber object
+- finished the GUI in the Extract Drills Tool
+- fixed issue in Film Tool where some parameters names in calls of method export_positive() were not matching the actual parameters name
+- finished the Extract Drills Tool
+- fixed a small issue in the DoubleSided Tool
+
+8.01.2020
+
+- working in NCC Tool
+- selected rows in the Tools Tables will stay colored in blue after loosing focus instead of the default gray
+- in NCC Tool the Tool name in the Parameters section will be the Tool ID in the Tool Table
+- added an exception catch in case the plotcanvas init failed for the OpenGL graphic engine and warn user about what happened
+
+7.01.2020
+
+- solved issue #368 - when using the Enable/Disable prj context menu entries the plotted status is not updated in the object properties
+- updates in NCC Tool
+
+6.01.2020
+
+- working on new NCC Tool
+
+2.01.2020
+
+- started to rework the NCC Tool GUI in preparation for adding a Tool DB feature
+- for auto-completer, now clicking an entry in the completer popup will select that entry and insert it
+- made available only for Linux and Windows (not OSX) the starting of the thread that checks if another instance of FlatCAM is already running at the launch of FLatCAM
+- modified Toggle Workspace function to work in the new Preferences UI configuration
+- cleaned the app from progress signal usage since it is not used anymore
+
+1.01.2020
+
+- fixed bug in NCC Tool: after trying to add a tool already in the Tool Table when trying to change the Tool Type the GUI does not change
+- final fix for app not quiting when running a script as argument, script that has the quit_flatcam Tcl command; fixed issue #360
+- fixed issue #363. The Tcl command drillcncjob does not create tool cut, does not allow creation of gcode, it forces the usage of dwell and dwelltime parameters
+- in NCC Tool I've added a warning so the user is warned that the NCC margin has to have a value of at least the tool diameter that is doing an iso_op job in the Tool Table
+- modified the Drillcncjob and Cncjob Tcl commands to be allowed to work without the 'dwell' and 'toolchange' arguments. If 'dwelltime' argument is present it will be assumed that the 'dwell' is True and the same for 'toolchangez' parameter, if present then 'toolchange' will be assumed to be True, else False
+- modified the extracut and multidepth parameters in Cncjob Tcl command like for dwell and toolchange
+- added ability for Tcl commands to have optional arguments with None value (meaning missing value). This case should be treated for each Tcl command in execute() method
+- fixed the Drillcncjob Tcl command by adding an custom self.options key "Tools_in_use" and build it's value, in case it does not exist, to make the toolchange command work
+- middle mouse click on closable tabs will close them
+
+30.12.2019
+
+- Buffer sub-tool in Transform Tool: added the possibility to apply a factor effectively scaling the aperture size thus the copper features sizes
+- in Transform Tool adjusted the GUI
+- fixed some decimals issues in NCC Tool, Paint Tool and Excellon Editor (they were still using the hardcoded values)
+- some small updates in the NCC Tool
+- changes in the Preferences UI for NCC and Paint Tool in Tool Dia entry field
+- fixed Tcl commands that use the overlap parameter to switch from fraction to percentage
+- in Transform Tool made sure that the buffer sub-tool parameters are better explained in tooltips
+- attempt to make TclCommand quit_flatcam work under Linux
+- some fixes in the NCC Tcl command (using the bool() method on some params)
+- another attempt to make TclCommand quit_flatcam work under Linux
+- another attempt to make TclCommand quit_flatcam work under Linux - use signal to call a hard exit when in Linux
+- TclCommand quit_flatcam work under Linux
+
+29.12.2019
+
+- the Apply button text in Preferences is now made red when changes were made and require to be applied
+- the Gerber UI is built only once now so the process is lighter on CPU
+- the Gerber apertures marking shapes storage is now built only once because the more are built the more sluggish is the interface
+- added a new function called by shortcut key combo Ctrl+G when the current widget in Plot Area is an Code Editor. It will jump to the specified line in the text.
+- fixed a small bug where the app tried to hide a label that I've removed previously
+- in Paint Tool Preferences is allowed to add a list of initial tools separated by comma
+- in Geometry Paint Tool fixed the Overlap rate to work between 0 and 99.9999%
+
+28.12.2019
+
+- more updates to the Preferences window and in some other parts of the GUI
+- updated the translations (less Russian)
+- fixed a minor issue that when saving a project with CNCJob objects, the variable that holds the origin of the CNCJob object was not saved in the project. Added to the serializable objects also the exc_cnc_tools dictionary 
+- some changes in the File menu
+
+28.12.2019
+
+- updated all the translations files
+- fixed the big mouse cursor in OpenGL(3D) graphic mode to get the set color
+- fixed the cursor to have the set color and set cursor width in the Legacy(2D) graphic engine
+- in Legacy(2D) graphic mode fixed the cursor toggle when the big cursor is activated
+- in Legacy(2D) fixed big mouse cursor to snap to the grid
+- RELEASE 8.991
+
+27.12.2019
+
+- updated the POT file and the translation files for German, Spanish and French languages
+- fixed some typos
+
+26.12.2019
+
+- modified the ToolDB class and changed some strings
+- Preferences classes now have access to the App attributes through app.setup_obj_classes() method
+- moved app.setup_obj_classes() upper in the App.__init__()
+- added a new Preferences setting allowing to modify the mouse cursor color
+- remade the GUI in Preferences -> General grouping the settings in a more clear way
+- made available the Jump To function in Excellon Editor
+- added a clean_up() method in all the Editor Tools that need it, to be run when aborting using the ESC key
+- fixed an error in the Gerber parser; it did not took into consideration the aperture size declared before the beginning of a Gerber region. Detected for Gerber files generated by KiCAD 5.x
+- in Panelize Tool made sure that for Gerber objects if one of the apertures is without geometry then it is ignored
+- further modifications in Preferences -> General GUI
+- further modifications in Preferences -> General GUI - extended the changes
+- in Legacy(2D) graphic engine made to work the mouse color change
+- theme changing is no longer auto-reboot upon change; it require now to press a button
+- cleaned the Preferences classes and added the signals and signal slots in those classes, removing them from the main app class
+- each FlatCAM object found in Preferences has it's own set of controls for changing the colors
+- added a set of gray icons to be used when the theme is complete dark (for now it is useful only for MacOS with dark theme because at the moment the app is not styled to dark UI except the plot area)
+
+25.12.2019
+
+- fixed an issue in old default file detection and in saving the factory defaults file
+- in Preferences window removed the Import/Export Preferences buttons because they are redundant with the entries in the File -> Menu -> Backup. and added a button to Restore Defaults
+- when in Basic mode the Tool type of the tool in the Geometry UI Tool Table after isolating a Gerber object is automatically selected as 'C1'
+- let the multiprocessing Pool have as many processes as needed
+- added a new Preferences setting allowing a custom mouse line width (to make it thicker or thinner)
+- changed the extension of the Tool Database file to FlatDB for easy recognition (in the future double clicking such a file might import the new tools in the FC database)
+
+24.12.2019
+
+- edited some icons so they don't contain white background
+- fixed an incorrect usage of object in the app.select_objects() method
+- fixed a typo in ToolDB.on_tool_add()
+
+23.12.2019
+
+- some fixes in the Legacy(2D) graphic mode regarding the possibility of changing the color of the Gerber objects
+- added a method to darken the outline color for Gerber objects when they have the color set
+- when Printing as PDF Gerber objects now the rendered color is the print color
+- speed up the plotting in OpenGL(3D) graphic mode
+- speed up the color setting for Gerber object when using the OpenGL(3D) graphic mode
+- setting color for Gerber objects work on a selection of Gerber objects
+- ~~when the selection is changed in the Project Tree the selection shape on canvas is deleted~~
+- if an object is selected on Project Tree and it does not have the selection shape drawn, first click on canvas over it will draw the selection shape 
+- in Tool Transform added a new feature named 'Buffer'. For Geometry and Gerber objects will create (and replace) a geometry at a distance from the original geometry and for Excellon will adjust the Tool diameters
+- solved issue #355 - when the tool diameter field in the Edit → Preferences → Geometry → Geometry General → Tools → Tool dia is only one the app failed to read it
+- solved issue #356 - in Tools DB can not be added more than one tool if a translation is active 
+- some changes related to the fact that the geometry default tool diameter value can be comma separated string of tool diameters
+
+22.12.2019
+
+- added a new option for the Gerber objects: on the project context menu now can be chosen a color for the selected Gerber object
+- fixed issue in Gerber UI where a label was not hidden when in Basic mode
+- added the color parameters of the objects to the serializable attributes
+- fixed Gerber object color set for Legacy(2D) graphic engine; glitch on the OpenGL(3D) graphic engine
+- fixed the above mentioned glitch in the OpenGL(3D) graphic engine when an Gerber object has been set with a color
+
+21.12.2019
+
+- fixed a typo in Distance Tool
+
+20.12.2019
+
+- fixed a rare issue in the generation of non-copper-region geometry started from the Gerber Object UI (selected tab)
+- Print function is now printing a PDF file for a selection of objects in the colors from canvas 
+- added an icon in the infobar that will show more clearly the status of the grid snapping
+- in Geometry Object UI (selected tab) when a tool type is changed from no matter what to V-shape, the cut_z value is saved and when the tool type is changed back to something different than V-shape, this saved cut-z value is restored
+- fixed re-cut length entry not staying disabled when the re-cut cb is not checked
+
+19.12.2019
+
+- in 2-Sided Tool added a way to calculate the bounding box values for a selection of objects, and also the centroid
+- in 2-Sided Tool fixed the Reset Tool button handler to reset the bounds value too; changed a string
+- added Preferences values for PDF margins when saving text in Code Editor as PDF
+- when clicking Cancel in Preferences now the values are reverted to what they used to be before opening Preferences tab and start changing values
+- starting to work to a general Print function; for now it will generate PDF files; currently it works only for one object not for a selection
+- added shortcut key Ctrl+P for printing to PDF method
+
+18.12.2019
+
+- added new parameters to improve Gerber parsing
+- small optimizations in the Preferences UI
+- the Jump To function reference is now saving it's last used value
+- added the ability to use the Jump To method in the Gerber Editor
+- improved the loading of Config File by using the advanced code editor
+- fixed a bug in the new feature 'extra buffering'
+- fixed the creation of CNCJob objects out of multigeo Geometry objects (objects with multiple tools)
+- optimized the NCC Tool
+
+17.12.2019
+
+- more optimizations in NCC Tool
+- optimizations in Paint Tool
+- maximum range for Cut Z is now zero to deal with the situation when using V-shape with tip-dia same value with cut width
+- modified QValidator in FCDoubleSpinner() GUI element to allow entering the minus sign when the range maximum is set as 0.0; also for positive numbers allowed entering the symbol plus
+- made sure that if in Gerber UI the isolation is made with a V-Shape tool then the tool type is automatically updated on the generated Geometry Object
+- added ability to save the Source File as PDF (still have to adjust the page size)
+- fixed the generate_from_geometry_2() method to use the default values in case the parameters are None
+- added ability to save the Source File as PDF - fixed page size and added line breaks
+- more mods to generate_from_geometry_2() method
+- fixed bug saving the FlatCAM project saying the file is used by another application
+- fixed issue #347 - a Gerber generated by Sprint Layout with copper pour ON will not have rendered the copper pour
+
+16.12.2019
+
+- in Geometry Editor added support for Jump To function such as that it works within the Editor Tools themselves. For now it works only in absolute jumps
+- modified the Jump To method such that now allows relative jump from the current mouse location
+- fixed the Defaults upgrade overwriting the new version number with the old one
+- fixed issue with clear_polygon3() - the one who makes 'lines' and fixed the NCC Tool
+- some small changes in the GeometryObject.on_tool_add() method
+- made sure that in Geometry Editor the self.app.mouse attribute is updated with the current mouse position (x, y)
+- updated the preprocessor files
+- fixed the HPGL preprocessor
+- fixed the CNCJob geometry created with HPGL preprocessor
+- fixed GCode generated with HPGL preprocessor to output only integer coordinates
+- fixed the HPGL2 import parsing for absolute linear movements
+- fixed the line endings for setup_ubuntu.sh
+
+15.12.2019
+
+- fixed a bug that created a crash in special conditions; it's related to the QSettings in FlatCAMGui.py
+- added a script to remove the bad profiles from resource pictures. From here: https://stackoverflow.com/questions/22745076/libpng-warning-iccp-known-incorrect-srgb-profile/43415650, link mentioned by @camellan (Andrey Kultyapov)
+- prepared the application for usage of dark icons in case of using the dark theme
+- updated the languages
+- fixed a typo
+- fixed layout on first launch of the app
+- fixed some issues with the recent preparation for dark icons resource usage
+- added a new preprocessor file contributed by Daniel Friderich and added fixes for it
+- modified the export_gcode() method and the preprocessors such that the preprocessors now have the information if to include the gcode header
+- updated all the translation PO files and the POT file
+- RELEASE 8.99
+
+14.12.2019
+
+- finished the strings update in the Google-translated Spanish
+- finished the strings update in the Google-translated French
+
+13.12.2019
+
+- HPGL2 import: added support for circles, arcs and 3-point arcs. Everything works only for absolute coordinates.
+- removed the .plt extension from Gcode extensions
+- some strings updated; update on the Romanian translate
+- more strings updated; finished the Romanian translation update
+- some work in updating the Spanish Google-translation
+- small updates (Google Translate) in Russian and Brazilian-PT languages
+
+12.12.2019
+
+- finished the Calibration Tool
+- changed the Scale Entry in Object UI to FCEntry() GUI element in order to allow expressions to be entered. E.g: 1/25.4
+- some small changes in the Scale button handler in FlatCAMObj() class
+- added option to save objects as PDF files in File -> Save menu
+- optimized the GerberObject.clear_plot_apertures() method
+- some changes in the ObjectUI and for the Geometry UI
+- finished a very rough and limited HPGL2 file import 
+
+11.12.2019
+
+- started work in HPGL2 parser
+- some more work in Calibration Tool
+
+10.12.2019
+
+- small changes in the Geometry UI
+- now extracut option in the Geometry Object will recut as many points as many they are within the specified re-cut length
+- if extracut_length is zero then the extracut will cut up until the first point in path no matter what the distance is
+- in Gerber isolation, when selection mode is checked, now area selection works too
+- in CNCJob UI, now the CNCJob objects made out of Excellon objects will display their CNC tools (drill bits)
+- fixed a cumulative error when using the Tool Offset for Excellon objects
+- added the display of the real depth of cut (cut z + offset_z) for CNC tools made out of an Excellon object
+- for OpenGL graphic mode added a fit_view() execution on canvas initialization
+- fixed Excellon scaling the UI values
+- replaced the SpindleSpeed entry with a FCSpinner() GUI element; if speed is set to 0 it will amount to None
+
+9.12.2019 
+
+- updated the border for fit view on OpenGL graphic mode
+- Calibration Tool - added preferences values
+- Calibration Tool - more work on it
+- reverted this change: "selected object in Project used to ask twice for UI build" because it will not build the UI when a tab is closed for Document object and the object is selected
+- fixed issue after Geometry object edit; the GCode made from an edited object did not reflect the changes in the object
+- in Object UI, the Scale FCDoubleSpinner will no longer work for Return key press due of issues of unwanted scaling on focusOut event
+- in GeometryObject fixed the scale and offset methods to always process the self.solid_geometry
+- Calibration Tool - finished the calibrated object creation method
+- updated the POT file
+- fixed an error in the German PO file
+- updated the languages PO files
+- some fixes on the app.jump_to() method
+- made sure that the ToolFilm will not start saving a file if there are no objects loaded
+- some fixes on the app.jump_to() method for the Legacy(2D) graphic mode
+
+8.12.2019
+
+- Calibrate Tool - rearranged the GUI
+- in Geometry UI made sure that the Label that points to the Tool parameters show clearly that those parameters apply only for the selected tool
+- fixed an small issue in Object UI
+- small fixes: selected object in Project used to ask twice for UI build; if scale factor in Object UI is 1 do nothing as there is no point in scaling with a factor of 1
+- in Geometry UI added a button that allow updating all the tools in the Tool Table with the current values in the UI form
+- updated Tcl commands to make use of either 0 or False for False value or 1 or True for True in case of a parameter with type Bool
+
+7.12.2019 
+
+- renamed Calibrate Excellon Tool to a simpler Calibrate Tool
+- Calibrate Tool - when generating verification GCode it will always load into an Editor from which it can be edited and/or saved. On save the editor will close.
+- updated the CNCJob and Drillcncjob Tcl Commands to use 0 and 1 as values for the parameters that are stated as of bool type, beside the normal keywords of False and True
+- Calibrate Tool - working on it
+
+6.12.2019
+
+- fixed the toggle_units() method so now the grid values are accurate to the decimal
+- cleaned up the Excellon parser and fixed some bugs (old and new); Excellon parser has it's own convert_units() method no longer inheriting from Geometry
+- in Excellon UI fixed bug that did not allow editing of the Offset Z parameter from the Tool table
+- in Properties Tool added new information's for the tools in the CNCjob objects
+- few bugs solved regarding the newly created empty objects
+- changed everywhere the name "preprocessor" with "preprocessor"
+- updated the preprocessor files in the toolchange section in order to avoid a graphical representation of travel lines glitch
+- fixed a GUI glitch in the Excellon tool table
+- added units to some of the parameters in the Properties Tool
+
+5.12.2019 
+
+- in NCC Tool, the new Geometry object that is created on copper clear now has the solid_geometry attribute where the geometry is stored not only in the obj.tools attribute
+- Copper Thieving Tool - added units label for the pattern plated area
+- Properties Tool - added a new parameter, the copper area which show the area of the copper features for the Gerber objects
+- Copper Thieving Tool - added a default value for the mask clearance when generating pattern plating mask
+- application wide change: introduced the precision parameters in Edit -> Preferences who will control how many decimals to use in the app parameters
+- changed the FCDoubleSpinner, FCSpinner and FCEntry GUI elements to allow passing an alignment value: left, right or center (not yet available in the app)
+- fixed the GUI of the Geometry Editor Tool Transform and adapted it to use the precision setting
+- updated Gerber Editor to use the precision setting and the Gerber Editor Transform Tool to use the FCDoubleSpinner GUI element
+- in Properties Tool added more information's regarding the Excellon tools, about travelled distance and job time; fixed issues when doing Properties on the CNCjob objects
+- TODO: I need to solve the mess in units conversion: it's too convoluted 
+
+4.12.2019 
+
+- made sure that if an older preferences file is detected then there are no errors and only the parameters that are currently active are loaded; the factory defaults file is deleted and recreated in the new format
+- in Preferences added a new button: 'Close' to close the Preferences window without saving
+- fixed bug in FCSpinner and FCDoubleSpinner GUI elements, that are now the main GUI element in FlatCAM, that made partial selection difficult
+- updated the Paint Tool in Geometry Editor to use the FCDoubleSpinner
+- added the possibility for suffix presence on the FCSpinner and FCDoubleSpinner GUI Elements
+- added the '%' symbol for overlap fields; I still need to divide the content by 100 to get the original (0 ... 1) value
+- fixed the overlap parameter all over the app to reflect the change to percentage
+- in Copper Thieving Tool added the display of the patterned plated area (approximate area) 
+- Copper Thieving Tool - updated the way plated area is calculated making it a bit more precise but still it is a bit bigger than the actual area
+- fixed the Copy Object function to copy also the source_file content
+- Copper Thieving Tool - when the clearance value for the pattern plating mask is negative it will be applied to the origin soldermask too
+- modified the GUI in all tools making the text of the buttons bold and adding a new button named Reset Tool which have to reset the tool GUI and variables (need to check all tools to see if happen)
+- when the Tool tab is in focus, clicking on canvas will no longer change the focus to Project tab
+- Copper Thieving Tool - when creating the pattern platting mask will make a new Gerber object with it
+- small fix in the GUI layout in Gerber Editor
+
+3.12.2019
+
+- in Preferences added an Apply button which apply the modified preferences but does not save to a file, minimizing the file IO operations; Ctrl+S key combo does the Apply now.
+- updated some of the default values to metric, values that were missed previously
+- remade the Gerber Editor way to import an Gerber object into the editor in such a way to use the multiprocessing
+- various small fixes
+- fix for toggle grid lines updating canvas only after moving the mouse (hack, actually)
+- some changes in the UI layout in Cutout Tool
+- added some geometry parameters in Cutout Tool as a convenience, to be passed to the generated Geometry objects
+
+2.12.2019
+
+- fixed issue #343; updated the Image Tool
+- improvements in Importing SVG as Gerber - added an automatic source generation (it is not infallible)
+- a hack to import correctly the QRCode exported as SVG from FlatCAM
+- added 3 new tcl commands: export dxf, export excellon and export gerber
+- added a Cancel button in Tools DB when requesting to add a tool in the Geometry Tool Table
+- modified the default values for the METRIC system; the app now starts in the METRIC units since the majority of the world use the METRIC units system
+- small changes, updated the estimated release date
+- Tool Copper Thieving - added pattern plating mask generation feature
+
+28.11.2019
+
+- small fixes in NCC Tool and in the GeometryObject class
+
+27.11.2019
+
+- in Tool Film added the page size and page orientation in case of saving the film as PDF file
+- the application workspace has now a lot more options selectable in the Edit -> Preferences -> General -> GUI Preferences
+- updated the drawing of the workspace such that the application overall start time is improved and after first turn on of the workspace, toggling it will have no performance penalty
+- updated the workspace functions to work in Legacy(2D) graphic mode
+- adjusted the selection color transparency for the Legacy(2D) graphic mode because it was too transparent for the fill
+
+26.11.2019
+
+- updated the Film Tool to allow exporting PDF and PNG file (besides the SVG file)
+
+25.11.2019
+
+- In Gerber isolation changed the UI
+- in Gerber isolation added the option to selectively isolate only certain polygons
+- made some optimizations in GerberObject.isolate() method
+- updated the 'single' isolation of Gerber polygons to remove the polygon if clicked on it and it is already in the list of single polygons to be isolated
+- clicking to add a polygon when doing Single type isolation will add a blue shape marking the selected polygon, second click will remove that shape
+- fixed bugs in Paint Tool when painting single polygon
+- in Gerber isolation added the option to selectively isolate only certain polygons - made it to work for Legacy(2D) graphic mode
+- remade the Paint Tool - single polygon painting; now it can single paint a list of polygons that are clicked onto (right click will start the actual painting)
+
+23.11.2019
+
+- in Tool Fiducials added a new fiducial type: chess pattern
+- work in Calibrate Excellon Tool
+- fixed the line numbers in the TextPlainEdit to fit all digits of the line number; activated the line numbers for ScriptObject objects too
+- line numbers in the TextPlainEdit for the selected line are bold
+- made sure that the self.defaults dictionary is deepcopy-ed in the self.options dictionary
+- made sure that the units are read from the self.defaults and not from the GUI
+- added Robber Bar option to Copper Thieving Tool
+
+22.11.2019
+
+- Tool Fiducials - added GUI in Preferences and entries in self.defaults dict
+- Tool Fiducials - updated the source_file object for the modified Gerber files
+- working on adding line numbers to the TextPlainEdit
+- GCode view now has line numbers
+- solved a bug that made selection of objects on canvas impossible if there is an object of type ScriptObject or DocumentObject opened
+
+21.11.2019
+
+- Tool Fiducials - finished the part with adding copper fiducials: manual and auto
+- Tool Fiducials - added choice of shapes: circular or non-standard cross
+- Tool Fiducials - finished the work on adding soldermask openings
+- Tool Fiducials - finished the tool
+- updated requirements.txt and setup_ubuntu.sh files
+
+20.11.2019
+
+- Tool Fiducials - added the GUI and the shortcut key
+- Tool Fiducials - updated the icon
+
+19.11.2019
+
+- removed the f-strings replacing them with the traditional string formatting due of not being supported by older versions of Python 3
+- fixed some TclCommands: MillDrills and OpenGerber
+- fixed bug in Tool Subtract that did not allow subtracting Gerber objects
+- starting to work on Tool Fiducials - created the file
+
+18.11.2019
+
+- finished the Dots and Squares options in the Copper Thieving Tool
+- working on the Lines option in Copper Thieving Tool
+- finished the Lines option in the Copper Thieving Tool; still have to add threading to maximize performance
+- finished Copper Thieving Tool improvements
+- working on the Calibrate Excellon Tool - remade the UI
+
+17.11.2019
+
+- optimized the storage of the Gerber mark shapes by making them one layer only
+- optimized the Distance Tool such that the distance utility geometry will be shown even when the mark shapes are plotted.
+- updated the make_freezed.py file to make sure that all the required files are included
+- updated the setup_ubuntu.sh to include the sudo command (courtesy of Krishna Torque on bitbucket)
+
+16.11.2019
+
+- fixed issue #341 that affected both preprocessors that have inlined feedrate: marlin and repetier. The used feedrate was the Feedrate X-Y and instead had to be Feedrate Z.
+
+15.11.2019
+
+- added all the recognized extensions to the save dialog filters; latest extension used will be preselected next time a save operation occur
+- fixed issue #335. The FCDoubleSPinBox (and FCSpinBox) value was not used when the user entered data but just hovered away the mouse expecting the data to be already confirmed
+- converted setup_ubuntu.sh to Linux line endings
+
+14.11.2019
+
+- made sure that the 'default' preprocessor file is always loaded first such that this name is always first in the GUI comboboxes
+- added a class in GUIElements for a TextEdit box with line numbers and highlight
+
+13.11.2019
+
+- trying to improve the performance of View CNC Code command by using QPlainTextEdit; made the mods for it
+- when using the Find function in the AppTextEditor and the result reach the bottom of the document, the next find will be the first in the document (before it defaulted to the beginning of the document)
+- finished improving the show of text files in FlatCAM (CNC Code, Source files)
+- fixed an issue in the FlatCAMObj.GerberObject.convert_units() which needed to be updated after changes elsewhere
+
+12.11.2019
+
+- added two new preprocessor files for ISEL CNC and for BERTA CNC
+- clicking on a FCTable GUI element empty space will also clear the focus now
+
+11.11.2019
+
+- in Tools Database added a contextual menu to add/copy/delete tool; Ctrl+C, DEL keys work too; key T for adding a tool is now only partially working
+- in Tools Database made the status bar messages show when adding/copying/deleting tools in DB
+- changed all Except statements that were single to except Exception as recommended in some PEP
+- renamed the Copper Fill Tool to Copper Thieving Tool as this is a more appropriate name; started to add ability for more types of copper thieving besides solid
+- fixed some issues recently introduced in ParseSVG
+- updated POT file
+- fixed GUI in 2Sided Tool
+- extending the Copper Thieving Tool - wip
+
+9.11.2019
+
+- fixed a new bug that did not allow to open the FlatCAM Preferences files by doubleclick in Windows
+- added a new feature: Tools Database for Geometry objects; resolved issue #308
+- added tooltips for the Tools Database table headers and buttons
+
+8.11.2019
+
+- updated the make file for frozen executable
+
+7.11.2019
+
+- added the '.ngc' file extension to the GCode Save file dialog filter
+- made the 'M2' Gcode command footer optional, default is False (can be set using the TclCommand: set_sys cncjob_footer True)
+- added a setting in Preferences to force the GCode output to have the Windows line-endings even for non-Windows OS's
+
+6.11.2019
+
+- the "CRTL+S" key combo when the Preferences Tab is in focus will save the Preferences instead of saving the Project
+- fixed bug in the Paint Tool that did not allow choosing a Paint Method that was not Standard
+- made sure that in the GeometryObject.merge() all the source data is deepcopy-ed in the final object
+- the font color of the Preferences tab will change to red if settings are not saved and it will revert to default when saved
+- fixed issue #333. The Geometry Editor Paint tool was not working and using it resulted in an error
+
+5.11.2019
+
+- added a new setting named 'Allow Machinist Unsafe Settings' that will allow the Travel Z and Cut Z to take both positive and negative values
+- fixed some issues when editing a multigeo geometry
+
+4.11.2019
+
+- wip
+- getting rid of all the Options GUI and related functions as it is no longer supported
+- updated the UI in Geometry UI
+- optimized the order of the defaults storage declaration and the update of the Preferences GUI from the defaults
+- started to add a Tool Database
+
+3.11.2019
+
+- fixed the V-shape tool diameter calculation in NCC Tool
+- in NCC Tool made the new tool dia (circular type) a parameter in Preferences
+- fixed a small issue with clicking in a disabled FCDoubleSpinner or FCSpinner still doing a selection
+
+30.10.2019
+
+- converted SolderPaste Tool to usage of SpinBoxes; changed the SolderPaste Tool UI in Preferences too
+- fixed a bug in SolderPaste Tool that did not allow to view the GCode
+
+29.10.2019
+
+- a bug fix in Geometry Object
+- fixed some missing properties in Tool Calculators
+
+28.10.2019
+
+- in Tools: Paint, NCC and Copper Fill, when using the Area Selection, now the selected areas will stay drawn as markers until the user click RMB
+- in legacy2D graphic engine, adding an utility geometry no longer draw the older ones, overwriting them
+- fixed some issues in the Gerber Editor (Aperture add was double adding an aperture)
+- converted Gerber Editor to usage of SpinBoxes
+- working on the Calibrate Excellon Tool
+- converted Excellon Editor to usage of SpinBoxes
+- Calibrate Excellon Tool: working on self.calculate_factors() method
+
+27.10.2019
+
+- Copper Fill Tool: some PEP8 corrections
+
+26.10.2019
+
+- fixed an error in the FCDoubleSpinner class when FlatCAM is run on system with locale that use the comma as decimal separator
+
+25.10.2019
+
+- QRCode Tool: added ability to add negative QRCodes (perhaps they can be isolated on copper?); added a clear area surrounding the QRCode in case it is dropped on a copper pour (region); fixed the Gerber export
+- QRCode Tool: all parameters are hard-coded for now
+- small update
+- fixed imports in all TclCommands
+- fixed the requirements.txt and setup_ubuntu.sh files
+- QRCode Tool: change the plot method parameter
+- QRCode Tool: added ability to save the generated QRCode as SVG file or PNG file
+- QRCode Tool: added the feature to save the PNG file with transparent background
+- QRCode Tool: added GUI category in Preferences window
+- QRCode Tool: shortcut key for this tool is now Alt+Q while PDF import Tool was relegated to Ctrl+Q combo key shortcut
+- added a new FlatCAM Tool: Copper Fill Tool. It will pour copper into a Gerber filling all empty space with copper, at a clearance distance of the Gerber features
+- Copper Fill Tool: added possibility to select between a bounding box rectangular or convex hull when the reference is the geometry of the source Gerber object
+- Copper Fill Tool: cleanup on not regular tool exit
+- Copper Fill Tool: added GUI category in Edit -> Preferences window
+- QRCode Tool: added a selection limit parameter to control the selection shape vs utility geo
+
+24.10.2019
+
+- added some placeholder texts in the TextBoxes.
+- working on QRCode Tool; added the utility geometry and initial functional layout
+- working on QRCode Tool; finished adding the QRCode geometry to the selected Gerber object and also finished adding the 'follow' geometry needed when exporting the Gerber object as a Gerber file in addition to the 'solid' geometry in the obj.apertures
+- working on QRCode Tool; finished offsetting the geometry both in apertures and in solid_geometry; updated the source_file of the source object
+
+23.10.2019
+
+- QRCode Tool - a SVG object is generated and plotted on screen having the QRCode data
+- fixed an import error in Distance Tool
+- fixed the Toggle Grid Lines functionality
+
+22.10.2019
+
+- working on the Calibrate Excellon Tool
+- finished the GUI layout for the Calibrate Excellon Tool
+- start working on QRCode Tool - not working yet
+- start working on QRCode Tool - searching for alternatives
+
+21.10.2019
+
+- the context menu for the Tabs in notebook and PlotTabArea is launched now on right mouse click on tabs themselves
+- fixed an error when trying to view the source file and there is no object selected
+- updated the Objects menu signals so whenever an object is (de)selected in the Project Tab, it's state will reflect the (un)checked state of the actions in the Object menu
+- fixed issue in Gerber Object UI of not updating the value of CutZ entry on TipDia or TipAngle entries change. Fixed issue #324
+
+18.10.2019
+
+- fixed a small bug in BETA status change
+- updated the About FlatCAM window
+- reverted change in tool dia being able to take only positive values in Gerber Object UI
+- started to work to a new tool: Calibrate Excellon Tool
+- solved the issue #329
+
+18.10.2019
+
+- finished the update on the Google translated Spanish translation.
+- updated the new objects icons for Gerber, Geometry and Excellon
+- small import problem fixed
+- RELEASE 8.98
+
+17.10.2019
+
+- fixed a bug in milling holes due of a message wrongly formatted
+- added an translator email address
+- finished the update on German Google translation. Part of it was corrected by Jens Karstedt
+- finished the update of the Romanian translation.
+- finished the Objects menu by adding the ability of actions to be checked so they will show the selected status of the objects and by adding to actions to (de)select all objects
+- fixed and optimized the click selection on canvas
+- fixed Gerber parsing for very simple Gerber files that have only one Polygon but many LPC zones
+- fixed SVG export; fix bug #327
+- finished the update on French Google translation.
+
+16.10.2019
+
+- small update to Romanian translation files
+
+15.10.2019
+
+- adjusted the layout in NCC Tool
+- fixed bug in Panelization Tool for which in case of Excellon objects, the panel kept a reference to the source object which created issues when moving or disabling/enabling the plots
+- cleaned up the module imports throughout the app (the TclCommands are not yet verified)
+- removed the styling on the comboboxes cellWidget's in the Tool Tables
+- replaced some of the icons that did not looked Ok on the dark theme
+- added a new toolbar button for the Copy object functionality
+- changed the Panelize tool icon
+- corrected some strings
+
+14.10.2019
+
+- modified the result highlight color in Check Rules Tool
+- added the Check Rules Tool parameters to the unit conversion list
+- converted more of the Preferences entries to FCDoubleSpinner and FCSpinner
+- converted all ObjectUI entries to FCDoubleSpinner and FCSpinner
+- updated the translation files (~ 89% translation level)
+- changed the splash screen as it seems that FlatCAM beta will never be more than beta
+- changed some of the signals from returnPressed to editingFinished due of now using the SpinBoxes
+- fixed an issue that caused the impossibility to load a GCode file that contained the % symbol even when was loaded in a regular way from the File menu
+- re-added the CNC tool diameter entry for the CNCjob object in Selected tab.FCSpinner
+- since the CNCjob geometry creation is only useful for graphical purposes and have no impact on the GCode creation I have removed the cascaded union on the GCode geometry therefore speeding up the Gcode display by many factors (perhaps hundreds of times faster)
+- added a secondary link in the bookmark manager
+- fixed the bookmark manager order of bookmark links; first two links are always protected from deletion or drag-and-drop to other positions
+- fixed a whole load of PyQT signal problems generated by recent changes to the usage of SpinBoxes; added a signal returnPressed for the FCSpinner and for FCDoubleSpinner
+- fixed issue in Paint Tool where the first added tool was expected to have a float diameter but it was a string
+- updated the translation files to the latest state in the app
+
+13.10.2019
+
+- fixed a bug in the Merge functions
+- fixed the Export PNG function when using the 2D legacy graphic engine
+- added a new capability to toggle the grid lines for both graphic engines: menu link in View and key shortcut combo Alt+G
+- changed the grid colors for 3D graphic engine when in Dark mode
+- enhanced the Tool Film adding the Film adjustments and added the GUI in Preferences
+- set the GUI layout in Preferences for a new category named Tools 2
+- added the Preferences for Check Rules Tool and for Optimal Tool and also updated the Film Tool to use the default settings in Preferences
+
+12.10.2019
+
+- fixed the Gerber Parser convert units unnecessary usage. The only units conversion should be done when creating the new object, after the parsing
+- more fixes in Rules Check Tool
+- optimized the Move Tool
+- added support for key-based panning in 3D graphic engine. Moving the mouse wheel while pressing the CTRL key will pan up-down and while pressing SHIFT key will pan left-right
+- fixed a bug in NCC Tool and start trying to make the App responsive while the NCC tool is run in a non-threaded way
+- fixed a GUI bug with the QMenuBar recently introduced
+
+11.10.2019
+
+- added a Bookmark Manager and a Bookmark menu in the Help Menu
+- added an initial support for rows drag and drop in FCTable in GUIElements; it crashes for CellWidgets for now, if CellWidgetsare in the table rows
+- fixed some issues in the Bookmark Manager
+- modified the Bookmark manager to be installed as a widget tab in Plot Area; fixed the drag & drop function for the table rows that have CellWidgets inside
+- marked in gray color the rows in the Bookmark Manager table that will populate the BookMark menu
+- made sure that only one instance of the BookmarkManager class is active at one time
+
+10.10.2019
+
+- fixed Tool Move to work only for objects that are selected but also plotted, therefore disabled objects will not be moved even if selected
+
+9.10.2019
+
+- updated the Rules Check Tool - solved some issues
+- made FCDoubleSpinner to use either comma or dot as a decimal separator
+- fixed the FCDoubleSpinner to only allow the amount of decimals already set with set_precision()
+- fixed ToolPanelize to use FCDoubleSpinner in some places
+
+8.10.2019
+
+- modified the FCSpinner and FCDoubleSpinner GUI elements such that the wheel event will not change the values inside unless there is a focus in the lineedit of the SpinBox
+- in Preferences General, Gerber, Geometry, Excellon, CNCJob sections made all the input fields of type SpinBox (where possible)
+- updated the Distance Tool utility geometry color to adapt to the dark theme canvas
+- Toggle Code Editor now works as expected even when the user is closing the Editor tab and not using the command Toggle Code Editor
+- more changes in Preferences GUI, replacing the FCEntries with Spinners
+- some small fixes in toggle units conversion
+- small GUI changes
+
+7.10.2019
+
+- fixed an conflict in a signal usage that was triggered by Tool SolderPaste when a new project was created
+- updated Optimal Tool to display both points coordinates that made a distance (and the minimum) not only the middle point (which is still the place where the jump happen)
+- added a dark theme to FlatCAM (only for canvas). The selection is done in Edit -> Preferences -> General -> GUI Settings
+- updated the .POT file and worked a bit in the romanian translation
+- small changes: reduced the thickness of the axis in 3D mode from 3 pixels to 1 pixel
+- made sure that is the text in the source file of a DocumentObject is HTML is loaded as such
+- added inverted icons
+
+6.10.2019
+
+- remade the Mark area Tool in Gerber Editor to be able to clear the markings and also to delete the marked polygons (Gerber apertures)
+- working in adding to the Optimal Tool the rest of the distances found in the Gerber and the locations associated; added GUI
+- added display of the results for the Rules Check Tool in a formatted way
+- made the Rules Check Tool document window Read Only
+- made Excellon and Gerber classes from camlib into their own files in the flatcamParser folder
+- moved the ApertureMacro class from camlib to ParseGerber file
+- moved back the ApertureMacro class to camlib for now and made some import changes in the new ParseGerber and ParseExcellon classes
+- some changes to the tests - perhaps I will try adding a few tests in the future
+- changed the Jump To icon and reverted some changes to the parseGerber and ParseExcellon classes
+- updated Tool Optimal with display of all distances (and locations of the middle point between where they happen) found in the Gerber Object
+
+5.10.2019
+
+- remade the Tool Calculators to use the QSpinBox in order to simplify the user interaction and remove possible errors
+- remade: Tool Cutout, Tool 2Sided, Tool Image, Panelize Tool, NCC Tool, Paint Tool  to use the QSpinBox GUI elements
+- optimized the Transformation Tool both in GUI and in functionality and replaced the entries with QSpinBox
+- fixed an issue with the tool table context menu in Paint Tool
+- made some changes in the GUI in Paint Tool, NCC Tool and SolderPaste Tool
+- changed some of the icons; added attributions for icons source in the About FlatCAM window
+- added a new tool in the Geometry Editor named Explode which is the opposite of Union Tool: it will explode the polygons into lines
+
+4.10.2019
+
+- updated the Film Tool and added the ability to generate Punched Positive films (holes in the pads) when a Gerber file is the film's source. The punch holes source can be either an Excellon file or the pads center
+- optimized Rules Check Tool so it runs faster when doing Copper 2 Copper rule
+- small GUI changes in Optimal Tool and in Film Tool
+- some PEP8 corrections
+- some code annotations to make it easier to navigate in the MainGUI.py
+- fixed exit FullScreen with Escape key
+- added a new menu category in the MenuBar named 'Objects'. It will hold the objects found in the Project tab. Useful when working in FullScreen
+- disabled a log.debug in ObjectColection.get_by_name()
+- added a Toggle Notebook button named 'NB' in the QMenBar which toggle the notebook
+- in Gerber isolation section, the tool dia value is updated when changing from Circular to V-shape and reverse
+- in Tool Film, when punching holes in a positive film, if the resulting object geometry is the same as the source object geometry, the film will not ge generated
+- fixed a bug that when a Gerber object is edited and it has as solid_geometry a single Polygon, saving the result was failing due of len() function not working on a single Polygon
+- added the Distance Tool, Distance Min Tool, Jump To and Set Origin functions to the Edit Toolbar
+
+3.10.2019
+
+- previously I've added the initial layout for the DocumentObject object
+- added more editing features in the Selected Tab for the DocumentObject object
+
+2.10.2019
+
+- fixed bug in Geometry Editor that did not allow the copy of geometric elements
+- created a new class that holds all the Code Editor functionality and integrated as a Editor in FlatCAM, the location is in flatcamEditors folder
+- remade all the functions for view_source, scripts and view_code to use the new AppTextEditor class; now all the Code Editor tabs are being kept alive, before only one could be in an open state
+- changed the name of the new object FlatCAMNotes to a more general one DocumentObject
+- changed the way a new ScriptObject object is made, the method that is processing the Tcl commands when the Run button is clicked is moved to the FlatCAMObj.ScriptObject() class
+- reused the Multiprocessing Pool declared in the App for the ToolRulesCheck() class
+- adapted the Project context menu for the new types of FLatCAM objects
+- modified the setup_recent_files to accommodate the new FlatCAM objects
+- made sure that when an ScriptObject object is deleted, it's associated Tab is closed
+- fixed the FlatCMAScript object saving when project is saved (loading a project with this script object is not working yet)
+- fixed the FlatCMAScript object when loading it from a project
+
+1.10.2019
+
+- fixed the FCSpinner and FCDoubleSpinner GUI elements to select all on first click and deselect on second click in the Spinbox LineEdit
+- for Gerber object in Selected Tab added ability to chose a V-Shape tool and therefore control the isolation better by adjusting the cut width of the isolation in function of the cut depth, tip width of the tool and the tip angle of the tool
+- when in Gerber UI is selected the V-Shape tool, all those parameters (tip dia, tip angle, tool_type = 'V' and cut Z) are transferred to the generated Geometry and prefilled in the Geoemtry UI
+- added a fix in the Gerber parser to work even when there is no information about zero suppression in the Gerber file
+- added new settings in Edit -> Preferences -> Gerber for Gerber Units and Gerber Zeros to be used as defaults in case that those informations are missing from the Gerber file
+- added new settings for the Gerber newly introduced feature to isolate with the V-Shape tools (tip dia, tip angle, tool_type and cut Z) in Edit -> Preferences -> Gerber Advanced
+- made those settings just added for Gerber, to be updated on object creation
+- added the Geo Tolerance parameter to those that are converted from MM to INCH
+- added two new FlatCAM objects: ScriptObject and FlatCAMNotes
+
+30.09.2019
+
+- modified the Distance Tool such that the number of decimals all over the tool is set in one place by the self.decimals
+- added a new tool named Minimum Distance Tool who will calculate the minimum distance between two objects; key shortcut: SHIFT + M
+- finished the Minimum Distance Tool in case of using it at the object level (not in Editors)
+- completed the Minimum Distance Tool by adding the usage in Editors
+- made the Minimum Distance Tool more precise for the Excellon Editor since in the Excellon Editor the holes shape are represented as a cross line but in reality they should be evaluated as circles
+- small change in the UI layout for Check Rules Tool by adding a new rule (Check trace size)
+- changed a tooltip in Optimal Tool
+- in Optimal Tool added display of how frequent that minimum distance is found
+- in Tool Distance and Tool Minimal Distance made the entry fields read-only
+- in Optimal Tool added the display of the locations where the minimum distance was detected
+- added support to use Multi Processing (multi core usage, not simple threading) in Rules Check Tool
+- in Rules Check Tool added the functionality for the following rules: Hole Size, Trace Size, Hole to Hole Clearance
+- in Rules Check Tool added the functionality for Copper to Copper Clearance
+- in Rules Check Tool added the functionality for Copper to Outline Clearance, Silk to Silk Clearance, Silk to Solder Mask Clearance, Silk to Outline Clearance, Minimum Solder Mask Sliver, Minimum Annular Ring
+- fixes to cover all possible situations for the Minimum Annular Ring Rule in Rules Check Tool
+- some fixes in Rules Check Tool and added a QSignal that is fired at the end of the job
+
+29.09.2019
+
+- work done for the GUI layout of the Rule Check Tool
+- setup signals in the Rules Check Tool GUI
+- changed the name of the Measurement Tool to Distance Tool. Moved it's location to the Edit Menu
+- added Angle parameter which is continuously updated to the Distance Tool
+
+28.09.2019
+
+- changed the icon for Open Script and reused it for the Check Rules Tool
+- added a new tool named "Optimal Tool" which will determine the minimum distance between the copper features for a Gerber object, in fact determining the maximum diameter for a isolation tool that can be used for a complete isolation
+- fixed the ToolMeasurement geometry not being displayed
+- fixed a bug in Excellon Editor that crashed the app when editing the first tool added automatically into a new black Excellon file
+- made sure that if the big mouse cursor is selected, the utility geometry in Excellon Editor has a thicker line width (2 pixels now) so it is visible over the geometry of the mouse cursor
+- fixed issue #319 where generating a CNCJob from a geometry made with NCC Tool made the app crash; also #328 which is the same
+- replaced in FlatCAM Tools and in FLatCAMObj.py  and in Editors all references to hardcoded decimals in string formats for tools with a variable declared in the __init__()
+- fixed a small bug that made app crash when the splash screen is disabled: it was trying to close it without being open
+
+27.09.2019
+
+- optimized the toggle axis command
+- added possibility of using a big mouse cursor or a small mouse cursor. The big mouse cursor is made from 2 infinite lines. This was implemented for both graphic engines
+- added ability to change the cursor size when the small mouse cursor is selected in Preferences -> General
+- removed the line that remove the spaces from the path parameter in the Tcl commands that open something (Gerber, Gcode, Excellon)
+- fixed issue with the old SysTray icon not hidden when the application is restarted programmatically
+- if an object is edited but the result is not saved, the app will reload the edited object UI and set the Selected tab as active
+- made the mouse cursor (big, small) change in real time for both graphic engines
+- started to work on a new FlatCAM tool: Rules Check
+- created the GUI for the Rule Check Tool
+- if there are (x, y) coordinates in the clipboard, when launching the "Jump to" function, those coordinates will be preloaded in the Dialog box.
+- when the combo SHIFT + LMB is executed there is no longer a deselection of objects
+- when the "Jump to" function is called, the mouse cursor (if active) will be moved to the new position and the screen position labels will be updated accordingly
+
+
+27.09.2019
+
+- RELEASE FlatCAM 8.97
+
+26.09.2019
+
+- added a Copy All button in the Code Editor, clicking this button will copy all text in the editor to the clipboard
+- added a 'Milling Type' radio button in Geometry Editor Preferences to contorl the type of geometry will be generated in the Geo Editor (for conventional milling or for the climb milling)
+- added the functionality to allow climb/conventional milling selection for the geometry created in the Geometry Editor
+- now any Geometry that is edited in Geometry editor will have coordinates ordered such that the resulting Gcode will allow the selected milling type in the 'Milling Type' radio button in Geometry Editor Preferences (which depends also of the spindle direction)
+- some strings update
+- French Google-translation at 100%
+- German Google-translation update to 100%
+- updated the other languages and the .POT file
+- changed some strings (that should not have been included for translation) and updated language files and the .POT file
+- fixed issue when rebooting from within in cx_freezed state (it issued a startup arg with the path to FlatCAM.exe but that triggered the last sys.exit(2) that I had in the App.args_at_startup())
+- modified the make_win script for the presence of MatPlotLib
+
+25.09.2019
+
+- French translation at 33%
+- fixed the 'Jump To' function to work in legacy graphic engine
+- in legacy graphic engine fixed the mouse cursor shape when grid snapping is ON, such that it fits with the shape from the OpenGL graphic engine
+- in legacy graphic engine fixed the axis toggle
+- French Google-translation at 48%
+
+24.09.2019
+
+- fixed the fullscreen method to show the application window in fullscreen wherever the mouse pointer it is therefore on the screen we are working on; before it was showing always on the primary screen
+- fixed setup_ubuntu.sh to include the matplotlib package required by the Legacy (2D) graphic engine
+- in legacy graphic engine, fixed issue where immediately after changing the mouse cursor snapping the mouse cursor shape was not updated
+- in legacy graphic engine, fixed issue where while zooming the mouse cursor shape was not updated
+- in legacy graphic engine, fixed issue where immediately after panning finished the mouse cursor shape was not updated
+- unfortunately the fix for issue where while zooming the mouse cursor shape was not updated braked something in way that Matplotlib work with PyQt5, therefore I removed it
+- fixed a bug in legacy graphic engine: when doing the self.app.collection.delete_all() in new_project an app crash occurred
+- implemented the Annotation change in CNCJob Selected Tab for the legacy graphic engine
+
+23.09.2019
+
+- in legacy graphic engine, fixed bug that made the old object disappear when a new object was loaded
+- in legacy graphic engine, fixed bug that crashed the app when creating a new project
+- in legacy graphic engine, fixed a bug that when deleting an object all objects where deleted
+- added a new TclCommand named "set_origin" which will set the origin for all loaded objects to zero if the -auto True argument is used and to a certain x,y location if the format is: set_origin 5,7
+- added a new TclCommand named "bounds" which will return a list of bounds values from a supplied list of objects names. For use in Tcl Scripts
+- updated strings in the translations and the .POT file
+- added the new keywords to the default keywords list
+- fixed the FullScreen option not working for the 3D graphic engine (due bug of Qt5 when OpenGL window is fullscreen) by creating a sort of fullscreen
+- added a final fix that allow full coverage of the screen in FullScreen in Windows and still the menus are working
+- optimized the Gerber mark shapes display
+- fixed a color format bug in Tool Move for 3D engine
+- made sure that when the Tool Move is used on a Gerber file with mark shapes active, those mark shapes are deleted before the actual move
+- in legacy graphic engine, fixed issue with Delete shortcut key trying to delete twice
+- 26% in Google-translated French translation and updated some strings too
+
+22.09.2019
+
+- fixed zoom directions legacy graphic engine (previous commit)
+- fixed display of MultiGeo geometries in legacy graphic engine
+- fixed Paint tool to work in legacy graphic engine
+- fixed CutOut Tool to work in legacy graphic engine
+- fixed display of distance labels and code optimizations in ToolPaint and NCC Tool
+- adjusted axis at startup for legacy graphic engine plotcanvas
+- when the graphic engine is changed in Edit -> Preferences -> General -> App Preferences, the application will restart
+- made hover shapes work in legacy graphic engine
+- fixed bug in display of the apertures marked in the Aperture table found in the Gerber Selected tab and through this made it to also work with the legacy graphic engine
+- fixed annotation in Mark Area Tool in Gerber Editor to work in legacy graphic engine
+- fixed the MultiColor plot option Gerber selected tab to work in legacy graphic engine
+- documented some methods in the ShapeCollectionLegacy class
+- updated the files: setup_ubuntu.sh and requirements.txt
+- some strings changed to be easier for translation
+- updated the .POT file and the translation files
+- updated and corrected the Romanian and Spanish translations
+- updated the .PO files for the rest of the translations, they need to be filled in.
+- fixed crash when trying to set a workspace in FlatCAM in the Legacy engine 2D mode by disabling this function for the case of 2D mode
+- fixed exception when trying to Fit View (shortcut key 'V') with no object loaded, in legacy graphic engine
+
+21.09.2019
+
+- fixed Measuring Tool in legacy graphic engine
+- fixed Gerber plotting in legacy graphic engine
+- fixed Geometry plotting in legacy graphic engine
+- fixed CNCJob and Excellon plotting in legacy graphic engine
+- in legacy graphic engine fixed the travel vs cut lines in CNCJob objects
+- final fix for key shortcuts with modifier in legacy graphic engine
+- refactored some of the code in the legacy graphic engine
+- fixed drawing of selection box when dragging mouse on screen and the selection shape drawing on the selected objects
+- fixed the moving drawing shape in Tool Move in legacy graphic engine
+- fixed moving geometry in Tool Measurement in legacy graphic engine
+- fixed Geometry Editor to work in legacy graphic engine
+- fixed Excellon Editor to work in legacy graphic engine
+- fixed Gerber Editor to work in legacy graphic engine
+- fixed NCC tool to work in legacy graphic engine
+
+20.09.2019
+
+- final fix for the --shellvar having spaces within the assigned value; now they are retained
+- legacy graphic engine - made the mouse events work (click, release, doubleclick, dragging)
+- legacy graphic engine - made the key events work (simple or with modifiers)
+- legacy graphic engine - made the mouse cursor work (enabled/disabled, position report); snapping is not moving the cursor yet
+- made the mouse cursor snap to the grid when grid snapping is active
+- changed the axis color to the one used in the OpenGL graphic engine
+- work on ShapeCollectionLegacy
+- fixed mouse cursor to work for all objects
+- fixed event signals to work in both graphic engines: 2D and 3D
+
+19.09.2019
+
+- made sure that if FlatCAM is registered with a file extension that it does not recognize it will exit
+- added some fixes in the the file extension detection
+- added some status messages for the Tcl script related methods
+- made sure that optionally, when a script is run then it is also loaded into the code editor
+- added control over the display of Sys Tray Icon in Edit -> Preferences -> General -> GUI Settings -> Sys Tray Icon checkbox
+- updated some of the default values to more reasonable ones
+- FlatCAM can be run in HEADLESS mode now. This mode can be selected by using the --headless=1 command line argument or by changing the line headless=False to True in config/configuration.txt file. In this mod the Sys Tray Icon menu will hold only the Run Scrip menu entry and Exit entry.
+- added a new TclCommand named quit_flatcam which will ... quit FlatCAM from Tcl Shell or from a script
+- fixed the command line argument --shellvar to work when there are spaces in the argument value
+- fixed bug in Gerber editor that did not allow to display all shapes after it encountered one shape without 'solid' geometry
+- fixed bug in Gerber Editor -> selection area handler where if some of the selected shapes did not had the 'solid' geometry will silently abort selection of further shapes
+- added new control in Edit -> Preferences -> General -> Gui Preferences -> Activity Icon. Will select a GIF from a selection, the one used to show that FlatCAM is working.
+- changed the script icon to a smaller one in the sys tray menu
+- fixed bug with losing the visibility of toolbars if at first startup the user tries to change something in the Preferences before doing a first save of Preferences
+- changed a bit the splash PNG file
+- moved all the GUI Preferences classes into it's own file flatcamGUI.PreferencesUI.py
+- changed the default method for Paint Tool to 'all'
+
+18.09.2019
+
+- added more functionality to the Extension registration with FLatCAM and added to the GUI in Edit -> Preferences -> Utilities
+- fixed the parsing of the Manufacturing files when double clicking them and they are registered with FlatCAM
+- fixed showing the GUI when some settings (maximized_GUI) are missing from QSettings
+- added sys tray menu
+- added possibility to edit the custom keywords used by the autocompleter (in Tcl Shell and in the Code Editor). It is done in the Edit -> Preferences -> Utilities
+- added a new setting in Edit -> Preferences -> General -> GUI Settings -> Textbox Font which control the font on the Textbox GUI elements
+- fixed issue with the sys tray icon not hiding after application close
+- added option to run a script from the context menu of the sys tray icon. Changed the color of the sys tray icon to a green one so it will be visible on light and dark themes
+
+17.09.2019
+
+- added more programmers that contributed to FlatCAM over the years, in the "About FlatCAM" -> Programmers window
+- fixed issue #315 where a script run with the --shellfile argument crashed the program if it contained a TclCommand New
+- added messages in the Splash Screen when running FlatCAM with arguments at startup
+- fixed issue #313 where TclCommand drillcncjob is spitting errors in Tcl Shell which should be ignored
+- fixed an bug where the pywrapcp name from Google OR-Tools is not defined; fix issue #316
+- if FlatCAM is started with the 'quit' or 'exit' as argument it will close immediately and it will close also another instance of FlatCAM that may be running
+- added a new command line parameter for FlatCAM named '--shellvars' which can load a text file with variables for Tcl Shell in the format: one variable assignment per line and looking like: 'a=3' without quotes
+- made --shellvars into --shellvar and make it only one list of commands passed to the Tcl. The list is separated by comma but without spaces. The variables are accessed in Tcl with the names shellvar_x where x is the index in the list of command comma separated values
+- fixed an issue in the TclShell that generated an exception IndexError which crashed the software
+- fixed the --shellvar and --shellfile FlatCAM arguments to work together but the --shellvar has precedence over --shellfile as it is most likely that whatever variable set by --shellvar will be used in the script file run by --shellfile
+
+16.09.2019
+
+- modified the TclCommand New so it will no longer close all tabs when called (it closed the Code Editor tab which may have been holding the code that run)
+- fixed the App.on_view_source() method for CNCJob objects: the Gcode will now contain the Prepend and Append code from the Edit -> Preferences -> CNCJob -> CNCJob Options
+- added a new parameter named 'muted' for the TclCommands: cncjob, drillcncjob and write_gcode. Setting it as -muted 1 will disable the error reporting in TCL Shell
+- some GUI optimizations
+- more GUI optimizations related to being part of the Advanced category or not
+- added possibility to change the positive SVG exported file color in Tool Film
+- fixed some issues recently introduced in the TclCommands CNCJob, DrillCNCJob and write_gcode; changed some parameters names
+- fixed issue in the Laser preprocessor where the laser was turned on as soon as the GCode started creating an unwanted cut up until the job start
+- added new links in Menu -> Help (Excellon, Gerber specifications and a Report Bug)
+- made the splashscreen to be showed on the current monitor on systems with multiple monitors
+- added a new entry in Menu -> View -> Redraw All which is doing what the name says: redraw all loaded objects
+- fixed issue where in TCl Shell the Windows paths were not understood due of backslash symbol understood as escape symbol instead of path separator
+- made sure that in for the TclCommand cncjob and for the drillcncjob if one of the args is stated but no value then the value used will be the default one
+- made available the TSA algorithm for drill path optimization when the used OS is 64bit. When used OS is 32bit the only available algorithm is TSA
+
+15.09.2019
+
+- refactored GeometryObject.mtool_gen_cncjob() method
+- fixed the TclCommandCncjob to work for multigeometry Geometry objects; still I had to fix the list of tools parameter, right now I am setting it to an empty list
+- update the Tcl Command isolate to be able to isolate exteriors, interiors besides the full isolation, using the iso_type parameter
+- fixed issue in ToolPaint that could not allow area painting of a geometry that was a list and not a Geometric element (polygon or MultiPolygon)
+- fixed UI showing before the initialization of FlatCAM is finished when the last state of GUI was maximized
+- finished updating the TclCommand cncjob to work for multi-geo Geometry objects with the parameters from the args
+- fixed the TclCommand cncjob to use the -outname parameter
+- added some more keywords in the data_model for auto-completer
+- fixed isolate TclCommand to use correctly the -outname parameter
+- added possibility to see the GCode when right clicking on the Project tab on a CNCJob object and then clicking View Source
+- added a new TclCommand named PlotObjects which will plot a list of FlatCAM objects
+- made that after opening an object in FlatCAM it is not automatically plotted. If the user wants to plot it can use the TclCommands PlotAll or PlotObjects
+- modified the TclCommands so that open files do not plot the opened files automatically
+- made all TclCommands not to be plotted automatically
+- made sure that all TclCommands are not threaded
+- added new TclCommands: NewExcellon, NewGerber
+- fixed the TclCommand open_project
+- added the outname parameter (and established an default name when outname not used) for the AlignDrillGrid and AlignDrill TclCommands
+- fixed Scripts repeating multiple time when the Code Editor is used. This repetition was correlated with multiple openings of the Code Editor window (especially after an error)
+- added the autocomplete keywords that can be changed to the defaults dictionary
+
+14.09.2019
+
+- more string changes
+- updated translation files
+- fixed a small bug
+- minor changes in the Code Editor GUI
+- minor changes in the 'FlatCAM About' GUI
+- added a new shortcut key F5 for doing the 'Plot All'
+- updated the google-translated Spanish translation strings
+- fixed the layouts to include toolbars breaks where it was needed
+- whenever the user changes the Excellon format values for loading files, the Export Excellon Format values will be updated
+- made optional the behavior of Excellon Export values following the values in the Excellon Loading section
+- updated the translations (except RU) and the POT file
+- added to the NonCopperClear.clear_copper() a parameter to be able to run it non-threaded
+
+13.09.2019
+
+- added control for simplification when loading a Gerber file in Preferences -> Gerber -> Gerber General -> Simplify
+- added some messages for the Edit -> Conversions -> Join methods() to make sure that there are at least 2 objects selected for join
+- added a grid layout in on_about()
+- upgraded the Script Editor to be able to run Tcl commands in batches
+- added some ToolTips for the buttons in the Code Editor
+- converted the big strings that hold the shortcut keys descriptions to smaller string to make translations easier
+- fixed some of the strings that were left in the old way
+- updated the POT file
+- updated Romanian language partially
+- added a new way to handle scripts with repeating Tcl commands
+- added new buttons in the Tools toolbar for running, opening and adding new scripts
+- finished the Romanian translation update and updated the POT file
+
+12.09.2019
+
+- small changes in the TclCommands: MillDrills, MillSlots, DrillCNCJob: the new parameter for tolerance is now named: diatol
+- cleaned up the 'About FlatCAM' window, started to give credits for the translation team
+- started to add an application splash screen
+- now, Excellon and Gerber edited objects will have the source_code updated and ready to be saved
+- the edited Gerber (or Excellon) object now is kept in the app after editing and the edited object is a new object
+- added a message to the splash screen
+- remade the splash screen to show multiple messages on app initialization
+- added a new splash image
+- added a control in Preferences -> General -> GUI Settings -> Splash Screen that control if the splash screen is shown at startup
+
+11.09.2019
+
+- added the Gerber code as source for the panelized object in Panelize Tool
+- whenever a Gerber file is deleted, the mark_shapes objects are deleted also
+- made faster the Gerber parser for the case of having a not valid geometry when loading a Gerber file without buffering
+- updated code in self.on_view_source() to make it more responsive
+- fixed the TclCommand MillHoles
+- changed the name of TclCommand MillHoles to MillDrills and added a new TclCommand named MillSlots
+- modified the MillDrills and MillSlots TclCommands to accept as parameter a list of tool diameters to be milled instead of tool indexes
+- fixed issue #302 where a copied object lost all the tools
+- modified the TclCommand DrillCncJob to have as parameter a list of tool diameters to be drilled instead of tool indexes
+- updated the Spanish translation (Google-translation)
+- added a new parameter in the TclCommands: DrillCNCJob, MillDrills, MillSlots named tol (from tolerance). If the diameters of the milled (drilled) dias are within the tolerance specified of the diameters in the Excellon object than those diameters will be processed. This is to help account for rounding errors when having units conversion
+
+10.09.2019
+
+- made isolation threaded
+- fixed a small typo in TclCommandCopperCLear
+- made changing the Plot kind in CNCJob selected tab, threaded
+- fixed an object used before declaring it in NCC Tool - Area option
+- added progress for the generation of Isolation geometry
+- added progress and possibility of graceful exit in Panel Tool
+- added graceful exit possibility when creating Isolation
+- changed the workers thread priority back to Normal
+- when disabling plots, if the selection shape is visible, it will be deleted
+- small changes in Tool Panel (eliminating some deepcopy() calls)
+- made sure that all the progress counters count to 100%
+
+9.09.2019
+
+- changed the triangulation type in VisPyVisuals for ShapeCollectionVisual class
+- added a setting in Preferences -> Gerber -> Gerber General named Buffering. If set to 'no' the Gerber objects load a lot more faster (perhaps 10 times faster than when set to 'full') but the visual look is not so great as all the aperture polygons can be seen
+- added for NCC Tool and Paint Tool a setting in the Preferences -> Tools --> (NCC Tool/ Paint Tool) that can set a progressive plotting (plot shapes as they are processed)
+- some fixes in Paint Tool when done over the Gerber objects in case that the progressive plotting is selected
+- some fixes in Gerber isolation in case that the progressive plotting is selected; added a 'Buffer solid geometry' button shown only when progressive plotting for Gerber object is selected. It will buffer the entire geometry of the object and plot it, in a threaded way.
+- modified FlatCAMObj.py file to the new string format that will allow easier translations
+- modified camlib.py, FlatCAMAPp.py and ObjectCollection.py files to the new string format that will allow easier translations
+- updated the POT file and the German language
+- fixed issue when loading unbuffered a Gerber file that has negative regions
+- fixed Panelize Tool to save the aperture geometries into the panel apertures. Also made the tool faster by removing the buffering at the end of the job
+- modified FlatCAMEditor's files to the new string format that will allow easier translations
+- updated POT file and the Romanian translation
+
+8.09.2019
+
+- added some documentation strings for methods in FlatCAMApp.App class
+- removed some @pyqtSlot() decorators as they interfere with the current way the program works
+
+7.09.2019
+
+- added a method to gracefully exit from threaded tasks and implemented it for the NCC Tool and for the Paint Tool
+- modified the on_about() function to reflect the reality in 2019 - FlatCAM it is an Open Source contributed software
+- remade the handlers for the Enable/Disable Project Tree context menu so they are threaded and activity is shown in the lower right corner of the main window
+- added to GUI new options for the Gerber object related to area subtraction
+- added new feature in the Gerber object isolation allowing for the isolation to avoid an area defined by another object (Gerber or Geometry)
+- all transformation functions show now the progress (rotate, mirror, scale, offset, skew)
+- made threaded the Offset and Scale operations found in the Selected tab of the object
+- corrected some issues and made Move Tool to show correctly when it is plotting and when it is offsetting the objects position
+- made Set Origin feature, threaded
+- updated German language translation files
+- separated the Plotting thread from the transformations threads
+
+6.09.2019
+
+- remade visibility threaded
+- reimplemented the thread listening for new FlatCAM process starting with args so it is no longer subclassed but using the moveToThread function
+- added percentage display for work done in NCC Tool
+- added percentage display for work done in Paint Tool
+- some fixes and prepared the activity monitor area to receive updated texts
+- added progress display in status bar for generating CNCJob from Excellon objects
+- added progress display in status bar for generating CNCJob from Geometry objects
+- modified all the FlatCAM tools strings to the new format in which the status is no longer included in the translated strings to make it easier for the future translations
+- more customization for the progress display in case of NCC Tool, Paint Tool and for the Gcode generation
+- updated POT file with the new strings
+- made the objects offset (therefore the Move Tool) show progress display
+
+5.09.2019
+
+- fixed issue with loading files at start-up
+- fixed issue with generating bounding box geometry for CNCJob objects
+- added some more infobar messages and log.debug
+- increased the priority for the worker tasks
+- hidden the configuration for G91 coordinates due of deciding to leave this development for another time; it require too much refactoring
+- added some messages for the G-code generation so the user know in which stage the process is
+
+4.09.2019
+
+- started to work on support for G91 in Gcode (relative coordinates)
+- added support for G91 coordinates
+- working in plotting the CNCjob generated with G91 coordinates
+
+3.09.2019
+
+- in NCC tool there is now a depth of cut parameter named 'Cut Z' which will dictate how deep the tool will enter into the PCB material
+- in NCC tool added possibility to choose between the type of tools to be used and when V-shape is used then the tool diameter is calculated from the desired depth of cut and from the V-tip parameters
+- small changes in NCC tool regarding the usage of the V-shape tool
+- fixed the isolation distance in NCC Tool for the tools with iso_op type
+- in NCC Tool now the Area adding is continuous until RMB is clicked (no key modifier is needed anymore)
+- fixed German language translation
+- in NCC Tool added a warning in case there are isolation tools and if those isolation's are interrupted by an area or a box
+- in Paint Tool made that the area selection is repeated until RMB click
+- in Paint Tool and NCC Tool fixed the RMB click detection when Area selection is used
+- finished the work on file extensions registration with FlatCAM. If the file extensions are deleted in the Preferences -> File Associations then those extensions are unregistered with FlatCAM
+- fixed bug in NCC Tools and in SolderPaste Tool if in Edit -> Preferences only one tool is entered
+- fixed bug in camblib.clear_polygon3() which caused that some copper clearing / paintings were not complete (some polygons were not processed) when the Straight Lines method was used
+- some changes in NCC Tools regarding of the clearing itself
+
+2.09.2019
+
+- fixed issue in NCC Tool when using area option
+- added formatting for some strings in the app strings, making the future translations easier
+- made changes in the Excellon Tools Table to make it more clear that the tools are selected in the # column and not in the Plot column
+- in Excellon and Gerber Selected tab made the Plot (mark) columns not selectable
+- some ToolTips were modified
+- in Properties Tool made threaded the calculation of convex_hull area and also made it to work for multi-geo objects
+- in NCC tool the type of tool that is used is transferred to the Geometry object
+- in NCC tool the type of isolation done with the tools selected as isolation tools can now be selected and it has also an Edit -> Preferences entry
+- in Properties Tool fixed the dimensions calculations (length, width, area) to work for multi-geo objects
+
+1.09.2019
+
+- fixed open handlers
+- fixed issue in NCC Tool where the tool table context menu could be installed multiple times
+- added new ability to create simple isolation's in the NCC Tool
+- fixed an issue when multi depth step is larger than the depth of cut
+
+27.08.2019
+
+- made FlatCAM so that whenever an associated file is double clicked, if there is an opened instance of FlatCAM, the file will be opened in the first instance without launching a new instance of FlatCAM. If FlatCAM is launched again it will spawn a new process (hopefully it will work when freezed).
+
+26.08.2019
+
+- added support for file associations with FlatCAM, for Windows
+
+25.08.2019
+
+- initial add of a new Tcl Command named CopperClear
+- remade the NCC Tool in preparation for the newly added TclCommand CopperClear
+- finished adding the TclCommandCopperClear that can be called with alias: 'ncc'
+- added new capability in NCC Tool when the reference object is of Gerber type and fixed some newly introduced errors
+- fixed issue #298. The changes in preprocessors done in Preferences dis not update the object UI layout as it was supposed to. The selection of Marlin postproc. did not unhidden the Feedrate Rapids entry.
+- fixed minor issues
+- fixed Tcl Command AddPolygon, AddPolyline
+- fixed Tcl Command CncJob
+- fixed crash due of Properties Tool trying to have a convex hull area on FlatCAMCNCJob objects which is not possible due of their nature
+- modified Tcl Command SubtractRectangle
+- fixed and modernized the Tcl Command Scale to be able to scale on X axis or on Y axis or on both and having as scale reference either the (0, 0) point or the minimum point of the bounding box or the center of the bounding box.
+- fixed and modernized the Tcl Command Skew
+
+24.08.2019
+
+- modified CutOut Tool so now the manual gaps adding will continue until the user is clicking the RMB
+- added ability to turn on/off the grid snapping and to jump to a location while in CutOut Tool manual gap adding action
+- made PlotCanvas class inherit from VisPy Canvas instead of creating an instance of it (work of JP)
+- fixed selection by dragging a selection shape in Geometry Editor
+- modified the Paint Tool. Now the Single Polygon and Area/Reference Object painting works with multiple tools too. The tools have to be selected in the Tool Table.
+- remade the TclCommand Paint to work in the new configuration of the the app (the painting functions are now in their own tool, Paint Tool)
+- fixed a bug in the Properties Tool
+- added a new TcL Command named Nregions who generate non-copper regions
+- added a new TclCommand named Bbox who generate a bounding box.
+
+23.08.2019
+
+- in Tool Cutout for the manual gaps, right mouse button click will exit from the action of adding gaps
+- in Tool Cutout tool I've added the possibility to create a cutout without bridge gaps; added the 'None' option in the Gaps combobox
+- in NCC Tool added ability to add multiple zones to clear when Area option is checked and the modifier key is pressed (either CTRL or SHIFT as set in Preferences). Right click of the mouse is an additional way to finish the job.
+- fixed a bug in Excellon Editor that made that the selection of drills is always cumulative
+- in Paint Tool added ability to add multiple zones to paint when Area option is checked and the modifier key is pressed (either CTRL or SHIFT as set in Preferences). Right click of the mouse is an additional way to finish the job.
+- in Paint Tool and NCC Tool, for the Area option, now mouse panning is allowed while adding areas to process
+- for all the FlatCAM tools launched from toolbar the behavior is modified: first click it will launch the tool; second click: if the Tool tab has focus it will close the tool but if another tab is selected, the tool will have focus
+- modified the NCC Tool and Paint Tool to work multiple times after first launch
+- fixed the issue with GUI entries content being deselected on right click in the box in order to copy the value
+- some changes in GUI tooltips
+- modified the way key modifiers are detected in Gerber Editor Selection class and in Excellon Editor Selection class
+- updated the translations
+- fixed aperture move in Gerber Editor
+- fixed drills/slots move in Excellon Editor
+- RELEASE 8.96
+
+22.08.2019
+
+- added ability to turn ON/OFF the detachable capability of the tabs in Notebook through a context menu activated by right mouse button click on the Notebook header
+- added ability to turn ON/OFF the detachable capability of the tabs in Plot Tab Area through a context menu activated by right mouse button click on the Notebook header
+- added possibility to turn application portable from the Edit -> Preferences -> General -> App. Preferences -> Portable checkbox
+- moved the canvas setup into it's own function and called it in the init() function
+- fixed the Buffer Tool in Geometry Editor; made the Buffer entry field a QDoubleSpinner and set the lower limit to zero.
+- fixed Tool Cutout so when the target Gerber is a single Polygon then the created manual geometry will follow the shape if shape is freeform
+- fixed TclCommandFollow command; an older function name was used who yielded wrong results
+- in Tool Cutout for the manual gaps, now the moving geometry that cuts gaps will orient itself to fit the angle of the cutout geometry
+
+21.08.2019
+
+- added feature in Paint Tool allowing the painting to be done on Gerber objects
+- added feature in Paint Tool to set how (and if) the tools are sorted
+- added Edit -> Preferences GUI entries for the above just added features
+- added new entry in Properties Tool which is the calculated Convex Hull Area (should give a more precise area for the irregular shapes than the box area)
+- added some more strings in Properties Tool for the translation
+- in NCC Tool added area selection feature
+- fixed bug in Excellon parser for the Excellon files that do not put the type of zero suppression they use in the file (like DipTrace eCAD)
+- fixed some issues introduced in NCC Tool
+
+20.08.2019
+
+- added ability to do copper clearing through NCC Tool on Geometry objects
+- replaced the layout from Grid to Form for the Reference objects comboboxes in Paint Tool and in NCC Tool
+
+19.08.2019
+
+- updated the Edit -> Preferences to include also the Gerber Editor complete Preferences
+- started to update the app strings to make it easier for future translations
+- fixed the POT file and the German translation
+- some mods in the Tool Sub
+- fixed bug in Tool Sub that created issues when toggling visibility of the plots
+- fixed the Spanish, Brazilian Portuguese and Romanian translations
+
+18.08.2019
+
+- made the exported preferences formatted therefore more easily read
+- projects at startup don't work in another thread so there is no multithreading if I want to double click an project and to load it
+- added messages in the application window title which show the progress in loading a project (which is not thread-safe therefore keeping the app from fully initialize until finished)
+- in NCC Tool added a new parameter (radio button) that offer the choice on the order of the tools both in tools table and in execution of engraving; added as a parameter also in Edit -> Preferences -> Tools -> NCC Tool
+- added possibility to drag & drop FlatCAM config files (*.FlatConfig) into the canvas to be opened into the application
+- added GUI in Paint tool in beginning to add Paint by external reference object 
+- finished adding in Paint Tool the usage of an external object to set the extent of th area painted. For simple shapes (single Polygon) the shape can be anything, for the rest will be a convex hull of the reference object
+- modified NCC tool so for simple objects (single Polygon) the external object used as reference can have any shape, for the other types of objects the copper cleared area will be the convex hull of the reference object
+- modified the strings of the app wherever they contained the char seq <b> </b> so it is not included in the translated string
+- updated the translation files for the modified strings (and for the newly added strings)
+- added ability to lock toolbars within the context menu that is popped up on any toolbars right mouse click. The value is saved in QSettings and it is persistent between application startup's.
+
+17.08.2019
+
+- added estimated time of routing for the CNCJob and added travelled distance parameter for geometry, too
+- fixed error when creating CNCJob due of having the annotations disabled from preferences but the plot2() function from camlib.CNCJob class still performed operations who yielded TypeError exceptions
+- coded a more accurate way to estimate the job time in CNCJob, taking into consideration if there is a usage of multi depth which generate more passes
+- another fix (final one) for the Exception generated by the annotations set not to show in Preferences
+- updated translations and changed version
+- fixed installer issue for the x64 version due of the used CX_FREEZE python package which was in unofficial version (obviously not ready to be used)
+- fixed bug in Geometry Editor, in disconnect_canvas_event_handlers() where I left some part of code without adding a try - except block which was required
+- moved the initialization of the FlatCAM editors after a read of the default values. If I don't do this then only at the first start of the application the Editors are not functional as the Editor objects are most likely destroyed
+- fixed bug in FlatCAM editors that caused the shapes to be drawn without resolution when the app units where INCH
+- modified the transformation functions in all classes in camlib.py and FlatCAMObj.py to work with empty geometries
+- RELEASE 8.95
+
+17.08.2019
+
+- updated the translations for the new strings
+- RELEASE 8.94
+
+16.08.2019
+
+- working in Excellon Editor to Tool Resize to consider the slots, too
+- fixed a weird error that created a crash in the following scenario: create a new excellon, edit it, add some drills/slots, delete it without saving, create a new excellon, try to edit and a crash is issued due of a wrapped C++ error
+- fixed bug selection in Excellon editor that caused not to select the corresponding row (tool dia) in the tool table when a selection rectangle selected an even number of geometric elements
+- updated the default values to more convenient ones
+- remade the enable/disable plots functions to work only where it needs to (no sense in disabling a plot already disabled)
+- made sure that if multi depth is choosed when creating GCode then if the multidepth is more than the depth of cut only one cut is made (to the depth of cut)
+- each CNCJob object has now it's own text_collection for the annotations which allow for the individual enabling and disabling of the annotations
+- added new menu category in File -> Backup with two menu entries that duplicate the functions of the export/import preferences buttons from the bottom of the Preferences window
+- in Excellon Editor fixed the display of the number of slots in the Tool Table after the resize done with the Resize tool
+- in Excellon Editor -> Resize tool, made sure that when the slot is resized, it's length remain the same, because the tool should influence only the 'thickness' of the slot. Since I don't know anything but the geometry and tool diameters (old and new), this is only an approximation and computationally intensive
+- in Excellon Editor -> remade the Tool edit made by editing the diameter values in the Tools Table to work for slots too
+- In Excellon Editor -> fixed bug that caused incorrect display of the relative coordinates in the status bar
+
+15.08.2019
+
+- added Edit -> Preferences GUI and storage for the Excellon Editor Add Slots
+- added a confirmation message for objects delete and a setting to activate it in Edit -> Preferences -> Global
+- merged pull request from Mike Smith which fix an application crash when attempting to open a not-a-FlatCAM-project file as project
+- merged pull request from Mike Smith that add support for a new SVG element: <use>
+- stored inside FlatCAM app the VisPy data files and at the first start the application will try to copy those files to the APPDATA (roaming) folder in case of running under Windows OS
+- created a configuration file in the root/config/configuration.txt with a configuration line for portability. Set portable to True to run the app as portable
+- working on the Slots Array in Excellon Editor - building the GUI
+- added a failsafe path to the source folder from which to copy the VisPy data
+- fixed the GUI for Slot Arrays in Excellon Editor
+- finished the Slot Array tool in Excellon Editor
+- added the key shortcut handlers for Add Slot and Add Slot Array tools in Excellon Editor
+- started to work on the Resize tool for the case of Excellon slots in Excellon Editor
+- final fix for the VisPy data files; the defaults files are saved to the Config folder when the app is set to be portable
+- added the Slot Type parameter for exporting Excellon in Edit -> Preferences -> Excellon -> Export Excellon. Now the Excellon object can be exported also with drilled slot command G85
+- fixed bug in Excellon export when there are no zero suppression (coordinates with decimals)
+
+14.08.2019
+
+- fixed the loading of Excellon with slots and the saving of edited Excellon object in regard of slots, in Excellon Editor
+- fixed the Delete tool, Select tool in Excellon Editor to work for Slots too
+- changes in the way the edited Excellon with added slots is saved
+- added more icons and cursor in Excellon Editor for Slots related functions
+- in Excellon Editor fixed the selection issue which in a certain step created a failure in the Copy and Move tools.
+- in Excellon Editor fixed the selection with key modifier pressed
+- edited the mouse cursors and saved them without included thumbnail in a bid to remove some CRC warnings made by libpng
+
+13.08.2019
+
+- added new option in ToolSub: the ability to close (or not) the resulting paths when using tool on Geometry objects. Added also a new category in the Edit -> Preferences -> Tools, the Substractor Tool Options
+- some PEP8 changes in FlatCAMApp.py
+- added new settings in Edit -> Preferences -> General for Notebook Font size (set font size for the items in Project Tree and for text in Selected Tab) and for canvas Axis font size. The values are stored in QSettings.
+- updated translations
+- fixed a bug in FCDoubleSpinner GUI element
+- added a new parameter in NCC tool named offset. If the offset is used then the copper clearing will finish to a set distance of the copper features
+- fixed bugs in Geometry Editor
+- added protection's against the 'bowtie' geometries for Subtract Tool in Geometry Editor
+- added all the tools from Geometry Editor to the the contextual menu
+- fixed bug in Add Text Tool in Geometry Editor that gave error when clicking to place text without having text in the box
+- added all the tools from Gerber Editor to the the contextual menu
+- added the menu entry "Edit" in the Project contextual menu for Gerber objects
+- started to work in adding slots and slots array in Excellon Editor
+- in SlotAdd finished the utility geometry and the GUI for it
+
+12.08.2019
+
+- done regression to solve the bug with multiple passes cutting from the copper features (I should remember not to make mods here)
+- if 'combine' is checked in Gerber isolation but there is only one pass, the resulting geometry will still be single geo
+- the 'passes' entry was changed to a IntSpinner so it will allow passes to be entered only in range (1, 999) - it will not allow entry of 0 which may create some issues
+- improved the GerberObject.isolate() function to work for geometry in the form of list and also in case that the elements of the list are LinearRings (like when doing the Exterior Isolation)
+- in NCC Tool made sure that at each run the old objects are deleted
+- fixed bug in camlib.Gerber.parse_lines() Gerber parser where for Allegro Gerber files the Gerber units were incorrectly detected
+- improved Mark Area Tool in Gerber Editor such that at each launch the previous markings are deleted
+
+11.08.2019
+
+- small changes regarding the Project Title
+- trying to fix reported bugs
+- made sure that the annotations are deleted when the object that contain them is deleted
+- fixed issue where the annotations for all the CNCJob objects are toggled together whenever the ones for an single object are toggled
+- optimizations in GeoEditor
+- updated translations
+
+10.08.2019
+
+- added new feature in NCC Tool: now another object can be used as reference for the area extent to be cleared of copper
+- fixed issue in the latest feature in NCC Tool: now it works also with reference objects made out of LineStrings (tool 'Path' in Geometry Editor)
+- translation files updated for the new strings (Google Translate)
+- RELEASE 8.93
+
+9.08.2019
+
+- added Exception handing for the case when the user is trying to save & overwrite a file already opened in another file
+- finished added 'Area' type of Paint in Paint Tool
+- fixed bug that created a choppy geometry for CNCJob when working in INCH
+- fixed bug that did not asked the user to save the preferences after importing a new set of preferences, after the user is trying to close the Preferences tab window
+
+7.08.2019
+
+- replaced setFixedWidth calls with setMinimumWidth
+- recoded the camlib.Geometry.isolation_geometry() function
+- started to work on Paint Area in Paint Tool
+
+6.08.2019
+
+- fixed bug that crashed the app after creating a new geometry, if a new object is loaded and the new geometry is deleted and then trying to select the just loaded new object
+- made some GUI elements in Edit -> Preferences to have a minimum width as opposed to the previous fixed one
+- fixed issue in the isolation function, if the isolation can't be done there will be generated no Geometry object 
+- some minor UI changes
+- strings added and translations updated
+
+5.08.2019
+
+- made sure that if using an negative Gerber isolation diameter, the resulting Geometry object will use a tool with positive diameter
+- fixed bug that when isolating a Gerber file made out of a single polygon, an RecursionException was issued together with inability to create tbe isolation
+- when applying a new language if there are any changes in the current project, the app will offer to save the project before the reboot
+
+3.08.2019
+
+- added project name to the window title
+- fulfilled request: When saving a CNC file, if the file name is changed in the OS window, the new name does appear in the “Selected” (in name) and “Project” tabs (in cnc_job)
+- solved bug such that the app is not crashing when some apertures in the Gerber file have no geometry. More than that, now the apertures that have geometry elements are bolded as opposed to the ones without geometry for which the text is unbolded
+- merged a pull request with language changes for Russian translate
+- updated the other translations
+
+31.07.2019
+
+- changed the order of the menu entries in the FIle -> Open ...
+- organized the list of recent files so the Project entries are to the top and separated from the other types of file
+- work on identification of changes in Preferences tab
+- added categories names for the recent files
+- added a detection if any values are changed in the Edit -> Preferences window and on close it will ask the user if he wants to save the changes or not
+- created a new menu entry in the File menu named Recent projects that will hold the recent projects and the previous "Recent files" will hold only the previous loaded files
+- updated all translations for the new strings
+- fixed bug recently introduced that when changing the units in the Edit -> Preferences it did not converted the values
+- fixed another bug that when selecting an Excellon object after disabling it it crashed the app
+- RELEASE 8.92
+
+30.07.2019
+
+- fixed bug that crashed the software when trying to edit a GUI value in Geometry selected tab without having a tool in the Tools Table
+- fixed bug that crashed the app when trying to add a tool without a tool diameter value
+- Spanish Google translation at 77%
+- changed the Disable plots menu entry in the context menu, into a Toggle Visibility menu entry
+- Spanish Google translation 100% but two strings (big ones) - needs review
+- added two more strings to translation strings (due of German language)
+- completed the Russian translation using the Google and Yandex translation engines (minus two big strings) - needs review
+
+28.07.2019
+
+- fixed issue with not using the current units in the tool tables after unit conversion
+- after unit conversion from Preferences, the default values are automatically saved by the app
+- in Basic mode, the tool type column is no longer hidden as it may create issues when using an painted geometry
+- some PEP8 clean-up in FlatCAMGui.py
+- fixed Panelize Tool to do panelization for multiple passes type of geometry that comes out of the isolation done with multiple passes
+
+20.07.2019
+
+- updated the CutOut tool so it will work on single PCB Gerbers or on PCB panel Gerbers
+- updated languages
+- 70% progress in Spanish Google translation
+
+19.07.2019
+
+- fixed bug in FlatCAMObj.GeometryObject.ui_disconnect(); the widgets signals were not disconnected from handlers when required therefore the signals were connected in an exponential way
+- some changes in the widgets used in the Selected tab for Geometry object
+- some PEP8 cleanup in FlatCAMObj.py
+- updated languages
+- 60% progress in Spanish Google translation
+
+17.07.2019
+
+- added some more strings to the translatable ones, especially the radio button labels
+- updated the .POT file and the available translations
+- 51% progress in Spanish Google translation
+- version date change
+
+16.07.2019
+
+- PEP8 correction in flatcamTools
+- merged the Brazilian-portuguese language from a pull request made by Carlos Stein
+- more PEP8 corrections
+
+15.07.2019
+
+- some PEP8 corrections
+
+13.07.2019
+
+- fixed a possible issue in Gerber Object class
+- added a new tool in Gerber Editor: Mark Area Tool. It will mark the polygons in a edited Gerber object with areas within a defined range, allowing to delete some of the not necessary  copper features
+- added new menu links in the Gerber Editor menu for Eraser Tool and Mark Area Tool
+- added key shortcuts for Eraser Tool (Ctrl+E) and Mark Area Tool (Alt+A) and updated the shortcuts list
+
+9.07.2019
+
+- some changes in the app.on_togle_units() to make sure we don't try to convert empty parameters which may cause crashes on FlatCAM units change
+- updated setup_ubuntu.sh file
+- made sure to import certain libraries in some of the FlatCAM files and not to rely on chained imports
+
+8.07.2019
+
+- fixed bug that allowed empty tool in the tools generated in Geometry object
+- fixed bug in Tool Cutout that did not allow the transfer of used cutout tool diameter to the cutout geometry object
+
+5.07.2019
+
+- fixed bug in CutOut Tool
+- some other bug in CutOut tool fixed
+
+1.07.2019
+
+- Spanish translation at 36%
+
+28.06.2019
+
+- Spanish translation (Google Translate) at 21%
+
+27.06.2019
+
+- added new translation: Spanish. Finished 10%
+
+23.06.2019
+
+- fixes issues with units conversion when the tool diameters are a list of comma separated values (NCC Tool, SolderPaste Tool and Geometry Object)
+- fixed a "typo" kind of bug in SolderPaste Tool
+- RELEASE 8.919
+
+22.06.2019
+
+- some GUI layout optimizations in Edit -> Preferences
+- added the possibility for multiple tool diameters in the Edit -> Preferences -> Geometry -> Geometry General -> Tool dia separated by comma
+- fixed scaling for the multiple tool diameters in Edit -> Preferences -> Geometry -> Geometry General -> Tool dia, for NCC tools more than 2 and for Solderpaste nozzles more than 2
+- fixed bug in CNCJob where the CNC Tools table will show always only 2 decimals for Tool diameters regardless of the current measuring units
+- made the tools diameters decimals in case of INCH FlatCAM units to be 4 instead of 3
+- fixed bug in updating Grid values whenever toggling the FlatCAM units and the X, Y Grid values are linked, bugs which caused the Y value to be scaled incorrectly
+- set the decimals for Grid values to be set to 6 if the units of FlatCAM is INCH and to set to 4 if FlatCAM units are METRIC
+- updated translations
+- updated the Russian translation from 51% complete to 69% complete using the Yandex translation engine
+- fixed recently introduced bug in milling drills/slots functions
+- moved Substract Tool from Menu -> Edit -> Conversions to Menu -> Tool
+- fixed bug in Gerber isolation (Geometry expects now a value in string format and not float)
+- fixed bug in Paint tool: now it is possible to paint geometry generated by External Isolation (or Internal isolation)
+- fixed bug in editing a multigeo Geometry object if previously a tool was deleted
+- optimized the toggle of annotations; now there is no need to replot the entire CNCJob object too on toggling of the annotations
+- on toggling off the plot visibility the annotations are turned off too
+- updated translations; Russian translation at 76% (using Yandex translator engine - needs verification by a native speaker of Russian)
+
+20.06.2019
+
+- fixed Scale and Buffer Tool in Gerber Editor
+- fixed Editor Transform Tool in Gerber Editor
+- added a message in the status bar when copying coordinates to clipboard with SHIFT + LMB click combo
+- languages update
+
+19.06.2019
+
+- milling an Excellon file (holes and/or slots) will now transfer the chosen milling bit diameter to the resulting Geometry object
+
+17.06.2019
+
+- fixed bug where for Geometry objects after a successful object rename done in the Object collection view (Project tab), deselect the object and reselect it and then in the Selected tab the name is not the new one but the old one
+- for Geometry objects, adding a new tool to the Tools table after a successful rename will now store the new name in the tool data
+
+15.06.2019
+
+- fixed bug in Gerber parser that made the Gerber files generated by Altium Designer 18 not to be loaded
+- fixed bug in Gerber editor - on multiple edits on the same object, the aperture size and dims were continuously multiplied due of the file units not being updated
+- restored the FlatCAMObj.visible() to a non-threaded default
+
+11.06.2019
+
+- fixed the Edit -> Conversion -> Join ... functions (merge() functions)
+- updated translations
+- Russian translate by @camellan is not finished yet
+- some PEP8 cleanup in camlib.py
+- RELEASE 8.918
+
+9.06.2019
+
+- updated translations
+- fixed the the labels for shortcut keys for zoom in and zoom out both in the Menu links and in the Shortcut list
+- made sure the zoom functions use the global_zoom_ratio parameter from App.self.defaults dictionary.
+- some PEP8 cleanup
+
+8.06.2019
+
+- make sure that the annotation shapes are deleted on creation of a new project
+- added folder for the Russian translation
+- made sure that visibility for TextGroup is set only if index is not None in VisPyVisuals.TextGroup.visible() setter
+
+7.06.2019
+
+- fixed bug in ToolCutout where creating a cutout object geometry from another external isolation geometry failed
+- fixed bug in cncjob TclCommand where the gcode could not be correctly generated due of missing bounds params in obj.options dict
+- fixed a hardcoded tolerance in GeometryObject.generatecncjob() and in GeometryObject.mtool_gen_cncjob() to use the parameter from Preferences
+- updated translations
+
+5.06.2019
+
+- updated translations
+- some layout changes in Edit -> Preferences such that the German translation (longer words than English) to fit correctly
+- after editing an parameter the focus is lost so the user knows that something happened
+
+4.06.2019
+
+- PEP8 updates in AppExcEditor.py
+- added the Excellon Editor parameters to the Edit -> Preferences -> Excellon GUI
+- fixed a small bug in Excellon Editor
+- PEP8 cleanup in FlatCAMGui
+- finished adding the Excellon Editor parameters into the app logic and added a selection limit within Excellon Editor just like in the other editors
+
+3.06.2019
+
+- TclCommand Geocutout is now creating a new geometry object when working on a geometry, preserving also the origin object
+- added a new parameter in Edit -> Preferences -> CNCJob named Annotation Color; it controls the color of the font used for annotations
+- added a new parameter in Edit -> Preferences -> CNCJob named Annotation Size; it controls the size of the font used for annotations
+- made visibility change threaded in FlatCAMObj()
+
+2.06.2019
+
+- fixed issue with geometry name not being updated immediately after change while doing geocutout TclCommand
+- some changes to enable/disable project context menu entry handlers
+
+1.06.2019
+
+- fixed text annotation for CNC job so there are no overlapping numbers when 2 lines meet on the same point
+- fixed issue in CNC job plotting where some of the isolation polygons are painted incorrectly
+- fixed issue in CNCJob where the set circle steps is not used 
+
+31.05.2019
+
+- added the possibility to display text annotation for the CNC travel lines. The setting is both in Preferences and in the CNC object properties
+
+30.05.2019
+
+- editing a multi geometry will no longer pop-up a Tcl window
+- solved issue #292 where a new geometry renamed with many underscores failed to store the name in a saved project
+- the name for the saved projects are updated to the current time and not to the time of the app startup
+- some PEP8 changes related to comments starting with only one '#' symbol
+- more PEP8 cleanup
+- solved issue where after the opening of an object the file path is not saved for further open operations
+
+24.05.2019
+
+- added a toggle Grid button to the canvas context menu in the Grids submenu
+- added a toggle left panel button to the canvas context menu
+
+23.05.2019
+
+- fixed bug in Gerber editor FCDisk and DiscSemiEditorGrb that the resulting geometry was not stored into the '0' aperture where all the solids are stored
+- fixed minor issue in Gerber Editor where apertures were included in the saved object even if there was no geometric data for that aperture
+- some PEP8 cleanup in FlatCAMApp.py
+
+22.05.2019
+
+- Geo Editor - added a new editor tool, Eraser
+- some PEP8 cleanup of the Geo Editor
+- fixed some selection issues in the new tool Eraser in Geometry Editor
+- updated the translation files
+- RELEASE 8.917
+
+21.05.2019
+
+- added the file extension .ncd to the Excellon file extension list
+- solved parsing issue for Excellon files generated by older Eagle versions (v6.x)
+- Gerber Editor: finished a new tool: Eraser. It will erase certain parts of Gerber geometries having the shape of a selected shape.
+
+20.05.2019
+
+- more PEP8 changes in Gerber editor
+- Gerber Editor - started to work on a new editor tool: Eraser
+
+19.05.2019
+
+- fixed the Circle Steps parameter for both Gerber and Geometry objects not being applied and instead the app internal defaults were used.
+- fixed the Tcl command Geocutout issue that gave an error when using the 4 or 8 value for gaps parameter
+- made wider the '#' column for Apertures Table for Gerber Object and for Gerber Editor; in this way numbers with 3 digits can be seen
+- PEP8 corrections in AppGerberEditor.py
+- added a selection limit parameter for Geometry Editor
+- added entries in Edit -> Preferences for the new parameter Selection limit for both the Gerber and Geometry Editors.
+- set the buttons in the lower part of the Preferences Window to have a preferred minimum width instead of fixed width
+- updated the translation files
+
+18.05.2019
+
+- added a new toggle option in Edit -> Preferences -> General Tab -> App Preferences -> "Open" Behavior. It controls which path is used when opening a new file. If checked the last saved path is used when saving files and the last opened path is used when opening files. If unchecked then the path for the last action (either open or save) is used.
+- fixed App.convert_any2gerber to work with the new Gerber apertures data structure
+- fixed Tool Sub to work with the new Gerber apertures data structure
+- fixed Tool PDF to work with the new Gerber apertures data structure
+
+17.05.2019
+
+- remade the Tool Cutout to work on panels
+- remade the Tool Cutout such that on multiple applications on the same object it will yield the same result
+- fixed an issue in the remade Cutout Tool where when applied on a single Gerber object, the Freeform Cutout produced no cutout Geometry object
+- remade the Properties Tool such that it works with the new Gerber data structure in the obj.apertures. Also changed the view for the Gerber object in Properties
+- fixed issue with false warning that the Gerber object has no geometry after an empty Gerber was edited and added geometry elements
+
+16.05.2019
+
+- Gerber Export: made sure that if some of the coordinates in a Gerber object geometry are repeating then the resulting Gerber code include only one copy
+- added a new parameter/feature: now the spindle can work in clockwise mode (CW) or counter clockwise mode (CCW)
+
+15.05.2019
+
+- rewrited the Gerber Parser in camlib - success
+- moved the self.apertures[aperture]['geometry'] processing for clear_geometry (geometry made with Gerber LPC command) in Gerber Editor
+- Gerber Editor: fixed the Poligonize Tool to work with new geometric structure and took care of a special case
+- Gerber Export is fixed to work with the new Gerber object data structure and it now works also for Gerber objects edited in Gerber Editor
+- Gerber Editor: fixed units conversion for obj.apertures keys that require it
+- camlib Gerber parser - made sure that we don't loose goemetry in regions
+- Gerber Editor - made sure that for some tools the added geometry is clean (the coordinates are non repeating)
+- covered some possible issues in Gerber Export
+
+12.05.2019
+
+- some modifications to ToolCutout
+
+11.05.2019
+
+- fixed issue in camlib.CNCjob.generate_from_excellon_by_tool() in the drill path optimization algorithm selection when selecting the MH algorithm. The new API's for Google OR-tools required some changes and also the time parameter can be now just an integer therefore I modified the GUI
+- made the Feedrate Rapids parameter to depend on the type of preprocessor choosed. It will be showed only for a preprocessor which the name contain 'marlin' and for any preprocessor's that have 'custom' in the name
+- fixed the camlib.Gerber functions of mirror, scale, offset, skew and rotate to work with the new data structure for apertures geometry
+- fixed Gerber Editor selection to work with the new Gerber data structure in self.apertures
+- fixed Gerber Editor PadEditorGrb class to work with the new Gerber data structure in self.apertures
+- fixed camlib.Gerber issues related to what happen after parsing rectangular apertures 
+- wip in camblib.Gerber
+- completely converted the Gerber editor to the new data structure
+- Gerber Editor: added a threshold limit for how many elements a move selection can have. If above the threshold only a bounding box Poly will be painted on canvas as utility geometry.
+
+10.05.2019
+
+- Gerber Editor - working in conversion to the new data format
+- made sure that only units toggle done in Edit -> Preferences will toggle the data in Preferences. The menu entry Edit -> Toggle Units and the shortcut key 'Q' will change only the display units in the app
+- optimized Transform tool
+- RELEASE 8.916
+
+9.05.2019
+
+- reworked the Gerber parser
+
+8.05.2019
+
+- added zoom fit for Set Origin command
+- added move action for solid_geometry stored in the gerber_obj.apertures
+- fixed camlib.Gerber skew, rotate, offset, mirror functions to work for geometry stored in the Gerber apertures
+- fixed Gerber Editor follow_geometry reconstruction
+- Geometry Editor: made the tool to be able to continuously move until the tool is exited either by ESC key or by right mouse button click
+- Geometry Editor Move Tool: if no shape is selected when triggering this tool, now it is possible to make the selection inside the tool
+- Gerber editor Move Tool: fixed a bug that repeated the plotting function unnecessarily 
+- Gerber editor Move Tool: if no shape is selected the tool will exit
+
+7.05.2019
+
+- remade the Tool Panelize GUI
+- work in Gerber Export: finished the header export
+- fixed the Gerber Object and Gerber Editor Apertures Table to not show extra rows when there are aperture macros in the object
+- work in Gerber Export: finished the body export but have some errors with clear geometry (LPC)
+- Gerber Export - finished
+
+6.05.2019
+
+- made units change from shortcut key 'Q' not to affect the preferences
+- made units change from Edit -> Toggle Units not to affect the preferences
+- remade the way the aperture marks are plotted in Gerber Object
+- fixed some bugs related to moving an Gerber object with the aperture table in view
+- added a new parameter in the Edit -> Preferences -> App Preferences named Geo Tolerance. This parameter control the level of geometric detail throughout FlatCAM. It directly influence the effect of Circle Steps parameter.
+- solved a bug in Excellon Editor that caused app crash when trying to edit a tool in Tool Table due of missing a tool offset
+- updated the ToolPanelize tool so the Gerber panel of type GerberObject can be isolated like any other GerberObject object
+- updated the ToolPanelize tool so it can be edited
+- modified the default values for toolchangez and endz parameters so they are now safe in all cases
+
+5.05.2019
+
+- another fix for bug in clear geometry processing for Gerber apertures
+- added a protection for the case that the aperture table is part of a deleted object
+- in Script Editor added support for auto-add closing parenthesis, brace and bracket
+- in Script Editor added support for "CTRL + / " key combo to comment/uncomment line
+
+4.05.2019
+
+- fixed bug in camlib.parse_lines() in the clear_geometry processing section for self.apertures
+- fixed bug in parsing Gerber regions (a point was added unnecessary)
+- renamed the menu entry Edit -> Copy as Geo to Convert Any to Geo and moved it in the Edit -> Conversion
+- created a new function named Convert Any to Gerber and installed it in Edit -> Conversion. It's doing what the name say: it will convert an Geometry or Excellon FlatCAM object to a Gerber object.
+
+01.05.2019
+
+- the project items color is now controlled from Foreground Role in ObjectCollection.data()
+- made again plot functions threaded but moved the dataChanged signal (update_view() ) to the main thread by using an already existing signal (plots_updated signal) to avoid the errors with register QVector
+- Enable/Disable Object toggle key ("Space" key) will trigger also the datChanged signal for the Project MVC
+- added a new setting for the color of the Project items, the color when they are disabled.
+- fixed a crash when triggering 'Jump To' menu action (shortcut key 'J' worked ok)
+- made some mods to what can be translated as some of the translations interfered with the correct functioning of FlatCAM
+- updated the translations
+- fixed bugs in Excellon Editor
+- Excellon Editor:  made Add Pad tool to work until right click
+- Excellon Editor: fixed mouse right click was always doing popup context menu
+- GUIElements.FCEntry2(): added a try-except clause
+- made sure that the Tools Tab is cleared on Editors exit
+- Geometry Editor: restored the old behavior: a tool is active until it is voluntarily exited: either by using the 'ESC' key, or selecting the Select tool or new: right click on canvas
+- RELEASE 8.915
+
+30.04.2019
+
+- in ObjectCollection class, made sure that renaming an object in Project View does not result in an empty name. If new name is blank the rename is cancelled.
+- made ObjectCollection.TreeItem() inherit KeySensitiveListVIew and implicitly QTreeView (in the hope that the theme applied on app will be applied on the tree items, too (for MacOs new DarkUI theme)
+- renamed SilkScreen Tool to Substract Tool and move it's menu location in Edit -> Conversion
+- started to modify the Substract Tool to work on Geometry objects too
+- progress in the new Substract Tool for Geometry Objects
+- finished the new Substract Tool
+- added new setting for the color of the Project Tree items; it helps in providing contrast when using dark theme like the one in MacOS
+
+29.04.2019
+
+- solved bug in Gerber Editor: the '0' aperture (the region aperture) had no size which created errors. Made the size to be zero.
+- solved bug in editors: the canvas selection shape was not deleted on mouse release if the grid snap was OFF
+- solved bug in Excellon Editor: when selecting a drill hole on canvas the selected row in the Tools Table was not the correct one but the next highest row
+- finished the Silkscreen Tool but there are some limitations (some wires fragments from silkscreen are lost)
+- solved the issue in Silkscreen Tool with losing some fragments of wires from silkscreen
+
+26.04.2019
+
+- small changes in GUI; optimized contextual menu display
+- made sure that the Project Tab is disabled while one of the Editors is active and it is restored after returning to app
+- fixed some bugs recently introduced in Editors due of the changes done to the way mouse panning is detected 
+- cleaned up the context menu's when in Editors; made some structural changes
+- updated the code in camlib.CNCJob.generate_from_excellon_by_tools() to work with the new API from Google OR-Tools
+- all Gerber regions (G36 G37) are stored in the '0' aperture
+- fixed a bug that added geometry with clear polarity in the apertures where was not supposed to be
+
+25.04.2019
+
+- Geometry Editor: modified the intersection (if the selected shapes don't intersects preserve them) and substract functions (delete all shapes that were used in the process)
+- work in the ToolSub
+- for all objects, if in Selected the object name is changed to the same name, the rename is not done (because there is nothing changed)
+- fixed Edit -> Copy as Geom function handler to work for Excellon objects, too
+- made sure that the mouse pointer is restored to default on Editor exit
+- added a toggle button in Preferences to toggle on/off the display of the selection box on canvas when the user is clicking an object or selecting it by mouse dragging.
+
+24.04.2019
+
+- PDF import tool: working in making the PDF layer rendering multithreaded in itself (one layer rendered on each worker)
+- PDF import tool: solved a bug in parsing the rectangle subpath (an extra point was added to the subpath creating nonexisting geometry)
+- PDF import tool: finished layer rendering multithreading
+- New tool: Silkscreen Tool: I am trying to remove the overlapped geo with the soldermask layer from overlay layer; layed out the class and functions - not working yet
+
+23.04.2019
+
+- Gerber Editor: added two new tools: Add Disc and Add SemiDisc (porting of Circle and Arc from Geometry Editor)
+- Gerber Editor: made Add Pad repeat until user exits the Add Pad through either mouse right click, or ESC key or deselecting the Add Pad menu item
+- Gerber and Geometry Editors: fixed some issues with the Add Arc/Add Semidisc; in mode 132, the norm() function was not the one from numpy but from a FlatCAM Class. Also fixed some of the texts and made sure that when changing the mode, the current points are reset to prepare for the newly selected mode.
+- Fixed Measurement Tool to show the mouse coordinates on the status bar (it was broken at some point)
+- updated the translation files
+- added more custom mouse cursors in Geometry and Gerber Editors
+- RELEASE 8.914
+
+22.04.2019
+
+- added PDF file as type in the Recent File list and capability to load it from there
+- PDF's can be drag & dropped on the GUI to be loaded
+- PDF import tool: added support for save/restore Graphics stack. Only for scale and offset transformations and for the linewidth. This is the final fix for Microsoft PDF printer who saves in PDF format 1.7
+- PDF Import tool: added support for PDF files that embed multiple Gerber layers (top, bottom, outline, silkscreen etc). Each will be opened in it's own Gerber file. The requirement is that each one is drawn in a different color
+- PDF Import tool: fixed bugs when drag & dropping PDF files on canvas the files geometry previously opened was added to the new one. Also scaling issues. Solved.
+- PDF Import tool: added support for detection of circular geometry drawn with white color which means actually invisible color. When detected, FlatCAM will build an Excellon file out of those geoms.
+- PDF Import tool: fixed storing geometries in apertures with the right size (before they were all stored in aperture D10)
+
+21.04.2019
+
+- fixed the PDF import tool to work with files generated by the Microsoft PDF printer (chained subpaths)
+- in PDF import tool added support for paths filled and at the same time stroked ('B' and 'B*'commands)
+- added a shortcut key for PDF Import Tool (Alt+Q) and updated the Shortcut list (also with the 'T' and 'R' keys for Gerber Editor where they control the bend in Track and Region tool and the 'M' and 'D' keys for Add Arc tool in Geometry Editor)
+
+20.04.2019
+
+- finished adding the PDF import tool although it does not support all kinds of outputs from PDF printers. Microsoft PDF printer is not supported.
+
+19.04.2019
+
+- started to work on PDF import tool
+
+
+18.04.2019
+
+- Gerber Editor: added custom mouse cursors for each mode in Add Track Tool
+- Gerber Editor: Poligonize Tool will first fuse polygons that touch each other and at a second try will create a polygon. The polygon will be automatically moved to Aperture '0' (regions).
+- Gerber Editor: Region Tool will add regions only in '0' aperture
+- Gerber Editor: the bending mode will now survive until the tool is exited
+- Gerber Editor: solved some bugs related with deleting an aperture and updating the last_selected_aperture
+
+17.04.2019
+
+- Gerber Editor: added some messages to warn user if no selection exists when trying to do aperture deletion or aperture geometry deletion
+- fixed version check
+- added custom mouse cursors for some tools in Gerber Editor
+- Gerber Editor: added multiple modes to lay a Region: 45-degrees, reverse 45-degrees, 90-degrees, reverse 90-degrees and free-angle. Added also key shortcuts 'T' and 'R' to cycle forward, respectively in reverse through the modes.
+- Excellon Editor: fixed issue not remembering last tool after adding a new tool
+- added custom mouse cursors for Excellon and Geometry Editors in some of their tools
+
+16.04.2019
+
+- added ability to use ENTER key to finish tool adding in Editors, NCC Tool, Paint Tool and SolderPaste Tool.
+- Gerber Editor: started to add modes of laying a track
+- Gerber Editor: Add Track Tool: added 5 modes for laying a track: 45-degrees, reverse-45 degrees, 90-degrees, reverse 90-degrees and free angle. Key 'T' will cycle forward through the modes and key 'R' will cycle in reverse through the track laying modes.
+- Gerber Editor: Add Track Tool: first right click will finish the track. Second right click will exit the Track Tool and return to Select Tool.
+- Gerber Editor: added protections for the Pad Array and Pad Tool for the case when the aperture size is zero (the aperture where to store the regions)
+
+15.04.2019
+
+- working on a new tool to process automatically PcbWizard Excellon files which are generated in 2 files
+- finished ToolPcbWizard; it will autodetect the Excellon format, units from the INF file
+- Gerber Editor: reduced the delay to show UI when editing an empty Gerber object
+- update the order of event handlers connection in Editors to first connect new handlers then disconnect old handlers. It seems that if nothing is connected some VispY functions like canvas panning no longer works if there is at least once nothing connected to the 'mouse_move' event
+- Excellon Editor: update so always there is a tool selected even after the Excellon object was just edited; before it always required a click inside of the tool table, not you do it only if needed.
+- fixed the menu File -> Edit -> Edit/Close Editor entry to reflect the status of the app (Editor active or not)
+- added support in Excellon parser for autodetection of Excellon file format for the Excellon files generated by the following ECAD sw: DipTrace, Eagle, Altium, Sprint Layout
+- Gerber Editor: finished a new tool: Poligonize Tool (Alt+N in Editor). It will fuse a selection of tracks into a polygon. It will fill a selection of polygons if they are apart and it will make a single polygon if the selection is overlapped. All the newly created filled polygons will be stored in aperture '0' (if it does not exist it will be automatically created)
+- fixed a bug in Move command in context menu who crashed the app when triggered
+- Gerber Editor: when adding a new aperture it will be store as the last selected and it will be used for any tools that are triggered until a new aperture is selected.
+
+14.04.2019
+
+- Gerber Editor: Remade the processing of 'clear_geometry' (geometry generated by polygons made with Gerber LPC command) to work if more than one such polygon exists
+- Gerber Editor: a disabled/enabled sequence for the VisPy cursor on Gerber edit make the graphics better
+- Editors: activated an old function that was no longer active: each tool can have it's own set of shortcut keys, the Editor general shortcut keys that are letters are overridden
+- Gerber and Geometry editors, when using the Backspace keys for certain tools, they will backtrack one point but now the utility geometry is immediately updated
+- In Geometry Editor I fixed bug in Arc modes. Arc mode shortcut key is now key 'M' and arc direction change shortcut key is 'D'
+- moved the key handler out of the Measurement tool to flatcamGUI.FlatCAMGui.keyPressEvent()
+- Gerber Editor: started to add new function of poligonize which should make a filled polygon out of a shape
+- cleaned up Measuring Tool
+- solved bug in Gerber apertures size and dimensions values conversion when file units are different than app units
+
+13.04.2019
+
+- updating the German translation
+- Gerber Editor: added ability to change on the fly the aperture after one of the tools: Add Pad or Add Pad Array is activated
+- Gerber Editor: if a tool is cancelled via key shortcut ESCAPE, the selection is now deleted and any other action require a new selection
+- finished German translation (Google translated with some adjustments)
+- final fix for issue #277. Previous fix was applied only for one case out of three.
+- RELEASE 8.913
+
+12.04.2019
+
+- Gerber Editor: added support for Oblong type of aperture
+- fixed an issue with automatically filled in aperture code when the edited Gerber file has no apertures; established an default with value 10 (according to Gerber specifications)
+- fixed a bug in editing a blank Gerber object
+- added handlers for the Gerber Editor context menu
+- updated the translation template POT file and the EN PO/MO files
+- Gerber Editor: added toggle effect to the Transform Tool
+- Gerber Editor: added shortcut for Transform Tool and also toggle effect here, too
+- updated the shortcut list with the Gerber Editor shortcut keys
+- Gerber Editor: fixed error when adding an aperture with code value lower than the ones that already exists
+- when adding an aperture with code '0' (zero) it will automatically be set with size zero and type: 'REG' (from region); here we store all the regions from a Gerber file, the ones without a declared aperture
+- Gerber Editor: added support for Gerber polarity change commands (LPD, LPC)
+- moved the polarity change processing from AppGerberEditor() class to camlib.Gerber().parse_lines()
+- made optional the saving of an edited object. Now the user can cancel the changes to the object.
+- replaced the standard buttons in the QMessageBox's used in the app with custom ones that can have text translated
+- updated the POT translation file and the MO/PO files for English and Romanian language
+
+11.04.2019
+
+- changed the color of the marked apertures to the global_selection_color
+- Gerber Editor: added Transformation Tool and Rotation key shortcut
+- in all Editors, manually deactivating a button in the editor toolbar will automatically select the 'Select' button
+- fixed Excellon Editor selection: when a tool is selected in Tools Table, all the drills belonging to that tool are selected. When a drill is selected on canvas, the associated tool will be selected without automatically selecting all other drills with same tool
+- Gerber Editor: added Add Pad Array tool
+- Gerber Editor: in Add Pad Array tool, if the pad is not circular type, for circular array the pad will be rotated to match the array angle
+- Gerber Editor: fixed multiple selection with key modifier such that first click selects, second deselects
+
+10.04.2019
+
+- Gerber Editor: added Add Track and Add Region functions
+- Gerber Editor: fixed key shortcuts
+- fixed setting the Layout combobox in Preferences according to the current layout
+- created menu links and shortcut keys for adding a new empty Gerber objects; on update of the edited Gerber, if the source object was an empty one (new blank one) this source obj will be deleted
+- removed the old apertures editing from Gerber Obj selected tab
+- Gerber Editor: added Add Pad (circular or rectangular type only)
+- Gerber Editor: autoincrement aperture code when adding new apertures
+- Gerber Editor: automatically calculate the size of the rectangular aperture
+
+9.04.2019
+
+- Gerber Editor: added buffer and scale tools
+- Gerber Editor: working on aperture selection to show on Aperture Table
+- Gerber Editor: finished the selection on canvas; should be used as an template for the other Editors
+- Gerber Editor: finished the Copy, Aperture Add, Buffer, Scale, Move including the Utility geometry
+- Trying to fix bug in Measurement Tool: the mouse events don't disconnect
+- fixed above bug in Measurement Tool (but there is a TODO there)
+
+7.04.2019
+
+- default values for Jump To function is jumping to origin (0, 0)
+
+6.04.2019
+
+- fixed bug in Geometry Editor in buffer_int() function that created an Circular Reference Error when applying buffer interior on a geometry.
+- fixed issue with not possible to close the app after a project save.
+- preliminary Gerber Editor.on_aperture_delete() 
+- fixed 'circular reference' error when creating the new Gerber file in Gerber Editor
+- preliminary Gerber Editor.on_aperture_add()
+
+5.04.2019
+
+- Gerber Editor: made geometry transfer (which is slow) to Editor to be multithreaded
+- Gerber Editor: plotting process is showed in the status bar
+- increased the number of workers in FlatCAM and made the number of workers customizable from Preferences
+- WIP in Gerber Editor: geometry is no longer stored in a Rtree storage as it is not needed
+- changed the way delayed plot is working in Gerber Editor to use a Qtimer instead of python threading module
+- WIP in Gerber Editor
+- fixed bug in saving the maximized state
+- fixed bug in applying default language on first start
+~~- on activating 'V' key shortcut (zoom fit) the mouse cursor is now jumping to origin (0, 0)~~
+- fixed bug in saving toolbars state; the file was saved before setting the self.defaults['global_toolbar_view]
+
+4.04.2019
+
+- added support for Gerber format specification D (no zero suppression) - PCBWizard Gerber files support
+- added support for Excellon file with no info about tool diameters - PCB Wizard Excellon file support
+- modified the bogus diameters series for Excellon objects that do not have tool diameter info
+- made Excellon Editor aware of the fact that the Excellon object that is edited has fake (bogus) tool diameters and therefore it will not sort the tools based on diameter but based on tool number
+- fixed bug on Excellon Editor: when diameter is edited in Tools Table and the target diameter is already in the tool table, the drills from current tool are moved to the new tool (with new dia) - before it crashed
+- fixed offset after editing drill diameters in Excellon Editor.
+
+3.04.2019
+
+- fixed plotting in Gerber Editor
+- working on GUI in Gerber Editor
+- added a Gcode end_command: default is M02
+- modified the calling of the editor2object() slot function to fix an issue with updating geometry imported from SVG file, after edit
+- working on Gerber Editor - added the key shortcuts: wip
+- made saving of the project file non-blocking and also while saving the project file, if the user tries again to close the app while project file is being saved, the app will close only after saving is complete (the project file size is non zero)
+- fixed the camlib.Geometry.import_svg() and camlib.Gerber.bounds() to work when importing SVG files as Gerber
+
+31.03.2019
+
+- fixed issue #281 by making generation of a convex shape for the freeform cutout in Tool Cutout a choice rather than the default
+- fixed bug in Tool Cutout, now in manual cutout mode the gap size reflect the value set
+- changed Measuring Tool to use the mouse click release instead of mouse click press; also fixed a bug when using the ESC key.
+- fixed errors when the File -> New Project is initiated while an Editor is still active.
+- the File->Exit action handler is now self.final_save() 
+- wip in Gerber editor
+
+29.03.2019
+
+- update the TCL keyword list
+- fix on the Gerber parser that makes searching for '%%' char optional when doing regex search for mode, units or image polarity. This allow loading Gerber files generated by the ECAD software TCl4.4
+- fix error in plotting Excellon when toggling units
+- FlatCAM editors now are separated each in it's own file
+- fixed TextTool in Geometry Editor so it will open the notebook on activation and close it after finishing text adding
+- started to work on a Gerber Editor
+- added a fix in the Excellon parser by allowing a comma in the tool definitions between the diameter and the rest
+
+28.03.2019
+
+- About 45% progress in German translation
+- new feature: added ability to edit MultiGeo geometry (geometry from Paint Tool)
+- changed all the info messages that are of type warning, error or success so they have a space added after the keyword
+- changed the Romanian translation by adding more diacritics  
+- modified Gerber parser to copy the follow_geometry in the self.apertures
+- modified the Properties Tool to show the number of elements in the follow_geometry for each aperture
+- modified the copy functions to copy the follow_geometry and also the apertures if it's possible (only for Gerber objects)
+
+27.03.2019
+
+- added new feature: user can delete apertures in Advanced mode and then create a new FlatCAM Gerber object
+- progress in German translation. About 27% done.
+- fixed issue #278. Crash on name change in the Name field in the Selected Tab.
+
+26.03.2019
+
+- fixed an issue where the Geometry plot function protested that it does not have an parameter that is used by the CNCJob plot function. But both inherit from FaltCAMObj plot function which does not have that parameter so something may need to be changed. Until then I provided a phony keyboard parameter to make that function 'shut up'
+- fixed bug: after using Paint Tool shortcut keys are disabled
+- added CNCJob geometry for the holes created by the drills from Excellon objects
+
+25.03.2019
+
+- in the TCL completer if the word is already complete don't add it again but add a space
+- added all the TCL keywords in the completer keyword list
+- work in progress in German translation ~7%
+- after any autocomplete in TCL completer, a space is added
+- fixed an module import issue in NCC Tool
+- minor change (optimization) of the CNCJob UI
+- work in progress in German translation ~20%
+
+22.03.2019
+
+- fixed an error that created a situation that when saving a project with some of the CNCJob objects disabled, on project reload the CNCJob objects are no longer loaded
+- fixed the Gerber.merge() function. When some of the Gerber files have apertures with same id, create a new aperture id for the object that is fused because each aperture id may hold different geometries.
+- changed the autoname for saving Preferences, Project and PNG file
+
+20.03.2019
+
+- added autocomplete finish with ENTER key for the TCL Shell
+- made sure that the autocomplete function works only for FlatCAM Scripts
+- ESC key will trigger normal view if in full screen and the ESC key is pressed
+- added an icon and title text for the Toggle Units QMessageBox
+
+19.03.2019
+
+- added autocomplete for Code editor;
+- autocomplete in Code Editor is finished by hitting either TAB key or ENTER key
+- fixed the Gerber.merge() to work for the case when one of the merged Gerber objects solid_geometry type is Polygon and not a list
+
+18.03.2019
+
+- added ability to create new scripts and open scripts in FlatCAM Script Editor
+- the Code Editor tab name is changed according to the task; 'save' and 'open' buttons will have filters installed for the QOpenDialog fit to the task
+- added ability to run a FlatCAM Tcl script by double-clicking on the file
+- in Code Editor added shortcut combo key Ctrl+Shift+V to function as a Special Paste that will replace the '\' char with '/' so the Windows paths will be pasted correctly for TCL Shell. Also doing SHIFT + LMB on the Paste in contextual menu is doing the same.
+
+17.03.2019
+
+- remade the layout in 2Sided Tool
+- work in progress for translation in Romanian - 91%
+- changed some of the app strings formatting to work better with Poedit translation software
+- fixed bug in Drillcncjob TclCommand
+- finished translation in Romanian
+- made the translations work when the app is frozen with CX_freeze
+- some formatting changes for the application strings
+- some changes on how the first layout is applied
+- minor bug fixes (typos from copy/paste from another part of the program)
+
+16.03.2019
+
+- fixed bug in Paint Tool - Single Poly: no geometry was generated
+- work in progress for translation in Romanian - 70%
+
+13.03.2019
+
+- made the layout combobox current item from Preferences -> General window to reflect the current layout
+- remade the POT translate file
+- work in progress in translation for Romanian language 44%
+- fix for showing tools by activating them from the Menu - final fix.
+
+11.03.2019
+
+- changed some icons here and there
+- fixed the Properties Project menu entry to work on the new way
+- in Properties tool now the Gerber apertures show the number of polygons in 'solid_geometry' instead of listing the objects
+- added a visual cue in Menu -> Edit about the entries to enter the Editor and to Save & Exit Editor. When one is enabled the other is disabled.
+- grouped all the UI files in flatcamGUI folder
+- grouped all parser files in flatcamParsers folder
+- another changes to the final_save() function
+- some strings were left outside the translation formatting - fixed
+- finished the replacement of '_' symbols throughout the app which conflicted with the _() function used by the i18n
+- reverted changes in Tools regarding the toggle effect - now they work as expected
+
+10.03.2019
+
+- added a fix in the Gerber parser when adding the geometry in the self.apertures dict for the case that the current aperture is None (Allegro does that)
+- finished support for internationalization by adding a set of .po/.mo files for the English language. Unfortunately the final action can be done only when Beta will be out of Beta (no more changes) or when I will decide to stop working on this app.
+- changed the tooltip for 'feedrate_rapids' parameter to point out that this parameter is useful only for the Marlin preprocessor
+- fix app crash for the case that there are no translation files
+- fixed some forgotten strings to be prepared for internationalization in ToolCalculators
+- fixed Tools menu no longer working due of changes
+- added some test translation for the ToolCalculators (in Romanian)
+- fixed bug in ToolCutOut where for each tool invocation the signals were reconnected
+- fixed some issues with ToolMeasurement due of above changes
+- updated the App.final_save() function
+- fixed an issue created by the fact that I used the '_' char inside the app to designate unused info and that conflicted with the _() function used by gettext
+- made impossible to try to reapply current language that it's already applied (un-necessary)
+
+8.03.2019
+
+- fixed issue when doing th CTRL (or SHIFT) + LMB, the focus is automatically moved to Project Tab
+- further work in internationalization, added a fallback to English language in case there is no translation for a string
+- fix for issue #262: when doing Edit-> Save & Close Editor on a Geometry that is not generated through first entering into an Editor, the geometry disappear
+- finished preparing for internationalization for the files: camlib and objectCollection
+- fixed tools shortcuts not working anymore due of the new toggle parameter for the .run().
+- finished preparing for internationalization for the files: FlatCAMEditor, MainGUI
+- finished preparing for internationalization for the files: FlatCAMObj, ObjectUI
+- sorted the languages in the Preferences combobox
+
+7.03.2019
+
+- made showing a shape when hovering over objects, optional, by adding a Preferences -> General parameter
+- starting to work in internationalization using gettext()
+- Finished adding _() in FlatCAM Tools
+- fixed Measuring Tool - after doing a measurement the Notebook was switching to Project Tab without letting the user see the results
+- more work on the translation engine; the app now restarts after a language is applied
+- added protection against using Travel Z parameter with negative or zero value (in Geometry).
+- made sure that when the Measuring Tools is active after last click the Status bar is no longer deleted
+
+6.03.2019
+
+- modified the way the FlatCAM Tools are run from toolbar as opposed of running them from other sources
+- some Gerber UI changes
+
+5.03.2019
+
+- modified the grbl-laser preprocessor lift_code()
+- treated an error created by Z_Cut parameter being None
+- changed the hover and selection box transparency
+
+4.03.2019
+
+- finished work on object hovering
+- fixed Excellon object move and all the other transformations
+- starting to work on Manual Cutout Tool
+- remade the CutOut Tool
+- finished Manual Cutout Tool by adding utility geometry to the cutting geometry
+- added CTRL + click behavior for adding manual bridge gaps in Cutout Tool
+- in Tool Cutout added shortcut key 'Escape' to cancel the current adding of bridge gaps
+
+3.03.2019
+
+- minor UI changes for Gerber UI
+- ~~after an object move, the apertures plotted shapes are deleted from canvas and the 'mark all' button is deselected~~
+- after move tool action or any other transform (rotate, skew, scale, mirror, offset), the plotted apertures are kept plotted.
+- changing units now will convert all the default values from one unit type to another
+- prettified the selection shape and the moving shape
+- initial work in object hovering shape
+
+02.03.2019
+
+- fixed offset, rotate, scale, skew for follow_geometry. Fixed the move tool also.
+- fixed offset, rotate, scale, skew for 'solid_geometry' inside the self.apertures.
+
+28.02.2019
+
+- added a change that when a double click is performed in a object on canvas resulting in a selection, if the notebook is hidden then it will be displayed
+- progress in ToolChange Custom commands replacement and rename
+
+27.02.2019
+
+- made the Custom ToolChange Text area in CNCJob Selected Tab depend on the status of the ToolChange Enable Checkbox even in the init stage.
+- added some parameters throughout camlib gcode generation functions; handled some possible errors (e.g like when attempting to use an empty Custom GCode Toolchange)
+- added toggle effect for the tools in the toolbar.
+- enhanced the toggle effect for the tools in the Tools Toolbar and also for Notebook Tab selection: if the current tool is activated it will toggle the notebook side but only if the installed widget is itself. If coming from another tool, the notebook will stay visible
+- upgraded the Tool Cutout when done from Gerber file to create a convex_hull around the Gerber file rather than trying to isolate it
+- added some protections for the FlatCAM Tools run after an object was loaded
+
+26.02.2019
+
+- added a function to read the parameters from ToolChange macro Text Box (I need to move it from CNCJob to Excellon and Geometry)
+- fixed the geometry adding to the self.apertures in the case when regions are done without declaring any aperture first (Allegro does that). Now, that geometry will be stored in the '0' aperture with type REG
+- work in progress to Toolchange_Custom code replacement -> finished the parse and replace function
+- fixed mouse selection on canvas, mouse drag, mouse click and mouse double click
+- fixed Gerber Aperture Table dimensions
+- added a Mark All button in the Gerber aperture table.
+- because adding shapes to the shapes collection (when doing Mark or Mark All) is time consuming I made the plot_aperture() threaded.
+- made the polygon fusing in modified Gerber creation, a list comprehension in an attempt for optimization
+- when right clicking the files in Project tab, the Save option for Excellon no longer export it but really save the original. 
+- in ToolChange Custom Code replacement, the Text Box in the CNCJob Selected tab will be active only if there is a 'toolchange_custom' in the name of the preprocessor file. This assume that it is, or was created having as template the Toolchange Custom preprocessor file.
+
+
+25.02.2019
+
+- fixed the Gerber object UI layout
+- added ability to mark individual apertures in Gerber file using the Gerber Aperture Table
+- more modifications for the Gerber UI layout; made 'follow' an advanced Gerber option
+- added in Preferences a new Category: Gerber Advanced Options. For now it controls the display of Gerber Aperture Table and the "follow" attribute4
+- fixed GerberObject.merge() to merge the self.apertures[ap]['solid_geometry'] too
+- started to work on a new feature that allow adding a ToolChange GCode macro - GUI added both in CNCJob Selected tab and in CNCJob Preferences
+- added a limited 'sort-of' Gerber Editor: it allows buffering and scaling of apertures
+
+24.02.2019
+
+- fixed a small bug in the Tool Solder Paste: the App don't take into consideration pads already filled with solder paste.
+- prettified the defaults files and the recent file. Now they are ordered and human readable
+- added a Toggle Code Editor Menu and key shortcut
+- added the ability to open FlatConfig configuration files in Code Editor, Modify them and then save them.
+- added ability to double click the FlatConfig files and open them in the FlatCAM Code Editor (to be verified)
+- when saving a file from Code Editor and there is no object active then the OpenFileDialog filters are reset to FlatConfig files.
+- reverted a change in GCode that might affect Gerber polarity change in Gerber parser
+- ability to double click the FlatConfig files and open them in the FlatCAM Code Editor - fixed and verified
+- fixed the Set To Origin function when Escape was clicked
+- added all the Tools in a new ToolBar
+- fixed bug that after changing the layout all the toolbar actions are no longer working
+- fixed bug in Set Origin function
+- fixed a typo in Toolchange_Probe_MACH3 preprocessor
+
+23.02.2019
+
+- remade the SolderPaste geometry generation function in ToolSoderPaste to work in certain scenarios where the Gerber pads in the SolderPaste mask Gerber may be just pads outlines
+- updated the Properties Tool to include more information's, also details if a Geometry is of type MultiGeo or SingleGeo
+- remade the Preferences GUI to include the Advanced Options in a separate way so it is obvious which are displayed when App Level is Advanced.
+- added protection, not allowing the user to make a Paint job on a MultiGeo geometry (one that is converted in the Edit -> Conversion menu)) because it is not supported
+
+22.02.2019
+
+- added Repetier preprocessor file
+- removed "added ability to regenerate objects (it's actually deletion followed by recreation)" because of the way Python pass parameters to functions by reference instead of copy
+- added ability to toggle globally the display of ToolTips. Edit -> Preferences -> General -> Enable ToolTips checkbox.
+- added true fullscreen support (for Windows OS)
+- added the ability of context menu inside the GuiElements.FCCombobox() object.
+- remade the UI for ToolSolderPaste. The object comboboxes now have context menu's that allow object deletion. Also the last object created is set as current item in comboboxes.
+- some GUI elements changes
+
+21.02.2019
+
+- added protection against creating CNCJob from an empty Geometry object (with no geometry inside)
+- changed the shortcut key for YouTube channel from F2 to key F4
+- changed the way APP LEVEL is showed both in Edit -> Preferences -> General tab and in each Selected Tab. Changed the ToolTips content for this.
+- added the functions for GCode View and GCode Save in Tool SolderPaste
+- some work in the Gcode generation function in Tool SolderPaste
+- added protection against trying to create a CNCJob from a solder_paste dispenser geometry. This one is different than the default Geometry and can be handled only by SolderPaste Tool.
+- ToolSolderPaste tools (nozzles) now have each it's own settings
+- creating the camlib functions for the ToolSolderPaste gcode generation functions
+- finished work in ToolSolderPaste
+- fixed issue with not updating correctly the plot kind (all, cut, travel) when clicking in the CNC Tools Table plot buttons
+- made the GCode Editor for ToolSolderPaste clear the text before updating the Code Editor tab
+- all the Tabs in Plot Area are closed (except Plot Area itself) on New Project creation
+- added ability to regenerate objects (it's actually deletion followed by recreation)
+
+20.02.2019
+
+- finished added a Tool Table for Tool SolderPaste
+- working on multi tool solder paste dispensing
+- finished the Edit -> Preferences defaults section
+- finished the UI, created the preprocessor file template
+- finished the multi-tool solder paste dispensing: it will start using the biggest nozzle, fill the pads it can, and then go to the next smaller nozzle until there are no pads without solder.
+
+19.02.2019
+
+- added the ability to compress the FlatCAM project on save with LZMA compression. There is a setting in Edit -> Preferences -> Compression Level between 0 and 9. 9 level yields best compression at the price of RAM usage and time spent.
+- made FlatCAM able to load old type (uncompressed) FlatCAM projects
+- fixed issue with not loading old projects that do not have certain information's required by the new versions of FlatCAM
+- compacted a bit more the GUI for Gerber Object
+- removed the Open Gerber with 'follow' menu entry and also the open_gerber Tcl Command attribute 'follow'. This is no longer required because now the follow_geometry is stored by default in a Gerber object attribute gerber_obj.follow_geometry
+- added a new parameter for the Tcl CommandIsolate, named: 'follow'. When follow = 1 (True) the resulting geometry will follow the Gerber paths.
+- added a new setting in Edit -> Preferences -> General that allow to select the type of saving for the FlatCAM project: either compressed or uncompressed. Compression introduce an time overhead to the saving/restoring of a FlatCAM project.
+- started to work on Solder Paste Dispensing Tool
+- fixed a bug in rotate from shortcut function
+- finished generating the solder paste dispense geometry
+
+18.02.2019
+
+- added protections again wrong values for the Buffer and Paint Tool in Geometry Editor
+- the Paint Tool in Geometry Editor will load the default values from Tool Paint in Preferences
+- when the Tools in Geometry Editor are activated, the notebook with the Tool Tab will be unhidden. After execution the notebook will hide again for the Buffer Tool.
+- changed the font in Tool names
+- added in Geometry Editor a new Tool: Transformation Tool.
+- in Geometry Editor by selecting a shape with a selection shape, that object was added multiple times (one per each selection) to the selected list, which is not intended. Bug fixed.
+- finished adding Transform Tool in Geometry Editor - everything is working as intended
+- fixed a bug in Tool Transform that made the user to not be able to capture the click coordinates with SHIFT + LMB click combo
+- added the ability to choose an App QStyle out of the offered choices (different for each OS) to be applied at the next app start (Preferences -> General -> Gui Pref -> Style Combobox)
+- added support for FlatCAM usage with High DPI monitors (4k). It is applied on the next app startup after change in Preferences -> General -> Gui Settings -> HDPI Support Checkbox
+- made the app not remember the window size if the app is maximized and remember in QSettings if it was maximized. This way we can restore the maximized state but restore the windows size unmaximized
+- added a button to clear the GUI preferences in Preferences -> General -> Gui Settings -> Clear GUI Settings
+- added key shortcuts for the shape transformations within Geometry Editor: X, Y keys for Flip(mirror), Shift+X, Shift+Y combo keys for Skew and Alt+X, Alt+Y combo keys for Offset
+- adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around) 
+- modified the GUI in Objects Selected Tab to accommodate 2 different modes: basic and Advanced. In Basic mode, some of the functionality's are hidden from the user.
+- added Tool Transform preferences in Edit -> Preferences and used them through out the app
+- made the output of Panelization Tool a choice out of Gerber and Geometry type of objects. Useful for those who want to engrave multiple copies of the same design.
+
+17.02.2019
+
+- changed some status bar messages
+- New feature: added the capability to view the source code of the Gerber/Excellon file that was loaded into the app. The file is also stored as an object attribute for later use. The view option is in the project context menu and in Menu -> Options -> View Source
+- Serialized the source_file of the Objects so it is saved in the FlatCAM project and restored.
+- if there is a single tool in the tool list (Geometry , Excellon) and the user click the Generate GCode, use that tool even if it is not selected
+- fixed issue where after loading a project, if the default kind of CNCjob view is only 'cuts' the plot will revert to the 'all' type
+- in Editors, if the modifier key set in Preferences (CTRL or SHIFT key) is pressed at the end of one tool operation it will automatically continue to that action until the modifier is no longer pressed when Select tool will be automatically selected.
+- in Geometry Editor, on entry the notebook is automatically hidden and restored on Geometry Editor exit.
+- when pressing Escape in Geometry Editor it will automatically deselect any shape not only the currently selected tool.
+- when deselecting an object in Project menu the status bar selection message is deleted
+- added ability to save the Gerber file content that is stored in FlatCAM on Gerber file loading. It's useful to recover from saved FlatCAM projects when the source files are no longer available.
+- fixed an issue where the function handler that changed the layout had a parameter changed accidentally by an index value passed by the 'activate' signal to which was connected
+- fixed bug in paint function in Geometry Editor that didn't allow painting due of overlap value
+
+16.02.2019
+
+- added the 'Save' menu entry to the Project context menu, for CNCJob: it will export the GCode.
+- added messages in info bar when selecting objects in the Project View list
+- fixed DblSided Tool so it correctly creates the Alignment Drills Excellon file using the new structure
+- fixed DblSided Tool so it will not crash the app if the user tries to make a mirror using no coordinates
+- added some relevant status bar messages in DblSided Tool
+- fixed DblSided Tool to correctly use the Box object (until now it used as reference only Gerber object in spite of Excellon or Geometry objects being available)
+- fixed DblSided Tool crash when trying to create Alignment Drills object without a Tool diameter specified
+- fixed DblSided Tool issue when entering Tool diameter values with comma decimal separator instead of decimal dot separator
+- fixed Cutout Tool Freeform to generate cutouts with options: LR, TB. 2LR, 2TB which didn't worked previously
+- fixed Excellon parser to detect correctly the units and zeros for Excellon's generated by Eagle 9.3.0
+- modified the initial size of the canvas on startup
+- modified the build file (make_win.py) to solve the issue with suddenly not accepting the version as Beta
+- changed the initial layout to 'compact'
+- updated the install scripts to uninstall a previously installed FlatCAM Beta (that has the same GUID)
+
+15.02.2019
+
+- rearranged the File and Edit menu's and added some explanatory tooltips on certain menu items that could be seen as cryptic
+- added Excellon Export Options in Edit -> Preferences
+- started to work in using the Excellon Export parameters
+- remade the Excellon export function to work with parameters entered in Edit -> Preferences -> Excellon Export
+- added a new entry in the Project Context Menu named 'Save'. It will actually work for Geometry and it will do Export DXF and for Excellon and it will do Export Excellon
+- reworked the offer to save a project so it is done only if there are objects in the project but those objects are new and/or are modified since last project load (if an old project was loaded.)
+- updated the Excellon plot function so it can plot the Excellon's from old projects
+- removed the message boxes that popup on Excellon Export errors and replaced them with status bar messages
+- small change in tab width so the tabs looks good in Linux, too.
+
+14.02.2019
+
+- added total travel distance for CNCJob object created from Excellon Object in the CNCJob Selected tab
+- added 'FlatCAM ' prefix to any detached tab, for easy identification
+- remade the Grids context menu (right mouse button click on canvas). Now it has values linked to the units type (inch or mm). Added ability to add or delete grid values and they are persistent.
+- updated the function for the project context menu 'Generate CNC' menu entry (Action) to use the modernized function FlatCAMObj.GeometryObject.on_generatecnc_button_click()
+- when linked, the grid snap on Y will copy the value in grid snap on X in real time
+- in Gerber aperture table now the values are displayed in the current units set in FlatCAM
+- added shortcut key 'J' (jump to location) in Editors and added an icon to the dialog popup window
+- the notebook is automatically collapsed when there are no objects in the collection and it is showed when adding an object
+- added new options in Edit -> Preferences -> General -> App Preferences to control if the Notebook is showed at startup and if the notebook is closed when there are no objects in the collection and showed when the collection has objects.
+
+13.02.2019
+
+- added new parameter for Excellon Object in Preferences: Fast Retract. If the checkbox is checked then after reaching the drill depth, the drill bit will be raised out of the hole asap.
+- started to work on GUI forms simplification
+- changed the Preferences GUI for Geometry and Excellon Objects to make a difference between parameters that are changed often and those that are not.
+- changed the layout in the Selected Tab UI
+- started to add apertures table support
+- finished Gerber aperture table display
+- made the Gerber aperture table not visible as default and added a checkbox that can toggle the visibility
+- fixed issue with plotting in CNCJob; with Plot kind set to something else than 'all' when toggling Plot, it was defaulting to kind = 'all'
+- added (and commented) an experimental FlatCAMObj.GerberObject.plot_aperture()
+
+12.02.2019
+
+- whenever a FlatCAM tool is activated, if the notebook side is hidden it will be unhidden
+- reactivated the Voronoi classes
+- added a new parameter named Offset in the Excellon tool table - work in progress
+- finished work on Offset parameter in Excellon Object (Excellon Editor, camlib, FlatCAMObj updated to take this param in consideration)
+- fixed a bug where in Excellon editor when editing a file, a tool was automatically added. That is supposed to happen only for empty newly created Excellon Objects.
+- starting to work on storing the solid_geometry for each tool in part in Excellon Object
+- stored solid_geometry of Excellon object in the self.tools dictionary
+- finished the solid_geometry restore after edit in Excellon Editor
+- finished plotting selection for each tool in the Excellon Tool Table
+- fixed the camlib.Excellon.bounds() function for the new type of Excellon geometry therefore fixed the canvas selection, too
+
+
+10.02.2019
+
+- the SELECTED type of messages are no longer printed to shell from 2 reasons: first, too much spam and second, issue with displaying html
+- on set_zero function and creation of new geometry or new excellon there is no longer a zoom fit 
+- repurposed shortcut key 'Delete' to delete tools in tooltable when the mouse is over the Seleted tab (with Geometry inside) or in Tools tab (when NCC Tool or Paint Tool is inside). Or in Excellon Editor when mouse is hovering the Selected tab selecting a tool, 'Delete' key will delete that tool, if on canvas 'Delete' key will delete a selected shape (drill). In rest, will delete selected objects.
+- adjusted the preprocessor files so the Spindle Off command (M5) is done before the move to Toolchange Z
+- adjusted the Toolchange Manual preprocessor file to have more descriptive messages on the toolchange event
+- added a strong focus to the object_name entry in the Selected tab
+- the keypad keyPressed are now detected correctly
+- added a pause and message/warning to do a rough zero for the Z axis, in case of Toolchange_Probe_MACH3 preprocessor file
+- changes in Toolchange_Probe_MACH3 preprocessor file
+
+9.02.2019
+
+- added a protection for when saving a file first time, it require a saved path and if none then it use the current working directory
+- added into Preferences the Calculator Tools
+- made the Preferences window scrollable on the horizontal side (it was only vertically scrollable before)
+- fixed an error in Excellon Editor -> add drill array that could appear by starting the function to add a drill array by shortcut before any mouse move is registered while in Editor
+- changed the messages from status bar on new object creation/selection
+- in Geometry Editor fixed the handler for the Rotate shortcut key ('R')
+
+8.02.2019
+
+- when shortcut keys 1, 2, 3 (tab selection) are activated, if the splitter left side (the notebook) is hidden it will be made visible
+- changed the menu entry Toggle Grid name to Toggle Grid Snap
+- fixed errors in Toggle Axis
+- fixed error with shortcut key triggering twice the keyPressEvent when in the Project List View
+- moved all shortcut keys handlers from Editors to the keyPressEvent() handler from FLatCAMGUI
+- in Excellon Editor added a protection for Tool_dia field in case numbers using comma as decimal separator are used. Also added a QDoubleValidator forcing a number with max 4 decimals and from 0.0000 to 9.9999
+- in Excellon Editor added a shortcut key 'T' that popup a window allowing to enter a new Tool with the set diameter
+- in App added a shortcut key 'T' that popup a windows allowing to enter a new Tool with set diameter only when the Selected tab is on focus and only if a Geometry object is selected
+- changed the shortcut key for Transform Tool from 'T' to 'Alt+T'
+- fixed bug in Geometry Selected tab that generated error when used tool offset was less than half of either total length or half of total width. Now the app signal the issue with a status bar message
+- added Double Validator for the Offset value so only float numbers can be entered.
+- in App added a shortcut key 'T' that popup a windows allowing to enter a new Tool with set diameter only when the Tool tab is on focus and only if a NCC Tool or Paint Area Tool object is installed in the Tool Tab
+- if trying to add a tool using shortcut key 'T' with value zero the app will react with a message telling to use a non-zero value.
+
+7.02.2019
+
+- in Paint Tool, when painting single polygon, when clicking on canvas for the polygon there is no longer a selection of the entire object
+- commented some debug messages
+- imported speedups for shapely
+- added a disable menu entry in the canvas contextual menu
+- small changes in Tools layout
+- added some new icons in the help menu and reorganized this menu
+- added a new function and the shortcut 'leftquote' (left of Key 1) for toggle of the notebook section
+- changed the Shortcut list shortcut key to F3
+- moved some graphical classes out of Tool Shell to GUIElements.py where they belong
+- when selecting an object on canvas by single click, it's name is displayed in status bar. When nothing is selected a blank message (nothing) it's displayed
+- in Move Tool I've added the type of object that was moved in the status bar message
+- color coded the status bar bullet to blue for selection
+- the name of the selected objects are displayed in the status bar color coded: green for Gerber objects, Brown for Excellon, Red for Geometry and Blue for CNCJobs.
+
+6.02.2019
+
+- fixed the units calculators crash FlatCAM when using comma as decimal separator
+- done a regression on Tool Tab default text. It somehow delete Tools in certain scenarios so I got rid of it
+- fixed bug in multigeometry geometry not having the bounds in self.options and crashing the GCode generation
+- fixed bug that crashed whole application in case that the GCode editor is activated on a Tool gcode that is defective. 
+- fixed bug in Excellon Slots milling: a value of a dict key was a string instead to be an int. A cast to integer solved it.
+- fixed the name self-insert in save dialog file for GCode; added protection in case the save path is None
+- fixed FlatCAM crash when trying to make drills GCode out of a file that have only slots.
+- changed the messages for Units Conversion
+- all key shortcuts work across the entire application; moved all the shortcuts definitions in MainGUI.keyPressEvent()
+- renamed the theme to layout because it is really a layout change
+- added plot kind for CNC Job in the App Preferences
+- combined the geocutout and cutout_any TCL commands - work in progress
+- added a new function (and shortcut key Escape) that when triggered it deselects all selected objects and delete the selection box(es) 
+- fixed bug in Excellon Gcode generation that made the toolchange X,Y always none regardless of the value in Preferences
+- fixed the Tcl Command Geocutout to work with Gerber objects too (besides Geometry objects)
+
+5.02.3019
+
+- added a text in the Selected Tab which is showed whenever the Selected Tab is selected but without having an object selected to display it's properties
+- added an initial text in the Tools tab
+- added possibility to use the shortcut key for shortcut list in the Notebook tabs
+- added a way to set the Probe depth if Toolchange_Probe preprocessors are selected
+- finished the preprocessor file for MACH3 tool probing on toolchange event
+- added a new parameter to set the feedrate of the probing in case the used preprocessor does probing (has toolchange_probe in it's name)
+- fixed bug in Marlin preprocessor for the Excellon files; the header and toolchange event always used the parenthesis witch is not compatible with GCode for Marlin
+- fixed a issue with a move to Z_move before any toolchange
+
+4.02.2019
+
+- modified the Toolchange_Probe_general preprocessor file to remove any Z moves before the actual toolchange event
+- created a prototype preprocessor file for usage with tool probing in MACH3
+- added the default values for Tool Film and Tool Panelize to the Edit -> Preferences
+- added a new parameter in the Tool Film which control the thickness of the stroke width in the resulting SVG. It's a scale parameter.
+- whatever was the visibility of the corresponding toolbar when we enter in the Editor, it will be set after exit from the Editor (either Geometry Editor or Excellon Editor).
+- added ability to be detached for the tabs in the Notebook section (Project, Selected and Tool)
+- added ability for all detachable tabs to be restored to the same position from where they were detached.
+- changed the shortcut keys for Zoom In, Zoom Out and Zoom Fit from 1, 2, 3 to '-', '=' respectively 'V'. Added new shortcut keys '1', '2', '3' for Select Project Tab, Select Selected Tab and Select Tool Tab.
+- formatted the Shortcut List Tab into a HTML table
+
+3.3.2019
+
+- updated the new shortcut list with the shortcuts added lately
+- now the special messages in the Shell are color coded according to the level. Before they all were RED. Now the WARNINGS are yellow, ERRORS are red and SUCCESS is a dark green. Also the level is in CAPS LOCK to make them more obvious
+- some more changes to GUI interface (solved issues)
+- added some status bar messages in the Geometry Editor to guide the user when using the Geometry Tools
+- now the '`' shortcut key that shows the 'shortcut key list' in Editors points to the same window which is created in a tab no longer as a pop-up window. This tab can be detached if needed.
+- added a remove_tools() function before install_tools() in the init_tools() that is called when creating a new project. Should solve the issue with having double menu entry's in the TOOLS menu
+- fixed remove_tools() so the Tcl Shell action is readded to the Tools menu and reconnected to it's slot function
+- added an automatic name on each save operation based on the object name and/or the current date
+- added more information's for the statistics
+
+2.2.2019
+
+- code cleanup in Tools
+- some GUI structure optimization's
+- added protection against entering float numbers with comma separator instead of decimal dot separator in key points of FlatCAM (not everywhere)
+- added a choice of plotting the kind of geometry for the CNC plot (all, travel and cut kind of geometries) in CNCJob Selected Tab
+- added a new preprocessor file named: 'probe_from_zmove' which allow probing to be done from z_move position on toolchange event 
+- fixed the snap magnet button in Geometry Editor, restored the checkable property to True
+- some more changes in the Editors GUI in deactivate() function
+- a fix for saving as empty an edited new and empty Excellon Object
+
+1.02.2019
+
+- fixed preprocessor files so now the bounds values are right aligned (assuming max string length of 9 chars which means 4 digits and 4 decimals)
+- corrected small type in list_sys Tcl command; added a protection of the Plot Area Tab after a successful edit.
+- remade the way FlatCAM saves the GUI position data from a file (previously) to use PyQt QSettings
+- added a 'theme' combo selection in Edit -> Preferences. Two themes are available: standard and compact.
+- some code cleanup
+- fixed a source of possible errors in DetachableTab Widget.
+- fixed gcode conversion/scale (on units change) when multiple values are found on each line
+- replaced the pop-up window for the shortcut list with a new detachable tab
+- removed the pop-up messages from the rotate, skew, flip commands
+
+31.01.2019
+
+- added a parameter ('Fast plunge' in Edit -> Preferences -> Geometry Options and Excellon Options) to control if the fast move to Z_move is done or not
+- added new function to toggle fullscreen status in Menu -> View -> Toggle Full Screen. Shortcut key: Alt+F10
+- added key shortcuts for Enable Plots, Disable Plots and Disable other plots functions (Alt+1, Alt+2, Alt+3)
+- hidden the snap magnet entry and snap magnet toggle from the main view; they are now active only in Editor Mode
+- updated the camlib.CNCJob.scale() function so now the GCode is scaled also (quite a HACK :( it will need to be replaced at some point)). Units change work now on the GCODE also.
+- added the bounds coordinates to the GCODE header
+- FlatCAM saves now to a file in self.data_path the toolbar positions and the position of TCL Shell
+- Plot Area Tab view can now be toggled, added entry in View Menu and shortcut key Ctrl+F10
+- All the tabs in the GUI right side are (Plot Are, Preferences etc) are now detachable to a separate windows which when closed it returns in the previous location in the toolbar. Those detached tabs can be also reattached by drag and drop.
+
+30.01.2019
+
+- added a space before Y coordinate in end_code() function in some of the preprocessor files
+- added in Calculators Tool an Electroplating Calculator.
+- remade the App Menu for Editors: now they will be showed only when the respective Editor is active and hidden when the Editor is closed.
+- added a traceback report in the TCL Shell for the errors that don't allow creation of an object; useful to trace exceptions/errors
+- in case that the Toolchange X,Y parameter in Selected (or in Preferences) are deleted then the app will still do the job using the current coordinates for toolchange
+- fixed an issue in camlib.CNCJob where tha variable self.toolchange_xy was used for 2 different purposes which created loss of information.
+- fixed unit conversion functions in case the toolchange_xy parameter is None
+- more fixes in camlib.CNCJob regarding usage of toolchange (in case it is None)
+- fixed preprocessor files to work with toolchange_xy parameter value = None (no values in Edit - Preferences fields)
+- fixed Tcl commands CncJob and DrillCncJob to work with toolchange
+- added to the preprocessor files the command after toolchange to go with G00 (fastest) to "Z Move" value of Z pozition.
+
+29.01.2019
+
+- fixed issue in Tool Calculators when a float value was entered starting only with the dot.
+- added protection for entering incorrect values in Offset and Scale fields for Gerber and Geometry objects (in Selected Tab)
+- added more shortcut keys in the Geometry Editor and in Excellon Editor; activated also the zoom (fit, in, out) shortcut keys ('1' , '2', '3') for the editors
+- disabled the context menu in tools table on Paint Tool in case that the painting method is single.
+- added protection when trying to do Intersection in Geometry Editor without having selected Geometry items.
+- fixed the scale, mirror, rotate, skew functions to work with Geometry Objects of multi-geometry type.
+- added a GUI for Excellon Search time for OR-TOOLS path optimization in Edit -> Preferences -> Excellon General -> Optimization Time
+- more changes in Edit -> Preferences -> Geometry, Gerber and in CNCJob
+- added new option for Cutout Tool Freeform Gaps in Edit -> Preferences -> Tools
+- fixed Freeform Cutout gaps issue (it was double than the value set)
+- added protection so the Cutout (either Freeform or Rectangular) cannot be done on a multigeo Geometry
+- added 2Sided Tool default values in Edit -> Preferences -> Tools
+- optimized the FlatCAMCNCJob.on_plot_cb_click_table() plot function and solved a bug regarding having tools numbers not in sync with the cnc tool table
+
+28.01.2018
+
+- fixed the GerberObject.merge() function
+- added a new menu entry for the Gerber Join function: Edit -> Conversions -> "Join Gerber(s) to Gerber" allowing joining Gerber objects into a final Gerber object
+- moved Paint Tool defaults from Geometry section to the Tools section in Edit -> Preferences
+- added key shortcuts for Open Manual = F1 and for Open Online VideoHelp = F2
+
+27.01.2018
+
+- added more key shortcuts into the application; they are now displayed in the GUI menu's
+- reorganized the Edit -> Preferences -> Global
+- redesigned the messagebox that is showed when quiting ot creating a New Project: now it has an option ('Cancel') to abort the process returning to the app
+- added options for trace segmentation that can be useful for auto-levelling (code snippet from Lei Zheng from a rejected pull request on FlatCAM https://bitbucket.org/realthunder/ )
+- added shortcut key 'L' for creating 'New Excellon' 
+- added shortcut key combo 'Shift+S' for Running a Script.
+- modified GRBL_laser preprocessor file so it includes a Sxxxx command on the line with M03 (laser active) whenever a value is enter in the Spindlespeed entry field
+- remade the EDIT -> PREFERENCES window, the Excellon and Gerber sections. Created a new section named TOOLS
+
+26.01.2019
+
+- fixed grbl_11 preprocessor in linear_code() function
+- added icons to the Project Tab context menu
+- added new entries to the Canvas context menu (Copy, Delete, Edit/Save, Move, New Excellon, New Geometry, New Project)
+- fixed GRBL_laser preprocessor file
+- updated function for copy of an Excellon object for the case when the object has slots
+- updated ExcellonObject.merge() function to work in case some (or all) of the merged objects have slots  
+
+25.01.2019
+
+- deleted junk folders
+- remade the Panelize Tool: now it is much faster, it is multi-threaded, it works with multitool geometries and it works with multigeo geometries too.
+- made sure to copy the options attribute to the final object in the case of: GeometryObject.merge(), GerberObject.merge() and for the Panelize Tool
+- modified the panelize TclCommand to take advantage of the new panelize() function; added a 'threaded' parameter (default value is 1) which controls the execution of the panelize TclCommand: threaded or non-threaded
+- fixed TclCommand Cutout
+- added a new TclCommand named CutoutAny. Keyword: cutout_any
+
+24.01.2019
+
+- trying to fix painting single when the actual painted object it's a MultiPolygon
+- fixed the Copy Object function when the object is Gerber
+- added the Copy entry to the Project context menu
+- made the functions behind Disable and Enable project context menu entries, non-threaded to fix a possible issue
+- added multiple object selection on Open ... and Import ... (idea and code snippet came from Travers Carter, BitBucket user https://bitbucket.org/travc/)
+- fixed 'GRBL_laser' preprocessor bugs (missing functions)
+- fixed display geometry for 'GRBL_laser' preprocessor
+- Excellon Editor - added possibility to create an linear drill array rotated at an custom angle
+- added the Edit and Properties entries to the Project context menu
+
+23.01.2019
+
+- added a new preprocessor file named 'line_xyz' which have x, y, z values on the same GCode line
+- fixed calculation of total path for Excellon Gcode file
+- modified the way FlatCAM preferences are saved. Now they can be saved as new files with .FlatConfig extension by the user and shared.
+- added possibility to open the folder where FlatCAM is saving the preferences files
+
+21.01.2019
+
+- changed some tooltips
+- added tooltips in Excellon tool table headers
+- in Excellon Tool Table the columns are now only selectable by clicking on the header (sorting is done automatically)
+- if CNCJob from Excellon then hide the CNC tools table in CNCJob Object
+
+ 
+20.01.2019
+
+- fixed the HPGL code geometry rendering when travel
+- fixed the message box layout when asking to save the current work
+- made sure that whenever the HPGL preprocessor is selected the Toolchange is always ON and the MultiDepth is OFF
+- the HPGL preprocessor entry is not allowed in Excellon Object preprocessor selection combobox as it is only applicable for Geometry
+- when saving HPGL code it will be saved as a file with extension .plt
+- the units mentioned in HPGL format are only METRIC therefore if FlatCAM units are in INCH they will be transform to METRIC
+- the minimum unit in HPGL is 0.025mm therefore the coordinates are rounded to a multiple of 0.025mm
+- removed the raise statement in do_worker_task() function as this is fatal in conjunction with PyQt5
+- added a try - except clause for the situations when for a font can't be determined the family and name
+- moved font parsing to the Geometry Editor: it is done everytime the Text tool is invoked
+- made sure that the HPGL preprocessor is not populated in the Excellon preprocessors in Preferences as it make no sense (HPGL is useful only for Geometries)
+
+19.01.2019
+
+- added initial implementation of HPGL preprocessor
+- fixed display HPGL code geometry on canvas
+
+11.01.2019
+
+- added a status message for font parsing
+
+9.01.2019
+
+- added a fix to allow creating of Excellon geometry even when there are points with no tools by skipping those points and warning the user about this in a Tcl message
+- added a message box asking users if they want to save the project in case that either New Project menu entry is clicked or if Exit menu entry is clicked or if the app is closed from the close button. The message box will be showed only if there are objects in the collection.
+- modified the first line in the Gcode header to show the FlatCAM version and version_date
+
+8.01.2019
+
+- added checkboxes in Preferences -> General -> Global Preferences to switch on/off version check at application startup and also to control if the app will send anonymous statistics about FlatCAM usage to help improve FlatCAM
+
+7.01.2019
+
+- added tooltips in Edit->Convert menu
+- fixed cutting from copper features when doing Gerber isolation with multiple passes
+
+6.01.2019
+
+- fixed the Marlin preprocessor detection in GCode header
+- the version date in GCode header is now the one set in FlatCAMApp.App.version_date
+- fixed bug in preprocessor files: number of drills is now calculated only for the Excellon objects in toolchange function (only Excellon objects have drills) 
+
+5.01.2019
+
+- fixed cncjob TclCommand - it used the default values for parameters
+- fixed the layout in ToolTransform
+- fixed the initial text in the ToolShell
+- reactivated the version check in case the release is not BETA; FlatCAMApp.App has now a beta object that when set True the application will show in the Title and help-> About that is Beta (and it disable version checking)
+- added a new name (mine: for good and/or bad) to the contributors list
+- fixed the Join function to work on Gerber and Excellon, Gerber and Gerber, Excellon and Excelon combination of objects. The merged property is the solid_geometry and the result is a GeometryObject object.
+
+3.01.2019
+
+- initial merge into FlatCAM regular
+
+28.12.2018
+
+- changed the workspace drawing from 'gl' to 'agg'. 'gl' has better performance but it messes with the overlapping graphics
+- removed the initial obj.build_ui() in App.editor2object()
+
+25.12.2018
+
+- fixed bugs in Excellon Editor due of PyQt5 port
+- fixed bug when loading Gerber with follow
+- fixed bug that when a Gerber was loaded with -follow parameter it could not be isolated external and full
+- changed multiple status bar messages
+- changed some assertions to (status error message + return) combo
+- fixed issues in 32bit installers
+- added protection against using Excellon joining on different kind of objects
+- fixed bug in ToolCutout where the Rectangular Cutout used the Type of Gaps from Freeform Cutout
+- fixed bug that didn't allowed saving SVG file from a Gerber file
+- modified setup_ubuntu.sh file for PyQt5 packages
+
+23.12.2018
+
+- added move (as in Tool Move) capability for CNCJob object and the GCode is updated on each move --> finished both for Gcode loaded and for CNCJob generated in the app
+- fixed some errors related to DialogOpen widget that I've missed in PyQt5 porting
+- added a bounds() method for CNCJob class in camlib (perhaps overdone as it worked well with the one inherited)
+- small changes in Paint Tool - the rest machining is working only partially
+- added more columns in CNCjob Tool Table showing more info about the present tools
+- make the columns in CNCJob Tool Table not editable as it has no sense
+
+22.12.2018
+
+- fixed issues in Transform Tool regarding the message boxes
+- fixed more error in Double Sided Tool and added some more information's in ToolTips
+- added more information's in CutOut Tool ToolTips
+- updated the tooltips in amost all FlatCAM tools; in Tool Tables added column header ToolTips
+- fixed NCC rest machining in NCC Tool; added status message and stop object creation if there is no geometry on any tool
+- fixed version number: now it will made of a number in format main_version.secondary_version/working_version
+- modified the makefile for windows builds to accommodate both 32bit and 64bit executable generation
+
+21.12.2018
+
+- added shortcut "SHIFT + W" for workspace toggle
+- updated the list of shortcuts
+- forbid editing for the MultiGeo type of Geometry because the Geometry Editor is not prepared for this
+- finished a "sort" of rest-machining for Non Copper Clearing Tool but it's time consuming operation
+- reworked the NCC Tool as it was fundamental wrong - still has issues on the rest machining
+- added a parameter reset for each run of Paint Tool and NCC Tool
+
+20.12.2018
+
+- porting application to PyQt5
+- adjusted the level of many status bar messages
+- created new bounds() methods for Excellon and Gerber objects as the one inherited from Geometry failed in conjunction with PyQt5
+- fixed some small bugs where a string was divided by a float finally casting the result to an integer
+- removed the 'raise' conditions everywhere I could and make protections against loading files in the wrong place
+- fixed a "PyCharm stupid paste on the previous tab level even after one free line " in Excellon.bounds()
+- in Geometry object fixed error in tool_delete regarding deletion while iterating a dict
+- started to rework the NCC Tool to generate one file only
+- in Geometry Tool Table added checkboxes for individual plot of tools in case of MultiGeo Geometry
+- rework of NCC Tool UI
+- added a automatic selector: if the system is 32bit the OR-tools imports are not done and the OR-tools drill path optimizations are replaced by a default Travelling Salesman drill path optimization
+- created a Win32 make file to generate a Win32 executable
+- disabled the Plot column in Geometry Tool Table when the geometry is SingleGeo as it is not needed
+- solved a issue when doing isolation, if the solid_geometry is not a list will make it a list
+- added tooltips in the Geometry Tool Table headers explaining each column
+- added a new Tcl Command: clear. It clears the Tcl Shell of all text and restore it to the original state
+- fixed Properties Tool area calculation; added status bar messages if there is no object selected show an error and successful showing properties is confirmed in status bar
+- when Preferences are saved, now the default values are instantly propagated within the application
+- when a geometry is MultiGeo and all the tools are deleted, it will have no geometry at all therefore all that it's plotted on canvas that used to belong to it has to be deleted and because now it is an empty object we demote it to SingleGeo so it can be edited
+
+19.12.2018
+
+- fixed SVG_export for MultiGeo Geometries
+- fixed DXF_export for MultiGeo Geometries
+- fixed SingleGeo to MultiGeo conversion plotting bug
+
+18.12.2018
+
+- small changes in GeometryObject.plot()
+- updated the GeometryObject.merge() function and the Join Geometry feature to accommodate the different types of geometries: singlegeo and multigeo type
+- added Conversion submenu in Edit where I moved the Join features and added the Convert from MultiGeo to SingleGeo type and the reverse
+- added Copy Tool (on a selection of tools) feature in Geometry Object UI 
+- fixed the bounds() method for the MultiGeo geometry object so the canvas selection is working and also the Properties Tool
+- fixed Move Tool to support MultiGeo geometry objects moving
+- added tool edit in Geometry Object Tool Table
+- added Tool Table context menu in Geometry Object and in Paint Tool
+- modified some Status Bar messages in Geometry Object
+
+17.12.2018
+
+- added support for multiple solid_geometry in a geometry object; each tool can now have it's own geometry. Plot, project save/load are OK.
+- added support for single GCode file generation from multi-tool PaintTool job
+- added protection for results of Paint Tool job that do not have geometry at all. An Error will be issued. It can happen if the combination of Paint parameters is not good enough
+- solved a small bug that didn't allow the Paint Job to be done with lines when the results were geometries not iterable 
+- added protection for the case when trying to run the cncjob Tcl Command on a Geometry object that do not have solid geometry or one that is multi-tool
+- Paint Tool Table: now it is possible to edit a tool to a new diameter and then edit another tool to the former diameter of the first edited tool
+- added a new type of warning, [WARNING_NOTCL]
+- fixed conflict with "space" keyboard shortcut for CNC job
+
+16.12.2018
+
+- redone the Options menu; removed the Transfer Options as they were not used
+- deleted some folders in the project structure that were never used
+- Paint polygon Single works only for left mouse click allowing mouse panning
+- added ability to print errors in status bar without raising Tcl Shell
+- fixed small bug: when doing interiors isolation on a Gerber that don't allow it, no object is created now and an error in the status bar is issued
+- fixed bug in Paint All for Geometry made from exteriors Gerber isolation
+- fixed the join geometry: when the geometries has different tools the join will fail with a status bar message (as it should). Allow joining of geometries that have no tool. // Reverted on 18.12.2018
+- changed the error messages that are simple to the kind that do not open the TCl shell
+- fixed some issues in Geometry Object
+- Paint Tool - reworked the UI and made it compatible with the Geometry Object UI
+- Paint Tool - tool edit functional
+- added Clear action in the Context menu of the TCl Shell
+
+14.12.2018
+
+- fixed typo in setup_ubuntu.sh
+- minor changes in Excellon Object UI
+- added Tool Table in Paint Tool
+- now in Paint Tool and Non Copper Clearing Tool a selection of tools can be deleted (not only one by one)
+- minor GUI changes (added/corrected tooltips)
+- optimized vispy startup time from about >6 sec to ~3 seconds
+- removed vispy text collection starting in plotcanvas as it did nothing // RESTORED 18.12.2018 as it messed the graphical presentation
+- fixed cncjob TclCommand for the new type of Geometry
+- make sure that when using the TclCommands, the object names are case insensitive
+- updated the TCL Shell auto-complete function; now it will index also the names of objects created or loaded in the application
+- on object removal the name is removed from the Shell auto-complete model
+
+13.12.2018
+
+NEW Geometry Object and CNC Object architecture (3rd attempt) which allow multiple tools for one geometry
+
+- fixed issue with cumulative G-code after successive delete/create of a CNCJob on the same geometry (some references were kept after deletion of CNCJob object which kept the deleted tools data and added it to a new one)
+- fixed plot and export G-code in new format
+- fixed project save/load in the new format for geometry
+- added new feature in CNCJob Object UI: since we may have multiple tools per CNCJob object due of having multiple tool in Geometry Object,
+now there is a Tool Table in CNC Object UI and each tool GCode can be enabled or disabled
+
+12.12.2018
+
+- Geometry Tool Table: when the Offset type is 'custom' each tool it's storing the value and it is updated on UI when that tool is selected in UI table
+- Geometry Tool Table: fixed tool offset conversion when the Offset in Tool Table UI is set to Custom
+
+11.12.2018
+
+- cleaned up the generatecncjob() function in FlatCAMObj
+- created a new function for generating cncjob out of multitool geometry, mtool_generate_cncjob()
+- cleaned up the generate_from_geometry_2() method in camlib
+- Geometry Tool Table: new tool added copy all the form fields (data) from the last tool
+- finished work on generation of a single CNC Job file (therefore a single GCODE file) even for multiple tools in Geo Tool Table
+- GCode header is added only on saving the file therefore the time generation will be reflected in the file
+- modified preprocessors to accommodate the new CNC Job file with multiple tools
+- modified preprocessors so the last X,Y move will be to the toolchange X,Y pos (set in Preferences)
+- save_project and load_project now work with the new type of multitool geometry and cncjob objects
+
+10.12.2018
+
+- added new feature in Geometry Tool Table: if the Offset type in tool table is 'Offset' then a new entry is unhidden and the user can use custom offset
+- Geometry Tool Table: fixed add new tool with diameter with many decimals
+- Geometry Tool Table: when editing the tip dia or tip angle for the V Shape tool, the CutZ is automatically calculated
+
+9.12.2018
+
+- new Geometry Tool Table has functional unit conversion
+- when entering a float number in Spindle Speed now there is no error and only the integer part is used, the decimals are discarded
+- finished the Geometry Tool Table in the form that generates only multiple files
+- if tool type is V-Shape ('V') then the Cut Z entry is disabled and new 'Tip Dia' and 'Tip Angle' fields are showed. The values entered will calculate the Cut Z parameter
+
+5.12.2018
+
+- remade the Geometry Tool Table, before this change each tool could not store it's own set of data in case of multiple tools with same diameter
+- added a new column in Geo Tool Table where to specify which type of tool to use: C for circular, B for Ball and V for V-shape
+
+4.12.2018
+
+- new geometry/excellon object name is now only "new_g"/"new_e" as the type is clear from the category is into (and the associated icon)
+- always autoselect the first tool in the Geometry Tool table
+- issue error message if the user is trying to generate CNCJob without a tool selected in Geometry Tool Table
+- add the whole data from Geometry Object GUI as dict in the geometry tool dict so each tool (file) will have it's own set of data
+
+3.12.2018
+
+- Geometry Tool table: delete multiple tools with same diameter = DONE
+- Geometry Tool table: possibility to cut a path inside or outside or on path = DONE
+- Geometry Tool table: fixed situation when user tries to add a tool but there is no tool diameter entered
+- if a geometry is a closed shape then create a Polygon out of it
+- some fixes in Non Copper Clearing Tool
+- Geometry Tool table: added option to delete_tool function for delete_all
+- Geometry Tool table: added ability to delete even the last tool in tool_table and added an warning if the user try to generate a CNC Job without a tool in tool table
+- if a geometry is painted inside the Geometry Editor then it will store the tool diameter used for this painting. Only one tool cn be stored (the last one) so if multiple paintings are done with different tools in the same geometry it will store only the last used tool.
+- if multiple geometries have different tool diameters associated (contain a paint geometry) they aren't allowed to be joined and a message is displayed letting the user know
+
+2.12.2018
+
+- started to work on a geometry Tool Table
+- renamed FlatCAMShell as ToolShell and moved it (and termwidget) to flatcamTools folder
+- cleaned up the ToolShell by removing the termwidget separate file and added those classes to ToolShell
+- added autocomplete for TCL Shell - the autocomplete key is 'TAB'
+- covered some possible exceptions in rotate/skew/mirror functions
+- Geometry Tool table: add/delete tools = DONE
+- Geometry Tool table: add multiple tools with same diameter = DONE
+
+1.12.2018
+
+- fixed Gerber parser so now the Gerber regions that have D02 operation code just before the end of the region will be processed correctly. Autotrax Dex Gerbers are now loaded
+- fixed an issue with temporary geo storage "geo" being referenced before assignment
+- moved all FlatCAM Tools into a single directory
+
+30.11.2018
+
+- remade the CutOut Tool. I've put together the former Freeform Cutout tool and the Cutout Object fount in Gerber Object GUI and left only a link in the Gerber Object GUI. This tidy the GUI a bit.
+- created a Paint Tool and replaced the Paint Area section in Geometry Object GUI with a link to this tool.
+- fixed bug in former Paint Area and in the new Paint Tool that made the paint method not to be saved in App preferences
+- solved a bug in Gerber parser: in case that an operation code D? was encountered alone it was not remembered - fixed
+- fixed bug related to the newly entered toolchange feature for Geometry: it was trying to evaluate toolchange_z as a comma separated value like for toolchange x,y
+- fixed bug in scaling units in CNC Job which made the unit change between INCH and MM not possible if a CNC Job was present in the project objects
+
+29.11.2018
+
+- added checks for using a Z Cut with positive value. The Z Cut parameter has to be negative so if the app will detect a positive value it will automatically convert it to negative
+- started to implement rest-machining for Non Copper clearing Tool - for now the results are not great
+- added Toolchange X,Y position parameters and modified the default and manual_toolchange preprocessor file to use them
+For now they are used only for Excellon objects who do have toolchange events
+- added Toolchange event selection for Geometry objects; for now it is as before, single tool on each file
+- remade the GUI for objects and in Preferences to have uniformity
+- fixed bug: after editing a newly created excellon/geometry object the object UI used to not keep the original settings
+- fixed some bugs in Tool Add feature of the new Non Copper Clear Tool
+- added some messages in the Non Copper Clear Tool
+- added parameters for coordinates no of decimals and for feedrate no of decimals used in the resulting GCODE. They are in EDIT -> Preferences -> CNC Job Options
+- modified the preprocessors to use the "decimals" parameters
+
+28.11.2018
+
+- added different methods of copper clearing (standard, seed, line_based) and "connect", "contour" options found in Paint function
+- remake of the non-copper clearing tool as a separate tool
+- modified the "About" menu entry to mention the main contributors to FlatCAM 3000 
+- modified Marlin preprocessor according to modifications made by @redbull0174 user from FlatCAM.org forum
+- modified Move Tool so it will detect if there is no object to move and issue a message
+
+27.11.2018
+
+- fixed bug in isolation with multiple passes
+- cosmetic changes in Buffer and Paint tool from Geometry Editor
+- changed the way selection box is working in Geometry Editor; now cumulative selection is done with modifier key (SHIFT or CONTROL) - before it was done by default
+- changed the default value for CNCJob tooldia to 1mm
+
+25.11.2018
+
+- each Tool change the name of the Tools tab to it's name
+- all open objects are no longer autoselected upon creation. Only on new Geometry/Excellon object creation it will be autoselected
+
+24.11.2018
+
+- restored the selection method in Geometry Editor to the original one found in FlatCAM 8.5
+- minor changes in Clear Copper function
+- minor changes in some preprocessors
+- change Join Geometry menu entry to Join Geo/Gerber
+- added menu entry for Toggle Axis in Menu -> View
+- added menu entry for Toggle Workspace in Menu -> View
+- added Bounding box area to the Properties (when metric units, in cm2)
+- non-copper clearing function optimization
+- fixed Z_toolchange value in the GCODE header
+
+21.11.2018
+
+- not very precise jump to location function
+- added shortcut key for jump to coordinates (J) and for Tool Transform (T)
+- some work in shortcut key
+
+19.11.2018
+
+- fixed issue with nested comment in preprocessors
+- fixed issue in Paint All; reverted changes
+
+18.11.2018
+
+- renamed FlatCAM 2018 to FlatCAM 3000
+- added new entries in the Help menu; one will show shortcut list and the other will start a YouTube webpage with a playlist where I will publish future training videos for this version of FlatCAM
+- if a Gerber region has issues the file will be loaded bypassing the error but there will be a TCL message letting the user know that there are parser errors. 
+
+17.11.2018
+
+- added Excellon parser support for units defined outside header
+
+
+12.11.2018
+
+- fixed bug in Paint Single Polygon
+- added spindle speed in laser preprocessor
+- added Z start move parameter. It controls the height at which the tool travel on the fist move in the job. Leave it blank if you don't need it.
+
+9.11.2018
+
+- fixed a reported bug generated by a typo for feedrate_z object in camlib.py. Because of that, the project could not be saved.
+- fixed a G01 usage (should be G1) in Marlin preprocessor.
+- changed the position of the Tool Dia entry in the Object UI and in MainGUI
+- fixed issues in the installer
+
+30.10.2018
+
+- fixed a bug in Freeform Cutout Tool - it was missing a change in the name of an object
+
+29.10.2018
+
+- added Excellon export menu entry and functionality that can export in fixed format 2:4 LZ INCH (format that Altium can load and it is a more generic format).
+It will be usefull for those who need FlatCAM to only convert the Excellon to a more useful format and visualize Gerbers.
+The other Excellon Export menu entry is exporting in units either Metric or INCH depending on the current units in FlatCAM, but it will always use the decimal format which may not be loaded in all cases.
+- disabled the Selected Tab while in Geometry Editor; the user is not supposed to have access to those functions while in Geometry Editor
+- added an menu entry in Menu -> File -> Recent Files named Clear Recent files which does exactly that
+- fixed issue: when a New Project is created but there is a Geometry still in Geometry Editor (or Excellon Editor) not saved, now that geometry is deleted
+- fixed problem when doing Clear Copper with Cut over 1st point option active. When the shape is not closed then it may cut over copper features. Originally the feature was meant to be used only with isolation geometry which is closed. Fixed
+
+28.10.2018
+
+- fixed Excellon Editor shortcut messages; also fixed differences in messages between usage by shortcuts and usage by menu toolbar actions
+- fixed Excellon Editor bug: it was triggering exceptions when the user selected a tool in tooltable and then tried to add a drill (or array) by clicking on canvas
+Clicking on canvas by default clear all the used tools, therefore the action could not be done. Fixed.
+- fixed bug Excellon Editor: when all the drills from a tool are resized, after resize they can't be selected.
+- Excellon Editor: added ability to delete multiple tools at once by doing multiple selection on the tooltable
+- Excellon Editor: if there are no more drills to a tool after doing drills resize then delete that tool from the tooltable
+- Excellon Editor: always select the last tool added to the tooltable
+- Excellon Editor: added a small canvas context menu for Excellon Editor
+
+27.10.2018
+
+- added a Paint tool toolbar icon and added shortcut key 'I' for Paint Tool
+- fixed unreliable multiple selection in Geometry Editor; some clicks were not registered
+- added utility geometry for Add Drill Array in Excellon Editor
+- fixed bug Excellon Editor: drills in drill array start now from the array start point (x, y); previously array start point was used only for calculating the radius
+- fixed bug Excellon Editor: Measurement Tool was not acting correctly in Exc Editor regarding connect/disconnect of events
+- in Excellon Editor every time a tool is clicked (except Select which is the default) the focus will return to Selected tab
+- added protection in Excellon Editor: if there is no tool/drill selected no operation over drills can be performed and a status bar message will be displayed
+- Excellon Editor: added relevant messages for all actions
+- fixed bug Excellon Editor: multiple selection with key modifier pressed (CTRL/SHIFT) either by simple click or through selection box is now working
+- fixed dwell parameter for Excellon in Preferences to be default Off
+
+26.10.2018
+
+- when objects are disabled they can't be selected
+- added Feedrate_z (Plunge) parameter for Geometry Object
+- fixed bug in units convert for Geometry Tab; added some missing parameters to the conversion list
+- fixed bug in isolation Geometry when the isolated Gerber was a single Polygon
+- updated the Paint function in Geometry Editor
+
+25.10.2018
+
+- added a verification on project saving to make sure that the project was saved successfully. If not, a message will be displayed in the status bar saying so.
+
+20.10.2018
+
+- fixed the SVG import as Gerber. But unfortunately, when there is a ground pour in a imported PCB SVG, the ground pour will be isolated inside
+instead to be isolated outside like every other feature. That's no way around this. The end result will be thinner features
+for the ground pour and if one is relying on those thin connections as GND links then it will not work as intended ,they may be broken.
+Of course one can edit the isolation geometry and delete the isolation for the ground pour.
+- delete selection shapes on double clicking on object as we may not want to have selection shape while Selected tab is active
+
+19.10.2018
+
+- solved some value update bugs in tool_table in Excellon Editor when editing tools followed by deleting another tool,
+and then re-adding the just-deleted tool.
+- added support for chaining blocks in DXF Import
+- fixed the DXF arc import
+- added support for a type of Gerber files generated by OrCAD where the Number format is combined with G74 on the same line
+- in Geometry Editor added the possibility for buffer to use different kinds of corners
+- added protection against loading an GCODE file as Excellon through drag & drop on canvas or file open dialog
+- added shortcut key 'B' for buffer operation inside Geometry Editor
+- added shell message in case the Font used in Text Tool in Geometry editor is not supported. Only Regular, Bold, Italic adn BoldItalic are supported as of yet.
+- added shortcut key 'T' for Text Tool inside Geometry Editor
+- added possibility for Drag & Drop on FlatCAM GUI with multiple files at once 
+
+18.10.2018
+
+- fixed DXF arc import in case of extrusion enabled
+- added on Geo Editor Toolbar the button for Buffer Geometry; added the possibility to create exterior and interior buffer
+- fixed a numpy import error
+
+17.10.2018
+
+- added Spline support and Ellipse (chord) support in DXF Import: chord might have issues
+(borrowed from the work of Vasilis Vlachoudis, https://github.com/vlachoudis/bCNC)
+- added Block support in DXF Import - no support yet for chained blocks (INSERT in block)
+- support for repasted block insertions
+
+16.10.2018
+
+- added persistent toolbar view: the enabled toolbars will be active at the next app startup while those that are not enabled will not be
+enabled at the next app startup. To enable/disable toolbars right click on the toolbar.
+
+15.10.2018
+
+- DXF Export works now also for Exteriors only and Interiors only geometry generated from Gerber Object
+- when a Geometry is edited, now the interiors and exterior of a Polygon that is part of the Geometry can be selected individually. In practice, if
+doing full isolation geometry, now both external and internal trace can be selected individually.
+
+13.10.2018
+
+- solved issue in CNC Code Editor: it appended text to the previous one even if the CNC Code Editor was closed
+- added .GBD Gerber extension to the lists
+- added support for closed polylines/lwpolylines in Import DXF; now PCB patterns found in PDF format can be imported in INKSCAPE
+and saved as DXF. FlatCAM can import DXF as Gerber and the user now can do isolation on it.
+
+12.10.2018
+
+- added zoom in, zoom out and zoom fit buttons on the View toolbar
+- fixed bug that on Double Sided Tool when a Excellon Alignment is created does not reset the list of Alignment drills
+- added a message warning the user to add Point coordinates in case the reference used in Double Sided Tool is Point
+- added new feature: DXF Export for Geometry
+
+10.10.2018
+
+- fixed a small bug in Setup Recent Files
+- small fix in Freeform Cutout Tool regarding objects populating the combo boxes
+- Excellon object name will reflect the number of edits performed on it
+
+9.10.2018
+
+- In Geometry Editor, now Path and Polygon draw mode can be finished not only with shortcut key Enter but also with right click on canvas
+- fixes regarding of circle linear approximation - final touch
+- fix for interference between Geo Editor and Excellon Editor
+- fixed Cut action in Geometry Editor so it can now be done multiple times on the target geometry without need for saving in between.
+- initial work on DXF import; made the GUI interface and functional structure
+- added import functions for DXF import
+- finished DXF Import (no blocks support, no SPLINE support for now)
+
+8.10.2018
+
+- completed toggle canvas selection when there is only one object under click position for the case when clicking the object is done
+while other object is already selected.
+- added static utility geometry just upon activating an Editor function
+- changed the way the canvas is showed on FlatCAM startup
+
+7.10.2018
+
+- solved mouse click not setting relative measurement origin to zero
+- solved bug that always added one drill when copying a selection of drills in the EXCELLON EDITOR
+- solved bug that the number of copied drills in Excellon Editor was not updated in the tool table
+- work in the Excellon Editor: found useful to change the diameter of one tool to another already in the list;
+could help for all those tools that are a fraction difference that comes from imperial to mm (or reverse) conversion,
+to reduce the tool changes - Done
+- in Excellon Editor, always auto-select the last tool added
+- in Excellon Editor fixed shortcuts for drill add and drill_array add: they were reversed. Now key 'A' is for array add
+and key 'D' is for drill add
+- solved a small bug in Excellon export: even when there were no slots in the file, it always added the tools list that
+acted as unnecessary toolchanges
+- after Move action, all objects are deselected
+
+
+6.10.2018
+
+- Added basic support for SVG text in SVG import. Will not work if some letters in a word have different style (italic bold or both)
+- added toggle selection to the canvas selection if there is only one object under the click position
+- added support for "repeat" command in Excellon file
+- added support for Allegro Gerber and Excellon files
+- Python 3.7 is used again; solved bug where the activity icon was not playing when FlatCAM active
+
+5.10.2018
+
+- fixed undesired setting focus to Project Tab when doing the SHIFT + LMB combo (to capture the click coordinates)
+
+4.10.2018
+
+- Excellon Editor: finished Add Drill Array - Linear type action
+- Excellon Editor: finished Add Drill Array - Circular type action
+- detected bug in shortcuts: Fixed
+- Excellon Editor: added constrain for adding circular array, if the number of drills multiplied by angle is more than 360
+the app will return with an message
+- solved sorting bug in the Excellon Editor tool table
+- solved bug in Menu -> Edit -> Sort Origin ; the selection box was not updated after offset
+- added Excellon Export in Menu -> File -> Export -> Export Excellon
+- added support to save the slots in the Excellon file in case there were some in the original file
+- fixed Double Sided Tool for the case of using the box as mirroring reference.
+
+2.10.2018
+
+- made slots persistent after edit
+- bug detected: in Excellon Editor if new tool added diameter is bigger than 10 it mess things up: SOLVED
+- Excellon Editor: finished Drill Resize action
+- after an object is deleted from the Project list, if the current tab in notebook is not Project,
+always focus in the Project Tab (deletion can be done by shortcut key also)
+- changed the initial view to include the possible enabled workspace guides
+
+1.10.2018
+
+- added GUI for Excellon Editor in the Tool Tab
+- Excellon Editor: created and populated the tool list
+- Excellon Editor: added possibility to add new tools in the list
+- Excellon Editor: added possibility to delete a tool (and the drills that it contain) by selecting a row in the tool table and 
+clicking the Delete Tool button
+- Excellon Editor: added possibility to change the tool diameter in the tool list for existing tool diameters.
+- Excellon Editor: when selecting a drill, it will highlight the tool in the Tool table
+- Excellon Editor: optimized single click selection
+- Excellon Editor: added selection for all drills with same diameter upon tool selection in tool table; fix in tool_edit
+- Excellon Editor: added constrain to selection by single click, it will select if within a certain area around the drill
+- Excellon Editor: finished Add Drill action
+- Excellon Editor: finished Move Drill action
+- Excellon Editor: finished Copy Drill action
+
+- fixed issue: when an object is selected before entering the Editor mode, now the selecting shape is deleted before entry 
+in the Editor (be it Geometry or Excellon).
+- fixed a few glitches regarding the units change
+- when an object is deselected on the Plot Area, the notebook will switch to Project Tab
+- changed the selection behavior for the dragging rectangle selection box in Editor (Geometry, Excellon): by dragging a
+selection box and selecting is cumulative: it just adds. To remove from selection press key Ctrl (or Shift depending of 
+the setting in the Preferences) and drag the rectangle across the objects you want to deselect.
+
+29.09.2018
+
+- optimized the combobox item population in Panelization Tool and in Film Tool
+- FlatCAM now remember the last path for saving files not only for opening
+- small fix in GUI
+- work on Excellon Editor. Excellon editor working functions are: loading an Excellon object into Editor, 
+saving an Excellon object from editor to FlatCAM, selecting drills by left click, selection of drills by dragging rectangle, deletion of drills.
+- fixed Excellon merge
+- added more Gcode details (depthperpass parameter in Gcode header) in preprocessors
+- deleted the Tool informations from header in preprocessors due to Mach3 not liking the lot of square brackets
+- more corrections in preprocessors
+
+
+28.09.2018
+
+- added a save_defaults() call on App exit from action on Menu -> File -> Exit
+- solved a small bug in Measurement Tool
+- disabled right mouse click functions when Measurement Tools is active so the user can do panning and find the destination point easily
+- added a new button named "Measure" in Measurement Tool that allow easy access to Measurement Tool from within the tool
+- fixed a bug in Gerber parser that when there was a rectangular aperture used within a region, some artifacts were generated.
+- some more work on Excellon Editor
+
+27.09.2018
+
+- fixed bug when creating a new project, if a previous object was selected on screen, the selection shape survived the creation of a new project
+- added compatibility with old type of FlatCAM projects
+- reverted modifications to the way that Excellon geometry was stored to the old way.
+- added exceptions for Paint functions so the user can know if something failed.
+- modified confirmation messages to use the color coded messages (error = red, success = green, warning = yellow)
+- restored activity icon
+
+26.09.2018
+
+- disabled selection of objects in Project Tab when in Editor
+- the Editor Toolbar is hidden in normal mode and it is showed when Editor is activated. I may change this behaviour back.
+- changed names in classes, functions to prepare for the Excellon editor
+
+- fixed bugs in Paint All function
+- fixed a bug in ParseSVG module in parse_svg_transform(), related to 'scale'
+
+- moved all the Editor menu/toolbar creation to FlatCAMUI where they belong
+- fixed a Gerber parse number issue when Gerber zeros are TZ (keep trailing zeros)
+
+- changed the way of how the solid_geometry for Excellon files is stored and plotted. Before everything was put in the same "container". Now, the geometries of drills and slots are organized into dictionaries having as keys the tool diameters and as values list of Shapely objects (polygons)
+- fix for Excellon plotting for newly created empty Excellon Object
+- fixed geometry.bounds() in camlib to work with the new format of the Excellon geometry (list of dicts)
+
+24.09.2018
+
+- added packages in the Requirements and setup_ubuntu.sh. Tested in Ubuntu and it's OK
+- added Replace (All) feature in the CNC Code Editor
+- made CNC Code generation for Excellon to show progress
+- added information about transforms in the object properties (like skew and how much, if it was mirrored and so on)
+- made all the transforms threaded and make them show progress in the progress bar
+- made FlatCAM project saving, threaded.
+ 
+23.09.2018
+
+- added support for "header-less" Excellon files. It seems that Mentor PADS does generate such non-standard Excellon files. The user will have to guess: units (IN/MM), type of zero suppression LZ/TZ  (leading zeros or trailing zeros are kept) and Excellon number format(digits and decimals).  All of those can be adjusted in Menu -> Edit -> Preferences -> Excellon Object -> Excellon format
+- fixed svgparse for Path. Now PCB rasted images can traced in Inkscape or PDF's can be converted and then saved as SVG files which can be imported into FlatCAM. This is a convolute way to convert a PDF to Gerber file.
+
+22.09.2018
+
+- added Drag & Drop capability. Now the user can drag and drop to FlatCAM GUI interface a file (with the right extension) that can be a FlatCAM project file (.FlatPrj) a Gerber file, an Excellon file, a G-Code file or a SVG file.
+- made the Move Tool command threaded
+- added Image import into FlatCAM
+
+21.09.2018
+
+- added new information's in the object properties: all used Tool-Table items are included in a new entry in self.options dictionary
+- modified the preprocessor files so they now include information's about how many drills (or slots) are for each tool. The Gcode will have this information displayed on the message from ToolChange.
+- removed some log.debug and add new log.debug especially for moments when some process is finished
+- fixed the utility geometry for Font geometry in Geometry Editor
+- work on selection in Geometry Editor
+- added multiple selection key as a Preference in Menu -> Edit -> Preferences It can be either Shift or Ctrl.
+- fixed bug in Gerber Object -> Copper Clearing.
+- added more comprehensive tooltips in Non-copper Clearing as advice on how to proceed.
+- adjusted make_win32.py file so it will work with Python 3.7 (cx_freeze can't copy OpenGL files, so it has to be done manually)
+
+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.
+
+- 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
+- made the workspace app default state = False
+- made the workspace to resize when units are changed
+- disabled automatic defaults save (might create SSD wear)
+- added an automatic defaults save on FlatCAM application close
+- made the draw method for the Workspace lines 'agg' so the quality of the FC objects will not be affected
+
+- added Area constrain to the Panelization Tool: if the resulting area is too big to fit within constrains, the number of columns and/or rows will be reduced to the maximum that still fits is.
+- removed the Flip command from Panelization Tools because Flipping (Mirroring) should be done properly with the Transform Tool or using the provided shortcut keys.
+
+- made Font parsing threaded so the application will not wait for the font parsing to complete therefore the app start is faster
+
+
+17.09.2018
+
+- fixed Measuring Tool not working when grid is turned OFF
+- fixed Roland MDX20 preprocessor
+- added a .GBR extension in the open_gerber filter
+- added ability to Scale and Offset (for all types of objects) to just press Enter after entering a value in the Entry just like in Tool Transform
+- added capability in Tool Transform to mirror(flip) around a certain Point. The point coordinates can either be entered by hand or they can be captured by left clicking while pressing key "SHIFT" and then clicking the Add button
+- added the .ROL extension when saving Machine Code
+- replaced strings that reference to G-Code from G-Code to CNC Code
+- added capability to open a project by serving the path/project_name.FlatPrj as a parameter to FlatCAM.py
+
+15.09.2018
+
+- removed dwell line generator and included dwell generation in the preprocessor files
+- added a proposed RML1 Roland_MDX20 preprocessor file.
+- added a limit of 15mm/sec (900mm/min) to the feedrate and to the feedrate_rapid. Anything faster than this will be capped to 900mm/min regardless what is entered in the program GUI. This is because Roland MDX-20 has a mechanical limit of the speed to 15mm/sec (900mm/min in GUI)
+
+14.09.2018
+- remade the Double Sided Tool so it now include mirroring of Excellon and Geometry Objects along Gerber. Made adding points easier by adding buttons to GUI that allow adding the coordinates captured by left mouse click + SHIFT key
+- added a few fixes in code to the other FlatCAM tools regarding reset_fields() function. The issue was present when clicking New Project entry in Menu -> File.
+- FIXED: fix adding/updating bounding box coords for the mirrored objects in Double side Tool.
+- FIXED: fix the bounding box values from within FlatCAM objects, upon units change.
+- fixed issue with running again the constructor of the drawing tools after the tool action was complete, in Geometry Editor
+- fixed issue with Tool tab not closed after Text Input tool is finished.
+- fixed issue with TEXT to GEOMETRY tool, the resulting geometry was not scaled depending of current units
+- fixed case when user is clicking on the canvas to place a Font Geometry without clicking apply button first or the Font Geometry is empty, in Geometry Editor - > Text Input tool
+- reworked Measuring Tool by adding more information's (START, STOP point coordinates) and remade the strings
+- added to Double Sided Tool the ability to use as reference box Excellon and Geometry Objects
+
+12.09.2018
+
+- fixed Excellon Object class such that Excellon files that have both drills and slots are supported
+- remade the GUI interface for the Excellon Object in a more compact way; added a column with slots numbers (if any) along the drills numbers so now there is only one tool table for drills and slots.
+- remade the GUI in Preferences and removed unwanted stretch that was broken the layout.
+- if for a certain tool, the slots number is zero it will not be displayed
+- reworked Text to Geometry feature to work in Linux and MacOS
+- remade the Text to Geometry so font collection process is done once at app start-up improving the performance
+
+
+09.09.2018
+
+- added TEXT ENTRY SUPPORT in Geometry Editor. It will convert strings of True Type Fonts to geometry. The actual dimensions are approximations because font size is in points and not in metric or inch units. For now full support is limited to Windows. In Linux/MacOS only the fonts for which the font name is the same as the font filename are supported. Italic and Bold functions may not work in Linux/MacOS.
+- solved bug: some Drawing menu entries not having connected functions
+
+28.08.2018
+
+- fixed Gerber parser so now G01 "moving" rectangular aperture is supported.
+- fixed import_svg function; it can import SVG as geometry (solved bug)
+- fixed import_svg function; it can import SVG as Gerber (it did not work previously)
+- added menu entry's for SVG import as Gerber and separated import as Geometry
+
+27.08.2018
+
+- fixed Gerber parser so now FlatCAM can load Gerber files generated by Mentor Graphics EDA programs.
+
+26.08.2018
+
+- added awareness for missing coordinates in Gerber parsing. It will try to use the previous coordinates but if there are not any those lines will be ignored and an Warning will be printed in Tcl Shell.
+- fixed TCL commands AlignDrillGrid and DrilCncJob
+- added TCL script file load_and_run support in GUI
+- made the tool_table in Excellon to automatically adjust the table height depending on the number of rows such that all the rows will be displayed.
+- structural changes in the Excellon build_ui()
+- icon changes and menu compress
+
+23.08.2018
+
+- added Excellon routing support
+- solved a small bug that crippled Excellon slot G85 support when the coordinates are with period.
+- changed the way selection is done in Geometry Editor; now it should work in all cases (although the method used may be computationally intensive, because sometimes you have to click twice to make selection if you do it too fast)
+
+21.08.2018
+
+- added Excellon slots support when using G85 command for generation of the slots file. Inspired from the work of @mgix. Thanks. Routing format support for slots will follow. 
+- minor bug solved: option "Cut over 1st pt" now has same name both in Preferences -> Geometry Options and in Selected tab -> Geomety Object. Solves #3
+- added option to select Climb or Conventional Milling in Gerber Object options Solves #4
+- made "Combine passes" option to be saved as an app preference
+- added Generate Exteriors Geo and Generate Interiors Geo buttons in the Gerber Object properties
+- added configuration for the number of steps used for Gerber circular aperture linear approximation. The option is in Preferences -> Gerber Options
+- added configuration for the number of steps used for Gcode circular aperture linear approximation. The option is in Preferences -> CNCjob Options
+- added configuration for the number of steps used for Geometry circular aperture linear approximation. The option is in Preferences -> Geometry Options. It is used on circles/arcs made in Geometry Editor and for other types of geometries generated in the app.
+
+17.07.2018
+
+- added the required packages in Requirements.txt file
+- added required packages in setup_ubuntu.sh file
+- added color control over almost all the colors in the application; those settings are in Menu -> Edit -> Preferences -> General Tab
+- added configuration of which mouse button to be used when panning (MMB or RMB)
+- fixed bug with missing 'drillz' parameter in function generate_from_excellon_by_tool() (credits for finding it goes to Stefan Smith https://bitbucket.org/stefan064/)
+- load Factory defaults in Preferences will load the defaults that are used just after first install. Load Defaults option in Preferences will load the User saved Defaults.
+
+03.07.2018
+
+- fixed bug in rotate function that didn't update the bounding box of the modified object (rotated) due of not emitting the right signal parameter.
+- removed the Options tab from the Notebook (the left area where is located also the Project tab). Replaced it with the Preferences Tab launched with Menu -> Edit -> Preferences
+- when FlatCAM is used under MacOS, multiple selection of shapes in Editor mode is done using SHIFT key instead of CTRL key due of MacOS interpreting Ctrl+LMB_click as a RMB click
+- when in Editor, clicking not on a shape, reset the index of selected shapes to zero
+- added a new Tab in the Plot Area named Gcode Editor. It allow the user to edit the Gcode and then Save it or Print it.
+- added a fix so the 'preamble' Gcode is correctly inserted between the comments header and the actual GCODE
+- added Find function in G-Code Editor
+
+27.06.2018
+
+- the Plot Area tab is changing name to "Editor Area" when the Editor is activated and returns to the "Plot Area" name upon exiting the Editor
+- made the labels shorter in Transform Tool in anticipation of Options Tab removal from Notebook and replacing it with Preferences
+- the Excellon Editor is not finished (not even started yet) so the Plot Area title should stay "Plot Area" not change to "Editor Area" when attempting to edit an Excellon file. Solved.
+- added a header comment block in the generated Gcode with useful information's
+- fixed issue that did not allow the Nightly's to be run in Windows 7 x64. The reason was an outdated DLL file (freetype.dll) used by Vispy python module.
+
+25.06.2018
+
+- "New" menu entry in Menu -> File is renamed to "New Project"
+- on "New Project" action, all the Tools are reinitialized so the Tools tab will work as expected
+- fixed issue in Film Tool when generating black film
+- fixed Measurement Tool acquiring and releasing the mouse/key events
+- fixed cursor shape is updated on grid_toggle
+- added some infobar messages to show the user when the Editor was activated and when it was closed (control returned to App).
+- added thread usage for Film tool; now the App is no longer blocked on film generation and there is a visual clue that the App is working
+
+22.06.2018
+
+- added export PNG image functionality and menu entry in Menu -> File -> Export PNG ...
+- added a command to set focus on canvas inside the mouve move event handler; once the mouse is moved the focus is moved to canvas so the shortcuts work immediatly.
+- solved a small bug when using the 'C' key to copy name of the selected object to clipboard
+- fixed millholes() function and isolate() so now it works even when the tool diameter is the same as the hole diameter.
+
+Actually if the passed value to  the buffer() function is zero, I
+artificially add a value of 0.0000001 (FlatCAM has a precision of
+6 decimals so I use a tenth of that value as a pseudo "zero")
+because the value has to be positive. This may have solved for some use
+cases the user complaints that on clearing the areas of copper there is
+still copper leftovers.
+
+- added shortcut "Shift+G" to toggle the axis presence. Useful when one wants to save a PNG file.
+- changed color of the grid from 'gray' to 'dimgray'
+- the selection shape is deleted when the object is deleted
+- the plot area is now in a TAB.
+- solved bug that allowed middle button click to create selection
+- fixed issue with main window geometry restore (hopefully).
+- made view toolbar to be hidden by default as it is not really needed (we have the functions in menu, zoom is done with mouse wheel, and there is also the canvas context menu that holds the functionality)
+- remade the GUIElements.FCInput() and made a GUIElements.FCTab()
+- on visibility plot toogle the selection shape is deleted
+- made sure that on panning in Geometry editor, the context menu is not displayed
+- disabled App shortcut keys on entry in Geometry Editor so only the local shortcut keys are working
+- deleted metric units in canvas context menu
+- added protection so object deletion can't be done until Geometry Editor session is finished. Solved bug when the shapes on Geometry Editor were not transferred to the New_geometry object yet and the New_Geometry object is deleted. In this case the drawn shapes are left in a intermediary state on canvas.
+- added selection shape drawing in Geometry Editor preserving the current behavior: click to select, click on canvas clear selection, Ctrl+click add to selection new shape but remove from selection if already selected. Drag LMB from left to right select enclosed shapes, drag LMB from right to left select touching shapes. Now the selection is made based on
+- added info message to be displayed in infobar, when a object is renamed
+
+20.06.2018
+
+- there are two types of mouse drag selection (rectangle selection). If there is a rectangle selection from left to right, the color of the selection rectangle is blue and the selection is "enclosing" - this means that the object to be selected has to be enclosed by the selecting blue rectangle shape. If there is a rectangle selection fro right to left, the color of the selection rectangle is green and the selection is "touching" - this means that it's enough to touch with the selecting green rectangle the object(s) to be selected so they become selected
+- changed the modifier key required to be pressed when LMB is ckicked over canvas in order to copy to clipboard the coordinates of the click, from CTRL to SHIFT. CTRL will be used for multiple selection.
+- change the entry names in the canvas context menu
+- disconnected the app mouse event functions while in geometry editor since the geometry editor has it's own mouse event functions and there was interference between object and geometry items. Exception for the mouse release event so the canvas context menu still work.
+- solved a bug that did not update the obj.options after a geometry object was edited in geometry editor
+- solved a bug in the signal that saved the position and dimensions of the application window.
+- solved a bug in app.on_preferences() that created an error when run in Linux
+
+18.06.2018 Update 1
+
+- reverted the 'units' parameter change to 'global_units' due of a bug that did not allow saving of the project
+- modified the camlib transform (rotate, mirror, scale etc) functions so now they work with Gerber file loaded with 'follow' parameter
+
+18.06.2018
+
+- reworked the Properties context menu option to a Tool that displays more informations on the selected object(s)
+- remade the FlatCAM project extension as .FlatPrj
+- rearranged the toolbar menu entries to a more properly order
+- objects can now be selected on canvas, a blue polygon is drawn around when selected
+- reworked the Tool Move so it will work with the new canvas selection
+- reworked the Measurement Tool so it will work with the new canvas selection
+- canvas selection can now be done by dragging left mouse boutton and creating a selection box over the objects
+- when the objects are overlapped on canvas, the mouse click selection works in a circular way, selecting the first, then the second, then ..., then the last and then again the first and so on.
+- double click on a object on canvas will open the Selected Tab
+- each object store the bounding box coordinates in the options dict
+- the bbox coordinates are updated on the obj options when the object is modified by a transform function (rotate, scale etc)
+
+
+15.06.2018
+
+- the selection marker when moving is now a semitransparent Polygon with a blue border
+- rectified a small typo in the ToolTip for Excellon Format for Diptrace excellon format; from 4:2 to 5:2
+- corrected an error that cause no Gcode could be saved
+
+14.06.2018
+
+- more work on the contextual menu
+- added Draw context menu
+- added a new tool that bring together all the transformations, named Transformation Tool (Rotate, Skew, Scale, Offset, Flip)
+- added shorcut key 'Q' which toggle the units between IN and MM
+- remade the Move tool, there is now a selection box to show where the move is done
+- remade the Measurement tool, there is now a line between the start point of measurement and the end point of the measurement.
+- renamed most of the system variables that have a global app effect to global_name where name is the parameter (variable)
+
+9.06.2018
+
+- reverted to PyQt4. PyQt5 require too much software rewrite
+- added calculators: units_calculator and V-shape Tool calculator
+- solved bug in Join Excellon
+- added right click menu over canvas
+
+6.06.2018 Update
+
+- fixed bug: G-Code could not be saved
+- fixed bug: double clicking a category in Project Tab made the app to crash
+- remade the bounds() function to work with nested lists of objects as per advice from JP which made the operation less performance taxing.
+- added shortcut Shift+R that is complement to 'R'
+- shorcuts 'R' and 'Shift+R' are working now in steps of 90 degrees instead of previous 45 degrees.
+- added filters in the open ... FlatCAM projects are saved automatically as *.flat, the Gerber files have few categories. So the Excellons and G-Code and SVG.
+
+6.06.2018
+
+- remade the transform functions (rotate, flip, skew) so they are now working for joined objects, too
+- modified the Skew and Rotate comamands: if they are applied over a selection of objects than the origin point will be the center of the biggest bounding box. That allow for perfect sync between the selected objects
+- started to modify the program so the exceptions are handled correctly
+- solved bug where a crash occur when ObjCollection.setData didn't return a bool value
+- work in progress for handling situations when a different file is loaded as another (like loading a Gerber file using Open Excellon commands.
+- added filters on open_gerber and open_excellon Dialogs. There is still the ability to select All Files but this should reduce the cases when the user is trying to oprn a file from a wrong place.
+
+4.06.2018
+
+- finished PyQt4 to PyQt4 port on the Vispy variant (there were some changes compared with the Matplotlib version for which the port was finished some time ago)
+- added Ctrl+S shortcut for the Geometry Editor. When is activated it will save de geometry ("update") and return to the main App.
+- modified the Mirror command for the case when multiple objects are selected and we want to mirror all together. In this case they should mirror around a bounding box to fill all.
+
+3.06.2018
+
+- removed the current drill path optimizations as they are inefficient
+- implemented Google OR-tools drill path optimization in 2 flavors; Basic OR-tools TSP algorithm and OR-Tools Metaheuristics Guided Local Path
+- Move tool is moved to Menu -> Edit under the name Move Object
+
+- solved some internal bugs (info command was creating an non-fatal error in PyQt, regarding using QPixMaps outside GUI thread
+- reworked camlib number parsing (still had some bugs)
+- working in porting the application from usage of PyQt4 to PyQt4
+- added TclCommands save_sys and list_sys. save_sys is saving all the system default parameters and list_sys is listing them by the first letters. listsys with no arguments will list all the system parameters.
+
+29.05.2018
+
+- modified the labels for the X,Y and Dx,Dy coordinates
+- modified the menu entries, added more icons
+- added initial work on a Excellon Editor
+- modified the behavior of when clicking on canvas the coordinates were copied to cliboard: now it is required to press CTRL key for this to happen, and it will only happen just for left mouse button click
+- removed the autocopy of the object name on new object creation
+- remade the Tcl commands drillcncjob and cncjob
+- added fix so the canvas is focused on the start of the program, therefore the shortcuts work without the need for doing first a click on canvas.
+
+28.05.2018
+
+- added total drill count column in Excellon Tool Table which displays the total number of drills
+- added aliases in panelize Tool (pan and panel should work)
+- modified generate_milling method which had issues from the Python3 port (it could not sort the tools due of dict to dict comparison no longer possible).
+- modified the 'default' preprocessor in order to include a space between the value of Xcoord and the following Y
+- made optional the using of threads for the milling command; by default it is OFF (False) because in the current configuration it creates issues when it is using threads
+- modified the Panelize function and Tcl command Panelize. It was having issues due to multithreading (kept trying to modify a dictionary in redraw() method)and automatically selecting the last created object (feature introduced by me). I've added a parameter to the app_obj.new_object method, named autoselected (by default it is True) and in the panelize method I initialized it with False.
+By initializing the plot parameter with False for the temporary objects, I have increased dramatically the  generation speed of the panel because now the temporary object are no longer ploted which consumed time.
+- replaced log.warn() with log.warning() in camlib.py. Reason: deprecated
+- fixed the issue that the "Defaults" button was having no effect when clicked and Options Combo was in Project Options
+- fixed issue with Tcl Shell loosing focus after each command, therefore needing to click in the edit line before we type a new command (borrowed from @brainstorm
+- added a header in the preprocessor files mentioning that the GCODE files were generated by FlatCAM.
+- modified the number of decimals in some of the line entries to 4.
+- added an alias for the millholes Tcl Command: 'mill'
+
+27.04.2018
+
+- modified the Gerber.scale() function from camlib.py in order to allow loading Gerber files with 'follow' parameter in other units than the current ones
+- snap_max_entry is disabled when the DRAW toolbar is disabled (previous fix didn't work)
+- added drill count column in Excellon Tool Table which displays the total number of drills for each tool
+- added a new menu entry in Menu -> EDIT named "Join Excellon". It will merge a selection of Excellon files into a new Excellon file
+- added menu stubs for other Excellon based actions
+- solved bug that was not possible to generate film from joined geometry
+- improved toggle active/inactive of the object through SPACE key. Now the command works not only for one object but also for a selection
+
+26.05.2018
+
+- made conversion to Python3
+- added Rtree Indexing drill path optimization
+- added a checkbox in Options Tab -> App Defaults -> Excellon Group named Excellon Optim. Type from which it can be selected the default optimization type: TS stands for Travelling Salesman algorithm and Rtree stands for Rtree Indexing
+- added a checkbox on the Grid Toolbar that when checked (default status is checked) whatever value entered in the GridX entry will be used instead of the now disabled GridY entry
+- modified the default behavior on when a line_entry is clicked. Now, on each click on a line_entry, the content is automatically selected.
+- snap_max_entry is disabled when the DRAW toolbar is disabled
+
+24.05.2015
+
+- in Geometry Editor added a initial form of Rotate Geometry command in toolbar
+- changed the way the geometry is finished if it requires a key: before it was using key 'Space' now it uses 'Enter'
+- added Shortcut for Rotate Geometry to key 'Space'
+- after using a tool in Geometry Editor it automatically defaults to 'Select Tool'
+
+23.05.2018
+
+Added key shortcut's in FlatCAMApp and in Geometry Editor.
+
+FlatCAMApp shortcut list:
+1      Zoom Fit
+2      Zoom Out
+3      Zoom In
+C      Copy Obj_Name
+E      Edit Geometry (if selected)
+G      Grid On/Off
+M      Move Obj
+
+N      New Geometry
+R      Rotate
+S      Shell Toggle
+V      View Fit
+X      Flip on X_axis
+Y      Flip on Y_axis
+~      Show Shortcut List
+
+Space:   En(Dis)able Obj Plot
+Ctrl+A   Select All
+Ctrl+C   Copy Obj
+Ctrl+E   Open Excellon File
+Ctrl+G   Open Gerber File
+Ctrl+M   Measurement Tool
+Ctrl+O   Open Project
+Ctrl+S   Save Project As
+Delete   Delete Obj'''
+
+
+Geometry Editor Key shortcut list:
+A       Add an 'Arc'
+C       Copy Geo Item
+G       Grid Snap On/Off
+K       Corner Snap On/Off
+M       Move Geo Item
+
+N       Add an 'Polygon'
+O       Add a 'Circle'
+P       Add a 'Path'
+R       Add an 'Rectangle'
+S       Select Tool Active
+
+
+~        Show Shortcut List
+Space:   Rotate Geometry
+Enter:   Finish Current Action
+Escape:  Abort Current Action
+Delete:  Delete Obj
+
+22.05.2018
+
+- Added Marlin preprocessor
+- Added a new entry into the Geometry and Excellon Object's UI: Feedrate rapid: the purpose is to set a feedrate for the G0 command that some firmwares like Marlin don't intepret as 'move with highest speed'
+- FlatCAM was not making the conversion from one type of units to another for a lot of parameters. Corrected that.
+- Modified the Marlin preprocessor so it will generate the required GCODE.
+
+21.05.2018
+
+- added new icons for menu entries
+- added shortcuts that work on the Project tab but also over Plot. Shorcut list is accesed with shortcut key '~' sau '`'
+- small GUI modification: on each "New File" command it will switch to the Project Tab regardless on which tab we were.
+- removed the global shear entries and checkbox as they can be damaging and it will build effect upon effect, which is not good
+- solved bug in that the Edit -> Shear on X (Y)axis could adjust only in integers. Now the angle can be adjusted in float with 3 decimals.
+- changed the tile of QInputDialog to a more general one
+- changed the "follow" Tcl command to the new format
+- added a new entry in the Menu -> File, to open a Gerber with the follow parameter = True
+- added a new checkbox in the Gerber Object Selection Tab that when checked it will create a "follow" geometry
+- added a few lines in Mill Holes Tcl command to check if there are promises and raise an Tcl error if there are any.
+- started to modify the Export_Svg Tcl command
+
+20.05.2018
+
+- changed the interpretation of the axis for the rotate and skew commands. Actually I reversed them to reflect reality.
+- for the rotate command a positive angle now rotates CW. It was reversed.
+- added shortcuts (for outside CANVAS; the CANVAS has it's own set of shortcuts) Ctrl+C will copy to clipboard the name of the selected object Ctrl+A will Select All objects
+"X" key will flip the selected objects on X axis
+"Y" key will flip the selected objects on Y axis
+"R" key will rotate CW with a 45 degrees step
+
+- changed the layout for the top of th Options page. Added a checkbox and entries for parameters for skew command. When the checkbox is checked it will save (and load at the next startup of the program) the option that at each CNCJob generation (be it from Excellon or Geometry) it will perform the Skew command with the parametrs set in the nearby field boxes (Skew X and Skey Y angles). It is useful in case the CNC router is not perfectly alligned between the X and Y axis
+- added some protection in case the skew command receive a None parameter
+- BUG solved: made an UGLY (really UGLY) HACK so now, when there is a panel geometry generated from GUI, the project WILL save. I had to create a copy of the generated panel geometry and delete the original panel geometry. This way there is no complain from JSON module about circular reference.
+- removed the Save buttons previously added on each Group in Application Defaults. Replaced them with a single Save button that stays always on top of the Options TAB
+- added settings for defaults for the Grid that are persistent
+- changed the default view at FlatCAM startup: now the origin is in the center of the screen
+
+19.05.2018
+
+- last object that is opened (created) is always automatically selected and the name of the object is automatically copied to clipboard; useful when using the TCL command :)
+- added new commands in MENU -> EDIT named: "Copy Object" and "Copy Obj as Geom". The first command will duplicate any object (Geometry, Gerber, Excellon). The second command will duplicate the object as a geometry. For example, holes in Excello now are just circles that can be "painted" if one wants it.
+- added new Tool named ToolFreeformCutout. It does what it says, it will make a board cutout from a "any shape" Gerber or Geometry file
+- solved bug in the TCL command "drillcncjob" that always used the endz parameter value as the toolchangez parameter value and for the endz value used a default value = 1
+- added preprocessor name into the TCL command "drillcncjob" parameters
+- when adding a new geometry the default name is now: "New_Geometry" instead of "New Geometry". TCL commands don't handle the spaces inside the name and require adding quotes.
+- solved bug in "cncjob" TCL command in which it used multidepth parameter as always True regardless of the argument provided
+- added a checkbox for Multidepth in the Options Tab -> Application Defaults
+
+18.05.2018
+
+- added an "Defaults" button in Excellon Defaults Group; it loads the following configuration (Excellon_format_in 2:4, Excellon_format_mm 3:3, Excellon_zeros LZ)
+- added Save buttons for each Defaults Group; in the future more parameters will be propagated in the app, for now they are a few
+- added functions for Skew on X axis and for Skew on Y menu stubs. Now, clicking on those Menu -> Options -> Transform Object menu entries will trigger those functions
+- added a CheckBox button in the Options Tab -> Application Defaults that control the behaviour of the TCL shell: checking it will make the TCL shell window visible at each start-up, unchecking it the TCL shell window will be hidden until needed
+- Depth/pass parameter from Geometry Object CNC Job is now in the defaults and it will keep it's value until changed in the Application Defaults.
+
+17.05.2018
+
+- added messages box for the Flip commands to show error in case there is no object selected when the command is executed
+- added field entries in the Options TAB - > Application Defaults for the following newly introduced parameters: 
+excellon_format_upper_in
+excellon_format_lower_in
+excellon_format_upper_mm
+excellon_format_lower_mm
+
+The ones with upper indicate how many digits are allocated for the units and the ones with lower indicate how many digits from coordinates are alocated for the decimals.
+
+[  Eg: Excellon format 2:4 in INCH
+   excellon_format_upper_in = 2
+   excellon_format_lower_in = 4
+where the first 2 digits are for units and the last 4 digits are
+decimals so from a number like 235589 we will get a coordinate 23.5589
+]
+
+- added Radio button in the Options TAB - > Application Defaults for the Excellon_zeros parameter
+After each change of those parameters the user will have to press "Save defaults" from File menu in order to propagate the new values, or wait for the autosave to kick in (each 20sec).
+Those parameters can be set in the set_sys TCL command.
+
+15.05.2018
+- modified SetSys TCL command: now it can change units
+- modified SetSys TCL command: now it can set new parameters: excellon_format_mm and excellon_format_in. the first one is when the excellon units are MM and the second is for when the excellon units are in INCH. Those parameters can be set with a number between 1 and 5 and it signify how many digits are before coma.
+- added new GUI command in EDIT -> Select All. It will select all objects on the first mouse click and on the second will deselect all (toggle action)
+- added new GUI commands in Options -> Transform object. Added Rotate selection, Flip on X axis of the selection and Flip on Y axis of the selection For the Rotate selection command, negative numbers means rotation CCW and positive numbers means rotation CW.
+- cleaned up a bit the module imports
+- worked on the excellon parsing for the case of trailing zeros. If there are more than 6digits in the coordinates, in case that there is no period, now the software will identify the issue and attempt to correct it by dividing the coordinate  further by 10 for each additional digit over 6. If the number of digits is less than 6 then the software will multiply by 10 the coordinates
+
+14.05.2018
+
+- fixed bug in Geometry CNCJob generation that prevented generating the object
+- added GRBL 1.1 preprocessor and Laser preprocessor (adapted from the work of MARCO A QUEZADA)
+
+13.05.2018
+
+- added postprocessing in correct form
+- added the possibility to select an preprocessor for Excellon Object
+- added a new preprocessor, manual_toolchange.py. It allows to change the tools and adjust the drill tip to touch the surface manually, always in the X=0, Y=0, Z = toolchangeZ coordinates.
+- fixed drillcncjob TCL command by adding toolchangeZ parameter
+- fixed the preprocessor file template 'default.py' in the toolchange command section
+- after I created a feature that the message in infobar is cleared by moving mouse on canvas, it generated a bug in TCL shell: everytime  mouse was moved it will add a space into the TCL read only section. Now this bug is fixed.
+- added an EndZ parameter for the drillcncjob and cncjob TCL commands: it will specify at what Z value to park the CNC when job ends
+- the spindle will be turned on after the toolchange and it will be turned off just before the final end move.
+
+Previously:
+- added GRID based working of FLATCAM
+- added Set Origin command
+- added FilmTool, PanelizeTool GUI, MoveTool
+- and others
+
+24.04.2018
+
+- Remade the Measurement Tool: it now ask for the Start point of the measurement and then for the Stop point. After it will display the measurement until we left click again on the canvas and so on. Previously you clicked the start point and reset the X and Y coords displayed and then you moved the mouse pointer wherever you wanted to measure, but moving the mouse from there you lost the measurement.
+- Added Relative measurement on the main plot
+- Now both the measuring tool and the relative measurement will work only with the left click of the mouse button because middle mouse click and right mouse click are used for panning
+- Renamed the tools files starting with Tool so they are grouped (in the future they may have their own folder like for TCL Commands)
+
+- Commented some shortcut keys and functions for features that are not present anymore or they are planned to be in the future but unfinished (like buffer tool, paint tool)
+- minor corrections regarding PEP8 (Pycharm complains about the m)
+- solved bug in TclCommandsSetSys.py Everytime that the command was executed it complain about the parameter not being in the list (something like this). There was a missing “else:”
+- when using the command “set_sys excellon_zeros” with parameter in lower case (either ‘l’ or ‘t’) now it is always written in the defaults file as capital letter
+
+- solved a bug introduced by me: when apertures macros were detected in Excellon file, FlatCam will complain about missing dictionary key “size”. Now it first check if the aperture is a macro and perform the check for zero value only for apertures with “size” key
+- solved a bug that didn't allowed FC to detect if Excellon file has leading zeros or trailing zeros
+- solved a bug that FC was searching for char ‘%’ that signal end of Excellon header even in commented lines (latest versions of Eagle end the commented line with a ‘%’)
+
+
+============================================
+This fork features:
+
+- Added buttons in the menu bar for opening of Gerber and Excellon files;
+- Reduced number of decimals for drill bits to two decimals;
+- Updated make_win32.py so it will work with cx_freeze 5.0.1 
+- Added capability so FlatCAM can now read Gerber files with traces having zero value (aperture size is zero);
+- Added Paint All / Seed based Paint functions from the JP's FlatCAM;
+- Added Excellon move optimization (travelling salesman algorithm) cherry-picked from David Kahler: https://bitbucket.org/dakahler/flatcam
+- Updated make_win32.py so it will work with cx_freeze 5.0.1
+- Corrected small typo in DblSidedTool.py
+- Added the TCL commands in the new format. Picked from FLATCAM master.
+- Hack to fix the issue with geometry not being updated after a TCL command was executed. Now after each TCL command the plot_all() function is executed and the canvas is refreshed.
+- Added GUI for panelization TCL command
+- Added GUI tool for the panelization TCL command: Changed some ToolTips.
+============================================
+
+Previously added features by Dennis
+
+- "Clear non-copper" feature, supporting multi-tool work.
+- Groups in Project view.
+- Pan view by dragging in visualizer window with pressed MMB.
+- OpenGL-based visualizer.
+

+ 42 - 9
FlatCAM.py

@@ -3,14 +3,20 @@ import os
 
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings, Qt
-from FlatCAMApp import App
+from app_Main import App
+from appGUI import VisPyPatches
+
 from multiprocessing import freeze_support
-from flatcamGUI import VisPyPatches
+# import copyreg
+# import types
 
 if sys.platform == "win32":
     # cx_freeze 'module win32' workaround
     pass
 
+MIN_VERSION_MAJOR = 3
+MIN_VERSION_MINOR = 5
+
 
 def debug_trace():
     """
@@ -29,6 +35,25 @@ if __name__ == '__main__':
     # NOTE: Never talk to the GUI from threads! This is why I commented the above.
     freeze_support()
 
+    major_v = sys.version_info.major
+    minor_v = sys.version_info.minor
+    # Supported Python version is >= 3.5
+    if major_v >= MIN_VERSION_MAJOR:
+        if minor_v >= MIN_VERSION_MINOR:
+            pass
+        else:
+            print("FlatCAM BETA uses PYTHON 3 or later. The version minimum is %s.%s\n"
+                  "Your Python version is: %s.%s" % (MIN_VERSION_MAJOR, MIN_VERSION_MINOR, str(major_v), str(minor_v)))
+
+            if minor_v >= 8:
+                os._exit(0)
+            else:
+                sys.exit(0)
+    else:
+        print("FlatCAM BETA uses PYTHON 3 or later. The version minimum is %s.%s\n"
+              "Your Python version is: %s.%s" % (MIN_VERSION_MAJOR, MIN_VERSION_MINOR, str(major_v), str(minor_v)))
+        sys.exit(0)
+
     debug_trace()
     VisPyPatches.apply_patches()
 
@@ -44,6 +69,19 @@ if __name__ == '__main__':
     else:
         os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "0"
 
+    # if hdpi_support == 2:
+    #     tst_screen = QtWidgets.QApplication(sys.argv)
+    #     if tst_screen.screens()[0].geometry().width() > 1930 or tst_screen.screens()[1].geometry().width() > 1930:
+    #         QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
+    #         del tst_screen
+    # else:
+    #     QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling, False)
+
+    if hdpi_support == 2:
+        QtWidgets.QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
+    else:
+        QtWidgets.QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, False)
+
     app = QtWidgets.QApplication(sys.argv)
 
     # apply style
@@ -52,11 +90,6 @@ if __name__ == '__main__':
         style = settings.value('style', type=str)
         app.setStyle(style)
 
-    if hdpi_support == 2:
-        app.setAttribute(Qt.AA_EnableHighDpiScaling, True)
-    else:
-        app.setAttribute(Qt.AA_EnableHighDpiScaling, False)
-
-    fc = App()
-
+    fc = App(qapp=app)
     sys.exit(app.exec_())
+    # app.exec_()

+ 0 - 9071
FlatCAMApp.py

@@ -1,9071 +0,0 @@
-# ###########################################################
-# FlatCAM: 2D Post-processing for Manufacturing             #
-# http://flatcam.org                                        #
-# Author: Juan Pablo Caram (c)                              #
-# Date: 2/5/2014                                            #
-# MIT Licence                                               #
-# ###########################################################
-
-import urllib.request, urllib.parse, urllib.error
-import getopt
-import random
-import simplejson as json
-import lzma
-import threading
-
-from stat import S_IREAD, S_IRGRP, S_IROTH
-import subprocess
-
-import tkinter as tk
-from PyQt5 import QtPrintSupport
-
-import urllib.request, urllib.parse, urllib.error
-from contextlib import contextmanager
-import gc
-
-from xml.dom.minidom import parseString as parse_xml_string
-
-# #######################################
-# #      Imports part of FlatCAM       ##
-# #######################################
-from ObjectCollection import *
-from FlatCAMObj import *
-from flatcamGUI.PlotCanvas import *
-from flatcamGUI.FlatCAMGUI import *
-from FlatCAMCommon import LoudDict
-from FlatCAMPostProc import load_postprocessors
-
-from flatcamEditors.FlatCAMGeoEditor import FlatCAMGeoEditor
-from flatcamEditors.FlatCAMExcEditor import FlatCAMExcEditor
-from flatcamEditors.FlatCAMGrbEditor import FlatCAMGrbEditor
-
-from FlatCAMProcess import *
-from FlatCAMWorkerStack import WorkerStack
-from flatcamGUI.VisPyVisuals import Color
-from vispy.gloo.util import _screenshot
-from vispy.io import write_png
-
-from flatcamTools import *
-
-from multiprocessing import Pool
-import tclCommands
-
-import gettext
-import FlatCAMTranslation as fcTranslate
-import builtins
-
-fcTranslate.apply_language('strings')
-if '_' not in builtins.__dict__:
-    _ = gettext.gettext
-
-# ########################################
-# #                App                 ###
-# ########################################
-
-
-class App(QtCore.QObject):
-    """
-    The main application class. The constructor starts the GUI.
-    """
-
-    # Get Cmd Line Options
-    cmd_line_shellfile = ''
-    cmd_line_help = "FlatCam.py --shellfile=<cmd_line_shellfile>"
-    try:
-        # Multiprocessing pool will spawn additional processes with 'multiprocessing-fork' flag
-        cmd_line_options, args = getopt.getopt(sys.argv[1:], "h:", ["shellfile=", "multiprocessing-fork="])
-    except getopt.GetoptError:
-        print(cmd_line_help)
-        sys.exit(2)
-    for opt, arg in cmd_line_options:
-        if opt == '-h':
-            print(cmd_line_help)
-            sys.exit()
-        elif opt == '--shellfile':
-            cmd_line_shellfile = arg
-
-    # ## Logging ###
-    log = logging.getLogger('base')
-    log.setLevel(logging.DEBUG)
-    # log.setLevel(logging.WARNING)
-    formatter = logging.Formatter('[%(levelname)s][%(threadName)s] %(message)s')
-    handler = logging.StreamHandler()
-    handler.setFormatter(formatter)
-    log.addHandler(handler)
-
-    # ####################################
-    # Version and VERSION DATE ###########
-    # ####################################
-    version = 8.93
-    version_date = "2019/08/10"
-    beta = True
-
-    # current date now
-    date = str(datetime.today()).rpartition('.')[0]
-    date = ''.join(c for c in date if c not in ':-')
-    date = date.replace(' ', '_')
-
-    # URL for update checks and statistics
-    version_url = "http://flatcam.org/version"
-
-    # App URL
-    app_url = "http://flatcam.org"
-
-    # Manual URL
-    manual_url = "http://flatcam.org/manual/index.html"
-    video_url = "https://www.youtube.com/playlist?list=PLVvP2SYRpx-AQgNlfoxw93tXUXon7G94_"
-
-    # this variable will hold the project status
-    # if True it will mean that the project was modified and not saved
-    should_we_save = False
-
-    # flag is True if saving action has been triggered
-    save_in_progress = False
-
-    # #################
-    # #    Signals   ##
-    # #################
-
-    # Inform the user
-    # Handled by:
-    #  * App.info() --> Print on the status bar
-    inform = QtCore.pyqtSignal(str)
-
-    app_quit = QtCore.pyqtSignal()
-
-    # General purpose background task
-    worker_task = QtCore.pyqtSignal(dict)
-
-    # File opened
-    # Handled by:
-    #  * register_folder()
-    #  * register_recent()
-    # Note: Setting the parameters to unicode does not seem
-    #       to have an effect. Then are received as Qstring
-    #       anyway.
-
-    # File type and filename
-    file_opened = QtCore.pyqtSignal(str, str)
-    # File type and filename
-    file_saved = QtCore.pyqtSignal(str, str)
-
-    # Percentage of progress
-    progress = QtCore.pyqtSignal(int)
-
-    plots_updated = QtCore.pyqtSignal()
-
-    # Emitted by new_object() and passes the new object as argument, plot flag.
-    # on_object_created() adds the object to the collection, plots on appropriate flag
-    # and emits new_object_available.
-    object_created = QtCore.pyqtSignal(object, bool, bool)
-
-    # Emitted when a object has been changed (like scaled, mirrored)
-    object_changed = QtCore.pyqtSignal(object)
-
-    # Emitted after object has been plotted.
-    # Calls 'on_zoom_fit' method to fit object in scene view in main thread to prevent drawing glitches.
-    object_plotted = QtCore.pyqtSignal(object)
-
-    # Emitted when a new object has been added or deleted from/to the collection
-    object_status_changed = QtCore.pyqtSignal(object, str)
-
-    message = QtCore.pyqtSignal(str, str, str)
-
-    # Emmited when shell command is finished(one command only)
-    shell_command_finished = QtCore.pyqtSignal(object)
-
-    # Emitted when multiprocess pool has been recreated
-    pool_recreated = QtCore.pyqtSignal(object)
-
-    # Emitted when an unhandled exception happens
-    # in the worker task.
-    thread_exception = QtCore.pyqtSignal(object)
-
-    def __init__(self, user_defaults=True, post_gui=None):
-        """
-        Starts the application.
-
-        :return: app
-        :rtype: App
-        """
-
-        App.log.info("FlatCAM Starting...")
-
-        self.main_thread = QtWidgets.QApplication.instance().thread()
-
-        # ################ ##
-        # # ## OS-specific # ##
-        # ################ ##
-
-        # Folder for user settings.
-        if sys.platform == 'win32':
-            from win32com.shell import shell, shellcon
-            if platform.architecture()[0] == '32bit':
-                App.log.debug("Win32!")
-            else:
-                App.log.debug("Win64!")
-
-            self.data_path = shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, None, 0) + '\FlatCAM'
-            self.os = 'windows'
-        else:  # Linux/Unix/MacOS
-            self.data_path = os.path.expanduser('~') + '/.FlatCAM'
-            self.os = 'unix'
-
-        # ############################ ##
-        # # ## Setup folders and files # ##
-        # ############################ ##
-
-        if not os.path.exists(self.data_path):
-            os.makedirs(self.data_path)
-            App.log.debug('Created data folder: ' + self.data_path)
-            os.makedirs(os.path.join(self.data_path, 'postprocessors'))
-            App.log.debug('Created data postprocessors folder: ' + os.path.join(self.data_path, 'postprocessors'))
-
-        self.postprocessorpaths = os.path.join(self.data_path,'postprocessors')
-        if not os.path.exists(self.postprocessorpaths):
-            os.makedirs(self.postprocessorpaths)
-            App.log.debug('Created postprocessors folder: ' + self.postprocessorpaths)
-
-        # create current_defaults.FlatConfig file if there is none
-        try:
-            f = open(self.data_path + '/current_defaults.FlatConfig')
-            f.close()
-        except IOError:
-            App.log.debug('Creating empty current_defaults.FlatConfig')
-            f = open(self.data_path + '/current_defaults.FlatConfig', 'w')
-            json.dump({}, f)
-            f.close()
-
-        # create factory_defaults.FlatConfig file if there is none
-        try:
-            f = open(self.data_path + '/factory_defaults.FlatConfig')
-            f.close()
-        except IOError:
-            App.log.debug('Creating empty factory_defaults.FlatConfig')
-            f = open(self.data_path + '/factory_defaults.FlatConfig', 'w')
-            json.dump({}, f)
-            f.close()
-
-        try:
-            f = open(self.data_path + '/recent.json')
-            f.close()
-        except IOError:
-            App.log.debug('Creating empty recent.json')
-            f = open(self.data_path + '/recent.json', 'w')
-            json.dump([], f)
-            f.close()
-
-        try:
-            fp = open(self.data_path + '/recent_projects.json')
-            fp.close()
-        except IOError:
-            App.log.debug('Creating empty recent_projects.json')
-            fp = open(self.data_path + '/recent_projects.json', 'w')
-            json.dump([], fp)
-            fp.close()
-
-        # Application directory. CHDIR to it. Otherwise, trying to load
-        # GUI icons will fail as their path is relative.
-        # This will fail under cx_freeze ...
-        self.app_home = os.path.dirname(os.path.realpath(__file__))
-        App.log.debug("Application path is " + self.app_home)
-        App.log.debug("Started in " + os.getcwd())
-
-        # cx_freeze workaround
-        if os.path.isfile(self.app_home):
-            self.app_home = os.path.dirname(self.app_home)
-
-        os.chdir(self.app_home)
-
-        # Create multiprocessing pool
-        self.pool = Pool()
-
-        # variable to store mouse coordinates
-        self.mouse = [0, 0]
-
-        # ###################
-        # # Initialize GUI ##
-        # ###################
-
-        # FlatCAM colors used in plotting
-        self.FC_light_green = '#BBF268BF'
-        self.FC_dark_green = '#006E20BF'
-        self.FC_light_blue = '#a5a5ffbf'
-        self.FC_dark_blue = '#0000ffbf'
-
-        QtCore.QObject.__init__(self)
-        self.ui = FlatCAMGUI(self.version, self.beta, self)
-        self.set_ui_title(name="New Project")
-
-        self.ui.geom_update[int, int, int, int, int].connect(self.save_geometry)
-        self.ui.final_save.connect(self.final_save)
-
-        # #############
-        # ### Data ####
-        # #############
-        self.recent = []
-        self.recent_projects = []
-
-        self.clipboard = QtWidgets.QApplication.clipboard()
-        self.proc_container = FCVisibleProcessContainer(self.ui.activity_view)
-
-        self.project_filename = None
-        self.toggle_units_ignore = False
-
-        # self.defaults_form = PreferencesUI()
-
-        # when adding entries here read the comments in the  method found bellow named:
-        # def new_object(self, kind, name, initialize, active=True, fit=True, plot=True)
-        self.defaults_form_fields = {
-            # General App
-            "units": self.ui.general_defaults_form.general_app_group.units_radio,
-            "global_app_level": self.ui.general_defaults_form.general_app_group.app_level_radio,
-            "global_language": self.ui.general_defaults_form.general_app_group.language_cb,
-
-            "global_shell_at_startup": self.ui.general_defaults_form.general_app_group.shell_startup_cb,
-            "global_version_check": self.ui.general_defaults_form.general_app_group.version_check_cb,
-            "global_send_stats": self.ui.general_defaults_form.general_app_group.send_stats_cb,
-            "global_pan_button": self.ui.general_defaults_form.general_app_group.pan_button_radio,
-            "global_mselect_key": self.ui.general_defaults_form.general_app_group.mselect_radio,
-
-            "global_project_at_startup": self.ui.general_defaults_form.general_app_group.project_startup_cb,
-            "global_project_autohide": self.ui.general_defaults_form.general_app_group.project_autohide_cb,
-            "global_toggle_tooltips": self.ui.general_defaults_form.general_app_group.toggle_tooltips_cb,
-            "global_worker_number": self.ui.general_defaults_form.general_app_group.worker_number_sb,
-            "global_tolerance": self.ui.general_defaults_form.general_app_group.tol_entry,
-
-            "global_open_style": self.ui.general_defaults_form.general_app_group.open_style_cb,
-
-            "global_compression_level": self.ui.general_defaults_form.general_app_group.compress_combo,
-            "global_save_compressed": self.ui.general_defaults_form.general_app_group.save_type_cb,
-
-            # General GUI Preferences
-            "global_gridx": self.ui.general_defaults_form.general_gui_group.gridx_entry,
-            "global_gridy": self.ui.general_defaults_form.general_gui_group.gridy_entry,
-            "global_snap_max": self.ui.general_defaults_form.general_gui_group.snap_max_dist_entry,
-            "global_workspace": self.ui.general_defaults_form.general_gui_group.workspace_cb,
-            "global_workspaceT": self.ui.general_defaults_form.general_gui_group.wk_cb,
-
-            "global_plot_fill": self.ui.general_defaults_form.general_gui_group.pf_color_entry,
-            "global_plot_line": self.ui.general_defaults_form.general_gui_group.pl_color_entry,
-            "global_sel_fill": self.ui.general_defaults_form.general_gui_group.sf_color_entry,
-            "global_sel_line": self.ui.general_defaults_form.general_gui_group.sl_color_entry,
-            "global_alt_sel_fill": self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry,
-            "global_alt_sel_line": self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry,
-            "global_draw_color": self.ui.general_defaults_form.general_gui_group.draw_color_entry,
-            "global_sel_draw_color": self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry,
-
-            "global_proj_item_color": self.ui.general_defaults_form.general_gui_group.proj_color_entry,
-            "global_proj_item_dis_color": self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry,
-
-            # General GUI Settings
-            "global_layout": self.ui.general_defaults_form.general_gui_set_group.layout_combo,
-            "global_hover": self.ui.general_defaults_form.general_gui_set_group.hover_cb,
-            "global_selection_shape": self.ui.general_defaults_form.general_gui_set_group.selection_cb,
-            # Gerber General
-            "gerber_plot": self.ui.gerber_defaults_form.gerber_gen_group.plot_cb,
-            "gerber_solid": self.ui.gerber_defaults_form.gerber_gen_group.solid_cb,
-            "gerber_multicolored": self.ui.gerber_defaults_form.gerber_gen_group.multicolored_cb,
-            "gerber_circle_steps": self.ui.gerber_defaults_form.gerber_gen_group.circle_steps_entry,
-
-            # Gerber Options
-            "gerber_isotooldia": self.ui.gerber_defaults_form.gerber_opt_group.iso_tool_dia_entry,
-            "gerber_isopasses": self.ui.gerber_defaults_form.gerber_opt_group.iso_width_entry,
-            "gerber_isooverlap": self.ui.gerber_defaults_form.gerber_opt_group.iso_overlap_entry,
-            "gerber_combine_passes": self.ui.gerber_defaults_form.gerber_opt_group.combine_passes_cb,
-            "gerber_milling_type": self.ui.gerber_defaults_form.gerber_opt_group.milling_type_radio,
-            "gerber_noncoppermargin": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_margin_entry,
-            "gerber_noncopperrounded": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_rounded_cb,
-            "gerber_bboxmargin": self.ui.gerber_defaults_form.gerber_opt_group.bbmargin_entry,
-            "gerber_bboxrounded": self.ui.gerber_defaults_form.gerber_opt_group.bbrounded_cb,
-
-            # Gerber Advanced Options
-            "gerber_aperture_display": self.ui.gerber_defaults_form.gerber_adv_opt_group.aperture_table_visibility_cb,
-            # "gerber_aperture_scale_factor": self.ui.gerber_defaults_form.gerber_adv_opt_group.scale_aperture_entry,
-            # "gerber_aperture_buffer_factor": self.ui.gerber_defaults_form.gerber_adv_opt_group.buffer_aperture_entry,
-            "gerber_follow": self.ui.gerber_defaults_form.gerber_adv_opt_group.follow_cb,
-
-            # Gerber Export
-            "gerber_exp_units": self.ui.gerber_defaults_form.gerber_exp_group.gerber_units_radio,
-            "gerber_exp_integer": self.ui.gerber_defaults_form.gerber_exp_group.format_whole_entry,
-            "gerber_exp_decimals": self.ui.gerber_defaults_form.gerber_exp_group.format_dec_entry,
-            "gerber_exp_zeros": self.ui.gerber_defaults_form.gerber_exp_group.zeros_radio,
-
-            # Gerber Editor
-            "gerber_editor_sel_limit": self.ui.gerber_defaults_form.gerber_editor_group.sel_limit_entry,
-
-            # Excellon General
-            "excellon_plot": self.ui.excellon_defaults_form.excellon_gen_group.plot_cb,
-            "excellon_solid": self.ui.excellon_defaults_form.excellon_gen_group.solid_cb,
-            "excellon_format_upper_in": self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_in_entry,
-            "excellon_format_lower_in": self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_lower_in_entry,
-            "excellon_format_upper_mm": self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_mm_entry,
-            "excellon_format_lower_mm": self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_lower_mm_entry,
-            "excellon_zeros": self.ui.excellon_defaults_form.excellon_gen_group.excellon_zeros_radio,
-            "excellon_units": self.ui.excellon_defaults_form.excellon_gen_group.excellon_units_radio,
-            "excellon_optimization_type": self.ui.excellon_defaults_form.excellon_gen_group.excellon_optimization_radio,
-            "excellon_search_time": self.ui.excellon_defaults_form.excellon_gen_group.optimization_time_entry,
-
-            # Excellon Options
-            "excellon_drillz": self.ui.excellon_defaults_form.excellon_opt_group.cutz_entry,
-            "excellon_travelz": self.ui.excellon_defaults_form.excellon_opt_group.travelz_entry,
-            "excellon_feedrate": self.ui.excellon_defaults_form.excellon_opt_group.feedrate_entry,
-            "excellon_spindlespeed": self.ui.excellon_defaults_form.excellon_opt_group.spindlespeed_entry,
-            "excellon_spindledir": self.ui.excellon_defaults_form.excellon_opt_group.spindledir_radio,
-            "excellon_dwell": self.ui.excellon_defaults_form.excellon_opt_group.dwell_cb,
-            "excellon_dwelltime": self.ui.excellon_defaults_form.excellon_opt_group.dwelltime_entry,
-            "excellon_toolchange": self.ui.excellon_defaults_form.excellon_opt_group.toolchange_cb,
-            "excellon_toolchangez": self.ui.excellon_defaults_form.excellon_opt_group.toolchangez_entry,
-            "excellon_ppname_e": self.ui.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb,
-            "excellon_tooldia": self.ui.excellon_defaults_form.excellon_opt_group.tooldia_entry,
-            "excellon_slot_tooldia": self.ui.excellon_defaults_form.excellon_opt_group.slot_tooldia_entry,
-            "excellon_gcode_type": self.ui.excellon_defaults_form.excellon_opt_group.excellon_gcode_type_radio,
-
-            # Excellon Advanced Options
-            "excellon_offset": self.ui.excellon_defaults_form.excellon_adv_opt_group.offset_entry,
-            "excellon_toolchangexy": self.ui.excellon_defaults_form.excellon_adv_opt_group.toolchangexy_entry,
-            "excellon_startz": self.ui.excellon_defaults_form.excellon_adv_opt_group.estartz_entry,
-            "excellon_endz": self.ui.excellon_defaults_form.excellon_adv_opt_group.eendz_entry,
-            "excellon_feedrate_rapid": self.ui.excellon_defaults_form.excellon_adv_opt_group.feedrate_rapid_entry,
-            "excellon_z_pdepth": self.ui.excellon_defaults_form.excellon_adv_opt_group.pdepth_entry,
-            "excellon_feedrate_probe": self.ui.excellon_defaults_form.excellon_adv_opt_group.feedrate_probe_entry,
-            "excellon_f_plunge": self.ui.excellon_defaults_form.excellon_adv_opt_group.fplunge_cb,
-            "excellon_f_retract": self.ui.excellon_defaults_form.excellon_adv_opt_group.fretract_cb,
-
-            # Excellon Export
-            "excellon_exp_units": self.ui.excellon_defaults_form.excellon_exp_group.excellon_units_radio,
-            "excellon_exp_format": self.ui.excellon_defaults_form.excellon_exp_group.format_radio,
-            "excellon_exp_integer": self.ui.excellon_defaults_form.excellon_exp_group.format_whole_entry,
-            "excellon_exp_decimals": self.ui.excellon_defaults_form.excellon_exp_group.format_dec_entry,
-            "excellon_exp_zeros": self.ui.excellon_defaults_form.excellon_exp_group.zeros_radio,
-
-            # Excellon Editor
-            "excellon_editor_sel_limit": self.ui.excellon_defaults_form.excellon_editor_group.sel_limit_entry,
-            "excellon_editor_newdia": self.ui.excellon_defaults_form.excellon_editor_group.addtool_entry,
-            "excellon_editor_array_size": self.ui.excellon_defaults_form.excellon_editor_group.drill_array_size_entry,
-            "excellon_editor_lin_dir": self.ui.excellon_defaults_form.excellon_editor_group.drill_axis_radio,
-            "excellon_editor_lin_pitch": self.ui.excellon_defaults_form.excellon_editor_group.drill_pitch_entry,
-            "excellon_editor_lin_angle": self.ui.excellon_defaults_form.excellon_editor_group.drill_angle_entry,
-            "excellon_editor_circ_dir": self.ui.excellon_defaults_form.excellon_editor_group.drill_circular_dir_radio,
-            "excellon_editor_circ_angle":
-                self.ui.excellon_defaults_form.excellon_editor_group.drill_circular_angle_entry,
-
-            # Geometry General
-            "geometry_plot": self.ui.geometry_defaults_form.geometry_gen_group.plot_cb,
-            "geometry_circle_steps": self.ui.geometry_defaults_form.geometry_gen_group.circle_steps_entry,
-            "geometry_cnctooldia": self.ui.geometry_defaults_form.geometry_gen_group.cnctooldia_entry,
-
-            # Geometry Options
-            "geometry_cutz": self.ui.geometry_defaults_form.geometry_opt_group.cutz_entry,
-            "geometry_travelz": self.ui.geometry_defaults_form.geometry_opt_group.travelz_entry,
-            "geometry_feedrate": self.ui.geometry_defaults_form.geometry_opt_group.cncfeedrate_entry,
-            "geometry_feedrate_z": self.ui.geometry_defaults_form.geometry_opt_group.cncplunge_entry,
-            "geometry_spindlespeed": self.ui.geometry_defaults_form.geometry_opt_group.cncspindlespeed_entry,
-            "geometry_spindledir": self.ui.geometry_defaults_form.geometry_opt_group.spindledir_radio,
-            "geometry_dwell": self.ui.geometry_defaults_form.geometry_opt_group.dwell_cb,
-            "geometry_dwelltime": self.ui.geometry_defaults_form.geometry_opt_group.dwelltime_entry,
-            "geometry_ppname_g": self.ui.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb,
-            "geometry_toolchange": self.ui.geometry_defaults_form.geometry_opt_group.toolchange_cb,
-            "geometry_toolchangez": self.ui.geometry_defaults_form.geometry_opt_group.toolchangez_entry,
-            "geometry_depthperpass": self.ui.geometry_defaults_form.geometry_opt_group.depthperpass_entry,
-            "geometry_multidepth": self.ui.geometry_defaults_form.geometry_opt_group.multidepth_cb,
-
-            # Geometry Advanced Options
-            "geometry_toolchangexy": self.ui.geometry_defaults_form.geometry_adv_opt_group.toolchangexy_entry,
-            "geometry_startz": self.ui.geometry_defaults_form.geometry_adv_opt_group.gstartz_entry,
-            "geometry_endz": self.ui.geometry_defaults_form.geometry_adv_opt_group.gendz_entry,
-            "geometry_feedrate_rapid": self.ui.geometry_defaults_form.geometry_adv_opt_group.cncfeedrate_rapid_entry,
-            "geometry_extracut": self.ui.geometry_defaults_form.geometry_adv_opt_group.extracut_cb,
-            "geometry_z_pdepth": self.ui.geometry_defaults_form.geometry_adv_opt_group.pdepth_entry,
-            "geometry_feedrate_probe": self.ui.geometry_defaults_form.geometry_adv_opt_group.feedrate_probe_entry,
-            "geometry_f_plunge": self.ui.geometry_defaults_form.geometry_adv_opt_group.fplunge_cb,
-            "geometry_segx": self.ui.geometry_defaults_form.geometry_adv_opt_group.segx_entry,
-            "geometry_segy": self.ui.geometry_defaults_form.geometry_adv_opt_group.segy_entry,
-
-            # Geometry Editor
-            "geometry_editor_sel_limit": self.ui.geometry_defaults_form.geometry_editor_group.sel_limit_entry,
-
-            # CNCJob General
-            "cncjob_plot": self.ui.cncjob_defaults_form.cncjob_gen_group.plot_cb,
-            "cncjob_plot_kind": self.ui.cncjob_defaults_form.cncjob_gen_group.cncplot_method_radio,
-            "cncjob_annotation": self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_cb,
-            "cncjob_annotation_fontsize": self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontsize_sp,
-            "cncjob_annotation_fontcolor": self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_entry,
-
-            "cncjob_tooldia": self.ui.cncjob_defaults_form.cncjob_gen_group.tooldia_entry,
-            "cncjob_coords_decimals": self.ui.cncjob_defaults_form.cncjob_gen_group.coords_dec_entry,
-            "cncjob_fr_decimals": self.ui.cncjob_defaults_form.cncjob_gen_group.fr_dec_entry,
-            "cncjob_steps_per_circle": self.ui.cncjob_defaults_form.cncjob_gen_group.steps_per_circle_entry,
-
-            # CNC Job Options
-            "cncjob_prepend": self.ui.cncjob_defaults_form.cncjob_opt_group.prepend_text,
-            "cncjob_append": self.ui.cncjob_defaults_form.cncjob_opt_group.append_text,
-
-            # CNC Job Advanced Options
-            "cncjob_toolchange_macro": self.ui.cncjob_defaults_form.cncjob_adv_opt_group.toolchange_text,
-            "cncjob_toolchange_macro_enable": self.ui.cncjob_defaults_form.cncjob_adv_opt_group.toolchange_cb,
-
-            # NCC Tool
-            "tools_ncctools": self.ui.tools_defaults_form.tools_ncc_group.ncc_tool_dia_entry,
-            "tools_nccoverlap": self.ui.tools_defaults_form.tools_ncc_group.ncc_overlap_entry,
-            "tools_nccmargin": self.ui.tools_defaults_form.tools_ncc_group.ncc_margin_entry,
-            "tools_nccmethod": self.ui.tools_defaults_form.tools_ncc_group.ncc_method_radio,
-            "tools_nccconnect": self.ui.tools_defaults_form.tools_ncc_group.ncc_connect_cb,
-            "tools_ncccontour": self.ui.tools_defaults_form.tools_ncc_group.ncc_contour_cb,
-            "tools_nccrest": self.ui.tools_defaults_form.tools_ncc_group.ncc_rest_cb,
-            "tools_nccref": self.ui.tools_defaults_form.tools_ncc_group.reference_radio,
-
-            # CutOut Tool
-            "tools_cutouttooldia": self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry,
-            "tools_cutoutkind": self.ui.tools_defaults_form.tools_cutout_group.obj_kind_combo,
-            "tools_cutoutmargin": self.ui.tools_defaults_form.tools_cutout_group.cutout_margin_entry,
-            "tools_cutoutgapsize": self.ui.tools_defaults_form.tools_cutout_group.cutout_gap_entry,
-            "tools_gaps_ff": self.ui.tools_defaults_form.tools_cutout_group.gaps_combo,
-            "tools_cutout_convexshape": self.ui.tools_defaults_form.tools_cutout_group.convex_box,
-
-            # Paint Area Tool
-            "tools_painttooldia": self.ui.tools_defaults_form.tools_paint_group.painttooldia_entry,
-            "tools_paintoverlap": self.ui.tools_defaults_form.tools_paint_group.paintoverlap_entry,
-            "tools_paintmargin": self.ui.tools_defaults_form.tools_paint_group.paintmargin_entry,
-            "tools_paintmethod": self.ui.tools_defaults_form.tools_paint_group.paintmethod_combo,
-            "tools_selectmethod": self.ui.tools_defaults_form.tools_paint_group.selectmethod_combo,
-            "tools_pathconnect": self.ui.tools_defaults_form.tools_paint_group.pathconnect_cb,
-            "tools_paintcontour": self.ui.tools_defaults_form.tools_paint_group.contour_cb,
-
-            # 2-sided Tool
-            "tools_2sided_mirror_axis": self.ui.tools_defaults_form.tools_2sided_group.mirror_axis_radio,
-            "tools_2sided_axis_loc": self.ui.tools_defaults_form.tools_2sided_group.axis_location_radio,
-            "tools_2sided_drilldia": self.ui.tools_defaults_form.tools_2sided_group.drill_dia_entry,
-
-            # Film Tool
-            "tools_film_type": self.ui.tools_defaults_form.tools_film_group.film_type_radio,
-            "tools_film_boundary": self.ui.tools_defaults_form.tools_film_group.film_boundary_entry,
-            "tools_film_scale": self.ui.tools_defaults_form.tools_film_group.film_scale_entry,
-
-            # Panelize Tool
-            "tools_panelize_spacing_columns": self.ui.tools_defaults_form.tools_panelize_group.pspacing_columns,
-            "tools_panelize_spacing_rows": self.ui.tools_defaults_form.tools_panelize_group.pspacing_rows,
-            "tools_panelize_columns": self.ui.tools_defaults_form.tools_panelize_group.pcolumns,
-            "tools_panelize_rows": self.ui.tools_defaults_form.tools_panelize_group.prows,
-            "tools_panelize_constrain": self.ui.tools_defaults_form.tools_panelize_group.pconstrain_cb,
-            "tools_panelize_constrainx": self.ui.tools_defaults_form.tools_panelize_group.px_width_entry,
-            "tools_panelize_constrainy": self.ui.tools_defaults_form.tools_panelize_group.py_height_entry,
-            "tools_panelize_panel_type": self.ui.tools_defaults_form.tools_panelize_group.panel_type_radio,
-
-            # Calculators Tool
-            "tools_calc_vshape_tip_dia": self.ui.tools_defaults_form.tools_calculators_group.tip_dia_entry,
-            "tools_calc_vshape_tip_angle": self.ui.tools_defaults_form.tools_calculators_group.tip_angle_entry,
-            "tools_calc_vshape_cut_z": self.ui.tools_defaults_form.tools_calculators_group.cut_z_entry,
-            "tools_calc_electro_length": self.ui.tools_defaults_form.tools_calculators_group.pcblength_entry,
-            "tools_calc_electro_width": self.ui.tools_defaults_form.tools_calculators_group.pcbwidth_entry,
-            "tools_calc_electro_cdensity": self.ui.tools_defaults_form.tools_calculators_group.cdensity_entry,
-            "tools_calc_electro_growth": self.ui.tools_defaults_form.tools_calculators_group.growth_entry,
-
-            # Transformations Tool
-            "tools_transform_rotate": self.ui.tools_defaults_form.tools_transform_group.rotate_entry,
-            "tools_transform_skew_x": self.ui.tools_defaults_form.tools_transform_group.skewx_entry,
-            "tools_transform_skew_y": self.ui.tools_defaults_form.tools_transform_group.skewy_entry,
-            "tools_transform_scale_x": self.ui.tools_defaults_form.tools_transform_group.scalex_entry,
-            "tools_transform_scale_y": self.ui.tools_defaults_form.tools_transform_group.scaley_entry,
-            "tools_transform_scale_link": self.ui.tools_defaults_form.tools_transform_group.link_cb,
-            "tools_transform_scale_reference": self.ui.tools_defaults_form.tools_transform_group.reference_cb,
-            "tools_transform_offset_x": self.ui.tools_defaults_form.tools_transform_group.offx_entry,
-            "tools_transform_offset_y": self.ui.tools_defaults_form.tools_transform_group.offy_entry,
-            "tools_transform_mirror_reference": self.ui.tools_defaults_form.tools_transform_group.mirror_reference_cb,
-            "tools_transform_mirror_point": self.ui.tools_defaults_form.tools_transform_group.flip_ref_entry,
-
-            # SolderPaste Dispensing Tool
-            "tools_solderpaste_tools": self.ui.tools_defaults_form.tools_solderpaste_group.nozzle_tool_dia_entry,
-            "tools_solderpaste_new": self.ui.tools_defaults_form.tools_solderpaste_group.addtool_entry,
-            "tools_solderpaste_z_start": self.ui.tools_defaults_form.tools_solderpaste_group.z_start_entry,
-            "tools_solderpaste_z_dispense": self.ui.tools_defaults_form.tools_solderpaste_group.z_dispense_entry,
-            "tools_solderpaste_z_stop": self.ui.tools_defaults_form.tools_solderpaste_group.z_stop_entry,
-            "tools_solderpaste_z_travel": self.ui.tools_defaults_form.tools_solderpaste_group.z_travel_entry,
-            "tools_solderpaste_z_toolchange": self.ui.tools_defaults_form.tools_solderpaste_group.z_toolchange_entry,
-            "tools_solderpaste_xy_toolchange": self.ui.tools_defaults_form.tools_solderpaste_group.xy_toolchange_entry,
-            "tools_solderpaste_frxy": self.ui.tools_defaults_form.tools_solderpaste_group.frxy_entry,
-            "tools_solderpaste_frz": self.ui.tools_defaults_form.tools_solderpaste_group.frz_entry,
-            "tools_solderpaste_frz_dispense": self.ui.tools_defaults_form.tools_solderpaste_group.frz_dispense_entry,
-            "tools_solderpaste_speedfwd": self.ui.tools_defaults_form.tools_solderpaste_group.speedfwd_entry,
-            "tools_solderpaste_dwellfwd": self.ui.tools_defaults_form.tools_solderpaste_group.dwellfwd_entry,
-            "tools_solderpaste_speedrev": self.ui.tools_defaults_form.tools_solderpaste_group.speedrev_entry,
-            "tools_solderpaste_dwellrev": self.ui.tools_defaults_form.tools_solderpaste_group.dwellrev_entry,
-            "tools_solderpaste_pp": self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo
-
-        }
-
-        # ############################
-        # ### LOAD POSTPROCESSORS ####
-        # ############################
-
-        self.postprocessors = load_postprocessors(self)
-
-        for name in list(self.postprocessors.keys()):
-            # 'Paste' postprocessors are to be used only in the Solder Paste Dispensing Tool
-            if name.partition('_')[0] == 'Paste':
-                self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo.addItem(name)
-                continue
-
-            self.ui.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb.addItem(name)
-            # HPGL postprocessor is only for Geometry objects therefore it should not be in the Excellon Preferences
-            if name == 'hpgl':
-                continue
-
-            self.ui.excellon_defaults_form.excellon_opt_group.pp_excellon_name_cb.addItem(name)
-
-        # ############################
-        # ### LOAD LANGUAGES      ####
-        # ############################
-
-        self.languages = fcTranslate.load_languages()
-        for name in sorted(self.languages.values()):
-            self.ui.general_defaults_form.general_app_group.language_cb.addItem(name)
-
-        self.defaults = LoudDict()
-        self.defaults.set_change_callback(self.on_defaults_dict_change)  # When the dictionary changes.
-        self.defaults.update({
-            # Global APP Preferences
-            "global_serial": 0,
-            "global_stats": {},
-            "units": "IN",
-            "global_app_level": 'b',
-            "global_language": 'English',
-            "global_version_check": True,
-            "global_send_stats": True,
-            "global_pan_button": '2',
-            "global_mselect_key": 'Control',
-            "global_project_at_startup": False,
-            "global_project_autohide": True,
-            "global_toggle_tooltips": True,
-            "global_worker_number": 2,
-            "global_tolerance": 0.01,
-            "global_open_style": True,
-            "global_compression_level": 3,
-            "global_save_compressed": True,
-
-            # Global GUI Preferences
-            "global_gridx": 0.0393701,
-            "global_gridy": 0.0393701,
-            "global_snap_max": 0.001968504,
-            "global_workspace": False,
-            "global_workspaceT": "A4P",
-
-            "global_grid_context_menu": {
-                'in': [0.01, 0.02, 0.025, 0.05, 0.1],
-                'mm': [0.1, 0.2, 0.5, 1, 2.54]
-            },
-
-            "global_plot_fill": '#BBF268BF',
-            "global_plot_line": '#006E20BF',
-            "global_sel_fill": '#a5a5ffbf',
-            "global_sel_line": '#0000ffbf',
-            "global_alt_sel_fill": '#BBF268BF',
-            "global_alt_sel_line": '#006E20BF',
-            "global_draw_color": '#FF0000',
-            "global_sel_draw_color": '#0000FF',
-            "global_proj_item_color": '#000000',
-            "global_proj_item_dis_color": '#b7b7cb',
-
-            "global_toolbar_view": 511,
-
-            "global_background_timeout": 300000,  # Default value is 5 minutes
-            "global_verbose_error_level": 0,  # Shell verbosity 0 = default
-            # (python trace only for unknown errors),
-            # 1 = show trace(show trace always),
-            # 2 = (For the future).
-
-            # Persistence
-            "global_last_folder": None,
-            "global_last_save_folder": None,
-
-            # Default window geometry
-            "global_def_win_x": 100,
-            "global_def_win_y": 100,
-            "global_def_win_w": 1024,
-            "global_def_win_h": 650,
-            "global_def_notebook_width": 1,
-            # Constants...
-            "global_defaults_save_period_ms": 20000,  # Time between default saves.
-            "global_shell_shape": [500, 300],  # Shape of the shell in pixels.
-            "global_shell_at_startup": False,  # Show the shell at startup.
-            "global_recent_limit": 10,  # Max. items in recent list.
-
-            "fit_key": 'V',
-            "zoom_out_key": '-',
-            "zoom_in_key": '=',
-            "grid_toggle_key": 'G',
-            "global_zoom_ratio": 1.5,
-            "global_point_clipboard_format": "(%.4f, %.4f)",
-            "global_zdownrate": None,
-
-            # General GUI Settings
-            "global_hover": False,
-            "global_selection_shape": True,
-            "global_layout": "compact",
-            # Gerber General
-            "gerber_plot": True,
-            "gerber_solid": True,
-            "gerber_multicolored": False,
-            "gerber_isotooldia": 0.016,
-            "gerber_isopasses": 1,
-            "gerber_isooverlap": 0.15,
-
-            # Gerber Options
-            "gerber_combine_passes": False,
-            "gerber_milling_type": "cl",
-            "gerber_noncoppermargin": 0.1,
-            "gerber_noncopperrounded": False,
-            "gerber_bboxmargin": 0.1,
-            "gerber_bboxrounded": False,
-            "gerber_circle_steps": 128,
-            "gerber_use_buffer_for_union": True,
-
-            # Gerber Advanced Options
-            "gerber_aperture_display": False,
-            "gerber_aperture_scale_factor": 1.0,
-            "gerber_aperture_buffer_factor": 0.0,
-            "gerber_follow": False,
-
-            # Gerber Export
-            "gerber_exp_units": 'IN',
-            "gerber_exp_integer": 2,
-            "gerber_exp_decimals": 4,
-            "gerber_exp_zeros": 'L',
-
-            # Gerber Editor
-            "gerber_editor_sel_limit": 30,
-
-            # Excellon General
-            "excellon_plot": True,
-            "excellon_solid": True,
-            "excellon_format_upper_in": 2,
-            "excellon_format_lower_in": 4,
-            "excellon_format_upper_mm": 3,
-            "excellon_format_lower_mm": 3,
-            "excellon_zeros": "L",
-            "excellon_units": "INCH",
-            "excellon_optimization_type": 'B',
-            "excellon_search_time": 3,
-
-            # Excellon Options
-            "excellon_drillz": -0.1,
-            "excellon_travelz": 0.1,
-            "excellon_feedrate": 3.0,
-            "excellon_spindlespeed": None,
-            "excellon_spindledir": 'CW',
-            "excellon_dwell": False,
-            "excellon_dwelltime": 1,
-            "excellon_toolchange": False,
-            "excellon_toolchangez": 0.5,
-            "excellon_ppname_e": 'default',
-            "excellon_tooldia": 0.016,
-            "excellon_slot_tooldia": 0.016,
-            "excellon_gcode_type": "drills",
-
-            # Excellon Advanced Options
-            "excellon_offset": 0.0,
-            "excellon_toolchangexy": "0.0, 0.0",
-            "excellon_startz": None,
-            "excellon_endz": 0.5,
-            "excellon_feedrate_rapid": 3.0,
-            "excellon_z_pdepth": -0.02,
-            "excellon_feedrate_probe": 3.0,
-            "excellon_f_plunge": False,
-            "excellon_f_retract": False,
-
-            # Excellon Export
-            "excellon_exp_units": 'INCH',
-            "excellon_exp_format": 'ndec',
-            "excellon_exp_integer": 2,
-            "excellon_exp_decimals": 4,
-            "excellon_exp_zeros": 'LZ',
-
-            # Excellon Editor
-            "excellon_editor_sel_limit": 30,
-            "excellon_editor_newdia": 0.039,
-            "excellon_editor_array_size": 5,
-            "excellon_editor_lin_dir": 'X',
-            "excellon_editor_lin_pitch": 0.1,
-            "excellon_editor_lin_angle": 0.0,
-            "excellon_editor_circ_dir": 'CW',
-            "excellon_editor_circ_angle": 12,
-
-            # Geometry General
-            "geometry_plot": True,
-            "geometry_circle_steps": 128,
-            "geometry_cnctooldia": "0.016",
-
-            # Geometry Options
-            "geometry_cutz": -0.002,
-            "geometry_vtipdia": 0.1,
-            "geometry_vtipangle": 30,
-            "geometry_multidepth": False,
-            "geometry_depthperpass": 0.002,
-            "geometry_travelz": 0.1,
-            "geometry_toolchange": False,
-            "geometry_toolchangez": 0.5,
-            "geometry_feedrate": 3.0,
-            "geometry_feedrate_z": 3.0,
-            "geometry_spindlespeed": None,
-            "geometry_spindledir": 'CW',
-            "geometry_dwell": False,
-            "geometry_dwelltime": 1,
-            "geometry_ppname_g": 'default',
-
-            # Geometry Advanced Options
-            "geometry_toolchangexy": "0.0, 0.0",
-            "geometry_startz": None,
-            "geometry_endz": 0.5,
-            "geometry_feedrate_rapid": 3.0,
-            "geometry_extracut": False,
-            "geometry_z_pdepth": -0.02,
-            "geometry_f_plunge": False,
-            "geometry_feedrate_probe": 3.0,
-            "geometry_segx": 0.0,
-            "geometry_segy": 0.0,
-
-            # Geometry Editor
-            "geometry_editor_sel_limit": 30,
-
-            # CNC Job General
-            "cncjob_plot": True,
-            "cncjob_plot_kind": 'all',
-            "cncjob_annotation": True,
-            "cncjob_annotation_fontsize": 9,
-            "cncjob_annotation_fontcolor": '#990000',
-            "cncjob_tooldia": 0.0393701,
-            "cncjob_coords_decimals": 4,
-            "cncjob_fr_decimals": 2,
-            "cncjob_steps_per_circle": 128,
-
-            # CNC Job Options
-            "cncjob_prepend": "",
-            "cncjob_append": "",
-
-            # CNC Job Advanced Options
-            "cncjob_toolchange_macro": "",
-            "cncjob_toolchange_macro_enable": False,
-
-            "tools_ncctools": "0.0393701, 0.019685",
-            "tools_nccoverlap": 0.015748,
-            "tools_nccmargin": 0.00393701,
-            "tools_nccmethod": "seed",
-            "tools_nccconnect": True,
-            "tools_ncccontour": True,
-            "tools_nccrest": False,
-            "tools_nccref": 'itself',
-
-            "tools_cutouttooldia": 0.00393701,
-            "tools_cutoutkind": "single",
-            "tools_cutoutmargin": 0.00393701,
-            "tools_cutoutgapsize": 0.005905512,
-            "tools_gaps_ff": "8",
-            "tools_cutout_convexshape": False,
-
-            "tools_painttooldia": 0.07,
-            "tools_paintoverlap": 0.15,
-            "tools_paintmargin": 0.0,
-            "tools_paintmethod": "seed",
-            "tools_selectmethod": "single",
-            "tools_pathconnect": True,
-            "tools_paintcontour": True,
-
-            "tools_2sided_mirror_axis": "X",
-            "tools_2sided_axis_loc": "point",
-            "tools_2sided_drilldia": 0.0393701,
-
-            "tools_film_type": 'neg',
-            "tools_film_boundary": 0.0393701,
-            "tools_film_scale": 0,
-
-            "tools_panelize_spacing_columns": 0,
-            "tools_panelize_spacing_rows": 0,
-            "tools_panelize_columns": 1,
-            "tools_panelize_rows": 1,
-            "tools_panelize_constrain": False,
-            "tools_panelize_constrainx": 0.0,
-            "tools_panelize_constrainy": 0.0,
-            "tools_panelize_panel_type": 'gerber',
-
-            "tools_calc_vshape_tip_dia": 0.007874,
-            "tools_calc_vshape_tip_angle": 30,
-            "tools_calc_vshape_cut_z": 0.000787,
-            "tools_calc_electro_length": 10.0,
-            "tools_calc_electro_width": 10.0,
-            "tools_calc_electro_cdensity": 13.0,
-            "tools_calc_electro_growth": 10.0,
-
-            "tools_transform_rotate": 90,
-            "tools_transform_skew_x": 0.0,
-            "tools_transform_skew_y": 0.0,
-            "tools_transform_scale_x": 1.0,
-            "tools_transform_scale_y": 1.0,
-            "tools_transform_scale_link": True,
-            "tools_transform_scale_reference": True,
-            "tools_transform_offset_x": 0.0,
-            "tools_transform_offset_y": 0.0,
-            "tools_transform_mirror_reference": False,
-            "tools_transform_mirror_point": (0, 0),
-
-            "tools_solderpaste_tools": "0.0393701, 0.011811",
-            "tools_solderpaste_new": 0.011811,
-            "tools_solderpaste_z_start": 0.00019685039,
-            "tools_solderpaste_z_dispense": 0.00393701,
-            "tools_solderpaste_z_stop": 0.00019685039,
-            "tools_solderpaste_z_travel": 0.00393701,
-            "tools_solderpaste_z_toolchange": 0.0393701,
-            "tools_solderpaste_xy_toolchange": "0.0, 0.0",
-            "tools_solderpaste_frxy": 3.0,
-            "tools_solderpaste_frz": 3.0,
-            "tools_solderpaste_frz_dispense": 0.0393701,
-            "tools_solderpaste_speedfwd": 20,
-            "tools_solderpaste_dwellfwd": 1,
-            "tools_solderpaste_speedrev": 10,
-            "tools_solderpaste_dwellrev": 1,
-            "tools_solderpaste_pp": 'Paste_1'
-        })
-
-        # ##############################
-        # ## Load defaults from file ###
-        # ##############################
-
-        if user_defaults:
-            self.load_defaults(filename='current_defaults')
-
-        # ###########################
-        # #### APPLY APP LANGUAGE ###
-        # ###########################
-
-        ret_val = fcTranslate.apply_language('strings')
-
-        if ret_val == "no language":
-            self.inform.emit(_("[ERROR] Could not find the Language files. The App strings are missing."))
-            log.debug("Could not find the Language files. The App strings are missing.")
-        else:
-            # make the current language the current selection on the language combobox
-            self.ui.general_defaults_form.general_app_group.language_cb.setCurrentText(ret_val)
-            log.debug("App.__init__() --> Applied %s language." % str(ret_val).capitalize())
-
-        # ##################################
-        # ### CREATE UNIQUE SERIAL NUMBER ##
-        # ##################################
-
-        chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
-        if self.defaults['global_serial'] == 0 or len(str(self.defaults['global_serial'])) < 10:
-            self.defaults['global_serial'] = ''.join([random.choice(chars) for i in range(20)])
-            self.save_defaults(silent=True)
-
-        self.propagate_defaults(silent=True)
-        self.restore_main_win_geom()
-
-        def auto_save_defaults():
-            try:
-                self.save_defaults(silent=True)
-                self.propagate_defaults(silent=True)
-            finally:
-                QtCore.QTimer.singleShot(self.defaults["global_defaults_save_period_ms"], auto_save_defaults)
-
-        # the following lines activates automatic defaults save
-        # if user_defaults:
-        #     QtCore.QTimer.singleShot(self.defaults["global_defaults_save_period_ms"], auto_save_defaults)
-
-        # self.options_form = PreferencesUI()
-
-        self.options_form_fields = {
-            "units": self.ui.general_options_form.general_app_group.units_radio,
-            "global_gridx": self.ui.general_options_form.general_gui_group.gridx_entry,
-            "global_gridy": self.ui.general_options_form.general_gui_group.gridy_entry,
-            "global_snap_max": self.ui.general_options_form.general_gui_group.snap_max_dist_entry,
-
-            "gerber_plot": self.ui.gerber_options_form.gerber_gen_group.plot_cb,
-            "gerber_solid": self.ui.gerber_options_form.gerber_gen_group.solid_cb,
-            "gerber_multicolored": self.ui.gerber_options_form.gerber_gen_group.multicolored_cb,
-
-            "gerber_isotooldia": self.ui.gerber_options_form.gerber_opt_group.iso_tool_dia_entry,
-            "gerber_isopasses": self.ui.gerber_options_form.gerber_opt_group.iso_width_entry,
-            "gerber_isooverlap": self.ui.gerber_options_form.gerber_opt_group.iso_overlap_entry,
-            "gerber_combine_passes": self.ui.gerber_options_form.gerber_opt_group.combine_passes_cb,
-            "gerber_noncoppermargin": self.ui.gerber_options_form.gerber_opt_group.noncopper_margin_entry,
-            "gerber_noncopperrounded": self.ui.gerber_options_form.gerber_opt_group.noncopper_rounded_cb,
-            "gerber_bboxmargin": self.ui.gerber_options_form.gerber_opt_group.bbmargin_entry,
-            "gerber_bboxrounded": self.ui.gerber_options_form.gerber_opt_group.bbrounded_cb,
-
-            "excellon_plot": self.ui.excellon_options_form.excellon_gen_group.plot_cb,
-            "excellon_solid": self.ui.excellon_options_form.excellon_gen_group.solid_cb,
-            "excellon_format_upper_in": self.ui.excellon_options_form.excellon_gen_group.excellon_format_upper_in_entry,
-            "excellon_format_lower_in": self.ui.excellon_options_form.excellon_gen_group.excellon_format_lower_in_entry,
-            "excellon_format_upper_mm": self.ui.excellon_options_form.excellon_gen_group.excellon_format_upper_mm_entry,
-            "excellon_format_lower_mm": self.ui.excellon_options_form.excellon_gen_group.excellon_format_lower_mm_entry,
-            "excellon_zeros": self.ui.excellon_options_form.excellon_gen_group.excellon_zeros_radio,
-            "excellon_units": self.ui.excellon_options_form.excellon_gen_group.excellon_units_radio,
-            "excellon_optimization_type": self.ui.excellon_options_form.excellon_gen_group.excellon_optimization_radio,
-
-            "excellon_drillz": self.ui.excellon_options_form.excellon_opt_group.cutz_entry,
-            "excellon_travelz": self.ui.excellon_options_form.excellon_opt_group.travelz_entry,
-            "excellon_feedrate": self.ui.excellon_options_form.excellon_opt_group.feedrate_entry,
-            "excellon_spindlespeed": self.ui.excellon_options_form.excellon_opt_group.spindlespeed_entry,
-            "excellon_spindledir": self.ui.excellon_options_form.excellon_opt_group.spindledir_radio,
-            "excellon_dwell": self.ui.excellon_options_form.excellon_opt_group.dwell_cb,
-            "excellon_dwelltime": self.ui.excellon_options_form.excellon_opt_group.dwelltime_entry,
-            "excellon_toolchange": self.ui.excellon_options_form.excellon_opt_group.toolchange_cb,
-            "excellon_toolchangez": self.ui.excellon_options_form.excellon_opt_group.toolchangez_entry,
-            "excellon_tooldia": self.ui.excellon_options_form.excellon_opt_group.tooldia_entry,
-            "excellon_ppname_e": self.ui.excellon_options_form.excellon_opt_group.pp_excellon_name_cb,
-
-            "excellon_feedrate_rapid": self.ui.excellon_options_form.excellon_adv_opt_group.feedrate_rapid_entry,
-            "excellon_toolchangexy": self.ui.excellon_options_form.excellon_adv_opt_group.toolchangexy_entry,
-            "excellon_f_plunge": self.ui.excellon_options_form.excellon_adv_opt_group.fplunge_cb,
-            "excellon_startz": self.ui.excellon_options_form.excellon_adv_opt_group.estartz_entry,
-            "excellon_endz": self.ui.excellon_options_form.excellon_adv_opt_group.eendz_entry,
-
-            "geometry_plot": self.ui.geometry_options_form.geometry_gen_group.plot_cb,
-            "geometry_cnctooldia": self.ui.geometry_options_form.geometry_gen_group.cnctooldia_entry,
-
-            "geometry_cutz": self.ui.geometry_options_form.geometry_opt_group.cutz_entry,
-            "geometry_travelz": self.ui.geometry_options_form.geometry_opt_group.travelz_entry,
-            "geometry_feedrate": self.ui.geometry_options_form.geometry_opt_group.cncfeedrate_entry,
-            "geometry_feedrate_z": self.ui.geometry_options_form.geometry_opt_group.cncplunge_entry,
-            "geometry_spindlespeed": self.ui.geometry_options_form.geometry_opt_group.cncspindlespeed_entry,
-            "geometry_spindledir": self.ui.geometry_options_form.geometry_opt_group.spindledir_radio,
-            "geometry_dwell": self.ui.geometry_options_form.geometry_opt_group.dwell_cb,
-            "geometry_dwelltime": self.ui.geometry_options_form.geometry_opt_group.dwelltime_entry,
-            "geometry_ppname_g": self.ui.geometry_options_form.geometry_opt_group.pp_geometry_name_cb,
-            "geometry_toolchange": self.ui.geometry_options_form.geometry_opt_group.toolchange_cb,
-            "geometry_toolchangez": self.ui.geometry_options_form.geometry_opt_group.toolchangez_entry,
-            "geometry_depthperpass": self.ui.geometry_options_form.geometry_opt_group.depthperpass_entry,
-            "geometry_multidepth": self.ui.geometry_options_form.geometry_opt_group.multidepth_cb,
-
-            "geometry_segx": self.ui.geometry_options_form.geometry_adv_opt_group.segx_entry,
-            "geometry_segy": self.ui.geometry_options_form.geometry_adv_opt_group.segy_entry,
-            "geometry_feedrate_rapid": self.ui.geometry_options_form.geometry_adv_opt_group.cncfeedrate_rapid_entry,
-            "geometry_f_plunge": self.ui.geometry_options_form.geometry_adv_opt_group.fplunge_cb,
-            "geometry_toolchangexy": self.ui.geometry_options_form.geometry_adv_opt_group.toolchangexy_entry,
-            "geometry_startz": self.ui.geometry_options_form.geometry_adv_opt_group.gstartz_entry,
-            "geometry_endz": self.ui.geometry_options_form.geometry_adv_opt_group.gendz_entry,
-            "geometry_extracut": self.ui.geometry_options_form.geometry_adv_opt_group.extracut_cb,
-
-            "cncjob_plot": self.ui.cncjob_options_form.cncjob_gen_group.plot_cb,
-            "cncjob_tooldia": self.ui.cncjob_options_form.cncjob_gen_group.tooldia_entry,
-
-            "cncjob_prepend": self.ui.cncjob_options_form.cncjob_opt_group.prepend_text,
-            "cncjob_append": self.ui.cncjob_options_form.cncjob_opt_group.append_text,
-
-            "tools_ncctools": self.ui.tools_options_form.tools_ncc_group.ncc_tool_dia_entry,
-            "tools_nccoverlap": self.ui.tools_options_form.tools_ncc_group.ncc_overlap_entry,
-            "tools_nccmargin": self.ui.tools_options_form.tools_ncc_group.ncc_margin_entry,
-
-            "tools_cutouttooldia": self.ui.tools_options_form.tools_cutout_group.cutout_tooldia_entry,
-            "tools_cutoutmargin": self.ui.tools_options_form.tools_cutout_group.cutout_margin_entry,
-            "tools_cutoutgapsize": self.ui.tools_options_form.tools_cutout_group.cutout_gap_entry,
-            "tools_gaps_ff": self.ui.tools_options_form.tools_cutout_group.gaps_combo,
-
-            "tools_painttooldia": self.ui.tools_options_form.tools_paint_group.painttooldia_entry,
-            "tools_paintoverlap": self.ui.tools_options_form.tools_paint_group.paintoverlap_entry,
-            "tools_paintmargin": self.ui.tools_options_form.tools_paint_group.paintmargin_entry,
-            "tools_paintmethod": self.ui.tools_options_form.tools_paint_group.paintmethod_combo,
-            "tools_selectmethod": self.ui.tools_options_form.tools_paint_group.selectmethod_combo,
-            "tools_pathconnect": self.ui.tools_options_form.tools_paint_group.pathconnect_cb,
-            "tools_paintcontour": self.ui.tools_options_form.tools_paint_group.contour_cb,
-
-            "tools_2sided_mirror_axis": self.ui.tools_options_form.tools_2sided_group.mirror_axis_radio,
-            "tools_2sided_axis_loc": self.ui.tools_options_form.tools_2sided_group.axis_location_radio,
-            "tools_2sided_drilldia": self.ui.tools_options_form.tools_2sided_group.drill_dia_entry,
-
-            "tools_film_type": self.ui.tools_options_form.tools_film_group.film_type_radio,
-            "tools_film_boundary": self.ui.tools_options_form.tools_film_group.film_boundary_entry,
-            "tools_film_scale": self.ui.tools_options_form.tools_film_group.film_scale_entry,
-
-            "tools_panelize_spacing_columns": self.ui.tools_options_form.tools_panelize_group.pspacing_columns,
-            "tools_panelize_spacing_rows": self.ui.tools_options_form.tools_panelize_group.pspacing_rows,
-            "tools_panelize_columns": self.ui.tools_options_form.tools_panelize_group.pcolumns,
-            "tools_panelize_rows": self.ui.tools_options_form.tools_panelize_group.prows,
-            "tools_panelize_constrain": self.ui.tools_options_form.tools_panelize_group.pconstrain_cb,
-            "tools_panelize_constrainx": self.ui.tools_options_form.tools_panelize_group.px_width_entry,
-            "tools_panelize_constrainy": self.ui.tools_options_form.tools_panelize_group.py_height_entry
-
-        }
-
-        for name in list(self.postprocessors.keys()):
-            self.ui.geometry_options_form.geometry_opt_group.pp_geometry_name_cb.addItem(name)
-            self.ui.excellon_options_form.excellon_opt_group.pp_excellon_name_cb.addItem(name)
-
-        self.options = LoudDict()
-        self.options.set_change_callback(self.on_options_dict_change)
-        self.options.update({
-            "units": "IN",
-            "global_gridx": 1.0,
-            "global_gridy": 1.0,
-            "global_snap_max": 0.05,
-            "global_background_timeout": 300000,  # Default value is 5 minutes
-            "global_verbose_error_level": 0,  # Shell verbosity:
-            # 0 = default(python trace only for unknown errors),
-            # 1 = show trace(show trace allways), 2 = (For the future).
-
-            "gerber_plot": True,
-            "gerber_solid": True,
-            "gerber_multicolored": False,
-            "gerber_isotooldia": 0.016,
-            "gerber_isopasses": 1,
-            "gerber_isooverlap": 0.15,
-            "gerber_combine_passes": True,
-            "gerber_noncoppermargin": 0.0,
-            "gerber_noncopperrounded": False,
-            "gerber_bboxmargin": 0.0,
-            "gerber_bboxrounded": False,
-
-            "excellon_plot": True,
-            "excellon_solid": False,
-            "excellon_format_upper_in": 2,
-            "excellon_format_lower_in": 4,
-            "excellon_format_upper_mm": 3,
-            "excellon_format_lower_mm": 3,
-            "excellon_units": 'INCH',
-            "excellon_optimization_type": 'B',
-            "excellon_search_time": 3,
-            "excellon_zeros": "L",
-
-            "excellon_drillz": -0.1,
-            "excellon_travelz": 0.1,
-            "excellon_feedrate": 3.0,
-            "excellon_feedrate_rapid": 3.0,
-            "excellon_spindlespeed": None,
-            "excellon_spindledir": 'CW',
-            "excellon_dwell": True,
-            "excellon_dwelltime": 1000,
-            "excellon_toolchange": False,
-            "excellon_toolchangez": 1.0,
-            "excellon_toolchangexy": "0.0, 0.0",
-            "excellon_tooldia": 0.016,
-            "excellon_ppname_e": 'default',
-            "excellon_f_plunge": False,
-            "excellon_startz": None,
-            "excellon_endz": 2.0,
-
-            "geometry_plot": True,
-            "geometry_segx": 0.0,
-            "geometry_segy": 0.0,
-            "geometry_cutz": -0.002,
-            "geometry_vtipdia": 0.1,
-            "geometry_vtipangle": 30,
-            "geometry_travelz": 0.1,
-            "geometry_feedrate": 3.0,
-            "geometry_feedrate_z": 3.0,
-            "geometry_feedrate_rapid": 3.0,
-            "geometry_spindlespeed": None,
-            "geometry_spindledir": 'CW',
-            "geometry_dwell": True,
-            "geometry_dwelltime": 1000,
-            "geometry_cnctooldia": 0.016,
-            "geometry_toolchange": False,
-            "geometry_toolchangez": 2.0,
-            "geometry_toolchangexy": "0.0, 0.0",
-            "geometry_startz": None,
-            "geometry_endz": 2.0,
-            "geometry_ppname_g": "default",
-            "geometry_f_plunge": False,
-            "geometry_depthperpass": 0.002,
-            "geometry_multidepth": False,
-            "geometry_extracut": False,
-
-            "cncjob_plot": True,
-            "cncjob_tooldia": 0.016,
-            "cncjob_prepend": "",
-            "cncjob_append": "",
-
-            "tools_ncctools": "1.0, 0.5",
-            "tools_nccoverlap": 0.4,
-            "tools_nccmargin": 1,
-
-            "tools_cutouttooldia": 0.07,
-            "tools_cutoutmargin": 0.1,
-            "tools_cutoutgapsize": 0.15,
-            "tools_gaps_ff": "8",
-
-            "tools_painttooldia": 0.07,
-            "tools_paintoverlap": 0.15,
-            "tools_paintmargin": 0.0,
-            "tools_paintmethod": "seed",
-            "tools_selectmethod": "single",
-            "tools_pathconnect": True,
-            "tools_paintcontour": True,
-
-            "tools_2sided_mirror_axis": "X",
-            "tools_2sided_axis_loc": 'point',
-            "tools_2sided_drilldia": 1,
-
-            "tools_film_type": 'neg',
-            "tools_film_boundary": 1,
-            "tools_film_scale": 0,
-
-            "tools_panelize_spacing_columns": 0,
-            "tools_panelize_spacing_rows": 0,
-            "tools_panelize_columns": 1,
-            "tools_panelize_rows": 1,
-            "tools_panelize_constrain": False,
-            "tools_panelize_constrainx": 0.0,
-            "tools_panelize_constrainy": 0.0
-
-        })
-
-        self.options.update(self.defaults)  # Copy app defaults to project options
-
-        self.gen_form = None
-        self.ger_form = None
-        self.exc_form = None
-        self.geo_form = None
-        self.cnc_form = None
-        self.tools_form = None
-        self.on_options_combo_change(0)  # Will show the initial form
-
-        # ### Define OBJECT COLLECTION ###
-        self.collection = ObjectCollection(self)
-        self.ui.project_tab_layout.addWidget(self.collection.view)
-        # ################################
-
-        self.log.debug("Finished creating Object Collection.")
-
-        # ### Initialize the color box's color in Preferences -> Global -> Color
-        # Init Plot Colors
-        self.ui.general_defaults_form.general_gui_group.pf_color_entry.set_value(self.defaults['global_plot_fill'])
-        self.ui.general_defaults_form.general_gui_group.pf_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_plot_fill'])[:7])
-        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_spinner.set_value(
-            int(self.defaults['global_plot_fill'][7:9], 16))
-        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.setValue(
-            int(self.defaults['global_plot_fill'][7:9], 16))
-
-        self.ui.general_defaults_form.general_gui_group.pl_color_entry.set_value(self.defaults['global_plot_line'])
-        self.ui.general_defaults_form.general_gui_group.pl_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_plot_line'])[:7])
-
-        # Init Left-Right Selection colors
-        self.ui.general_defaults_form.general_gui_group.sf_color_entry.set_value(self.defaults['global_sel_fill'])
-        self.ui.general_defaults_form.general_gui_group.sf_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_sel_fill'])[:7])
-        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_spinner.set_value(
-            int(self.defaults['global_sel_fill'][7:9], 16))
-        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_slider.setValue(
-            int(self.defaults['global_sel_fill'][7:9], 16))
-
-        self.ui.general_defaults_form.general_gui_group.sl_color_entry.set_value(self.defaults['global_sel_line'])
-        self.ui.general_defaults_form.general_gui_group.sl_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_sel_line'])[:7])
-
-        # Init Right-Left Selection colors
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry.set_value(
-            self.defaults['global_alt_sel_fill'])
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_alt_sel_fill'])[:7])
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_spinner.set_value(
-            int(self.defaults['global_sel_fill'][7:9], 16))
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.setValue(
-            int(self.defaults['global_sel_fill'][7:9], 16))
-
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry.set_value(
-            self.defaults['global_alt_sel_line'])
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_alt_sel_line'])[:7])
-
-        # Init Draw color and Selection Draw Color
-        self.ui.general_defaults_form.general_gui_group.draw_color_entry.set_value(
-            self.defaults['global_draw_color'])
-        self.ui.general_defaults_form.general_gui_group.draw_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_draw_color'])[:7])
-
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry.set_value(
-            self.defaults['global_sel_draw_color'])
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_sel_draw_color'])[:7])
-
-        # Init Project Items color
-        self.ui.general_defaults_form.general_gui_group.proj_color_entry.set_value(
-            self.defaults['global_proj_item_color'])
-        self.ui.general_defaults_form.general_gui_group.proj_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_proj_item_color'])[:7])
-
-        self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry.set_value(
-            self.defaults['global_proj_item_dis_color'])
-        self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_proj_item_dis_color'])[:7])
-
-        # Init the Annotation CNC Job color
-        self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_entry.set_value(
-            self.defaults['cncjob_annotation_fontcolor'])
-        self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['cncjob_annotation_fontcolor'])[:7])
-        # ### End of Data ####
-
-        # ### Plot Area ####
-        start_plot_time = time.time()   # debug
-        self.plotcanvas = PlotCanvas(self.ui.right_layout, self)
-
-        self.plotcanvas.vis_connect('mouse_move', self.on_mouse_move_over_plot)
-        self.plotcanvas.vis_connect('mouse_press', self.on_mouse_click_over_plot)
-        self.plotcanvas.vis_connect('mouse_release', self.on_mouse_click_release_over_plot)
-        self.plotcanvas.vis_connect('mouse_double_click', self.on_double_click_over_plot)
-
-        # Keys over plot enabled
-        self.plotcanvas.vis_connect('key_press', self.ui.keyPressEvent)
-
-        self.ui.splitter.setStretchFactor(1, 2)
-
-        # So it can receive key presses
-        self.plotcanvas.vispy_canvas.native.setFocus()
-
-        self.app_cursor = self.plotcanvas.new_cursor()
-        self.app_cursor.enabled = False
-
-        # to use for tools like Measurement tool who depends on the event sources who are changed inside the Editors
-        # depending on from where those tools are called different actions can be done
-        self.call_source = 'app'
-
-        end_plot_time = time.time()
-        self.log.debug("Finished Canvas initialization in %s seconds." % (str(end_plot_time - start_plot_time)))
-
-        # ### EDITOR section
-        self.geo_editor = FlatCAMGeoEditor(self, disabled=True)
-        self.exc_editor = FlatCAMExcEditor(self)
-        self.grb_editor = FlatCAMGrbEditor(self)
-
-        # ### Adjust tabs width ## ##
-        # self.collection.view.setMinimumWidth(self.ui.options_scroll_area.widget().sizeHint().width() +
-        #     self.ui.options_scroll_area.verticalScrollBar().sizeHint().width())
-        self.collection.view.setMinimumWidth(290)
-
-        self.log.debug("Finished adding FlatCAM Editor's.")
-
-        # ### Worker ####
-        if self.defaults["global_worker_number"]:
-            self.workers = WorkerStack(workers_number=int(self.defaults["global_worker_number"]))
-        else:
-            self.workers = WorkerStack(workers_number=2)
-        self.worker_task.connect(self.workers.add_task)
-
-        # ### Signal handling ###
-        # ### Custom signals  ###
-        self.inform.connect(self.info)
-        self.app_quit.connect(self.quit_application)
-        self.message.connect(self.message_dialog)
-        self.progress.connect(self.set_progress_bar)
-        self.object_created.connect(self.on_object_created)
-        self.object_changed.connect(self.on_object_changed)
-        self.object_plotted.connect(self.on_object_plotted)
-        self.plots_updated.connect(self.on_plots_updated)
-        self.file_opened.connect(self.register_recent)
-        self.file_opened.connect(lambda kind, filename: self.register_folder(filename))
-        self.file_saved.connect(lambda kind, filename: self.register_save_folder(filename))
-
-        # ### Standard signals
-        # ### Menu
-        self.ui.menufilenewproject.triggered.connect(self.on_file_new_click)
-        self.ui.menufilenewgeo.triggered.connect(self.new_geometry_object)
-        self.ui.menufilenewgrb.triggered.connect(self.new_gerber_object)
-        self.ui.menufilenewexc.triggered.connect(self.new_excellon_object)
-
-        self.ui.menufileopengerber.triggered.connect(self.on_fileopengerber)
-        self.ui.menufileopenexcellon.triggered.connect(self.on_fileopenexcellon)
-        self.ui.menufileopengcode.triggered.connect(self.on_fileopengcode)
-        self.ui.menufileopenproject.triggered.connect(self.on_file_openproject)
-        self.ui.menufileopenconfig.triggered.connect(self.on_file_openconfig)
-
-        self.ui.menufilenewscript.triggered.connect(self.on_filenewscript)
-        self.ui.menufileopenscript.triggered.connect(self.on_fileopenscript)
-
-        self.ui.menufilerunscript.triggered.connect(self.on_filerunscript)
-
-        self.ui.menufileimportsvg.triggered.connect(lambda: self.on_file_importsvg("geometry"))
-        self.ui.menufileimportsvg_as_gerber.triggered.connect(lambda: self.on_file_importsvg("gerber"))
-
-        self.ui.menufileimportdxf.triggered.connect(lambda: self.on_file_importdxf("geometry"))
-        self.ui.menufileimportdxf_as_gerber.triggered.connect(lambda: self.on_file_importdxf("gerber"))
-
-        self.ui.menufileexportsvg.triggered.connect(self.on_file_exportsvg)
-        self.ui.menufileexportpng.triggered.connect(self.on_file_exportpng)
-        self.ui.menufileexportexcellon.triggered.connect(self.on_file_exportexcellon)
-        self.ui.menufileexportgerber.triggered.connect(self.on_file_exportgerber)
-
-        self.ui.menufileexportdxf.triggered.connect(self.on_file_exportdxf)
-
-        self.ui.menufilesaveproject.triggered.connect(self.on_file_saveproject)
-        self.ui.menufilesaveprojectas.triggered.connect(self.on_file_saveprojectas)
-        self.ui.menufilesaveprojectcopy.triggered.connect(lambda: self.on_file_saveprojectas(make_copy=True))
-        self.ui.menufilesavedefaults.triggered.connect(self.on_file_savedefaults)
-        self.ui.menufile_exit.triggered.connect(self.final_save)
-
-        self.ui.menueditedit.triggered.connect(lambda: self.object2editor())
-        self.ui.menueditok.triggered.connect(lambda: self.editor2object())
-
-        self.ui.menuedit_convertjoin.triggered.connect(self.on_edit_join)
-        self.ui.menuedit_convertjoinexc.triggered.connect(self.on_edit_join_exc)
-        self.ui.menuedit_convertjoingrb.triggered.connect(self.on_edit_join_grb)
-
-        self.ui.menuedit_convert_sg2mg.triggered.connect(self.on_convert_singlegeo_to_multigeo)
-        self.ui.menuedit_convert_mg2sg.triggered.connect(self.on_convert_multigeo_to_singlegeo)
-
-        self.ui.menueditdelete.triggered.connect(self.on_delete)
-
-        self.ui.menueditcopyobject.triggered.connect(self.on_copy_object)
-        self.ui.menueditconvert_any2geo.triggered.connect(self.convert_any2geo)
-        self.ui.menueditconvert_any2gerber.triggered.connect(self.convert_any2gerber)
-
-        self.ui.menueditorigin.triggered.connect(self.on_set_origin)
-        self.ui.menueditjump.triggered.connect(self.on_jump_to)
-
-        self.ui.menuedittoggleunits.triggered.connect(self.on_toggle_units_click)
-        self.ui.menueditselectall.triggered.connect(self.on_selectall)
-        self.ui.menueditpreferences.triggered.connect(self.on_preferences)
-
-        # self.ui.menuoptions_transfer_a2o.triggered.connect(self.on_options_app2object)
-        # self.ui.menuoptions_transfer_a2p.triggered.connect(self.on_options_app2project)
-        # self.ui.menuoptions_transfer_o2a.triggered.connect(self.on_options_object2app)
-        # self.ui.menuoptions_transfer_p2a.triggered.connect(self.on_options_project2app)
-        # self.ui.menuoptions_transfer_o2p.triggered.connect(self.on_options_object2project)
-        # self.ui.menuoptions_transfer_p2o.triggered.connect(self.on_options_project2object)
-
-        self.ui.menuoptions_transform_rotate.triggered.connect(self.on_rotate)
-
-        self.ui.menuoptions_transform_skewx.triggered.connect(self.on_skewx)
-        self.ui.menuoptions_transform_skewy.triggered.connect(self.on_skewy)
-
-        self.ui.menuoptions_transform_flipx.triggered.connect(self.on_flipx)
-        self.ui.menuoptions_transform_flipy.triggered.connect(self.on_flipy)
-        self.ui.menuoptions_view_source.triggered.connect(self.on_view_source)
-
-        self.ui.menuviewdisableall.triggered.connect(self.disable_all_plots)
-        self.ui.menuviewdisableother.triggered.connect(self.disable_other_plots)
-        self.ui.menuviewenable.triggered.connect(self.enable_all_plots)
-
-        self.ui.menuview_zoom_fit.triggered.connect(self.on_zoom_fit)
-        self.ui.menuview_zoom_in.triggered.connect(
-            lambda: self.plotcanvas.zoom(1 / float(self.defaults['global_zoom_ratio']))
-        )
-        self.ui.menuview_zoom_out.triggered.connect(
-            lambda: self.plotcanvas.zoom(float(self.defaults['global_zoom_ratio']))
-        )
-
-        self.ui.menuview_toggle_code_editor.triggered.connect(self.on_toggle_code_editor)
-        self.ui.menuview_toggle_fscreen.triggered.connect(self.on_fullscreen)
-        self.ui.menuview_toggle_parea.triggered.connect(self.on_toggle_plotarea)
-        self.ui.menuview_toggle_notebook.triggered.connect(self.on_toggle_notebook)
-
-        self.ui.menuview_toggle_grid.triggered.connect(self.on_toggle_grid)
-        self.ui.menuview_toggle_axis.triggered.connect(self.on_toggle_axis)
-        self.ui.menuview_toggle_workspace.triggered.connect(self.on_workspace_menu)
-
-        self.ui.menutoolshell.triggered.connect(self.on_toggle_shell)
-
-        self.ui.menuhelp_about.triggered.connect(self.on_about)
-        self.ui.menuhelp_home.triggered.connect(lambda: webbrowser.open(self.app_url))
-        self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.manual_url))
-        self.ui.menuhelp_videohelp.triggered.connect(lambda: webbrowser.open(self.video_url))
-        self.ui.menuhelp_shortcut_list.triggered.connect(self.on_shortcut_list)
-
-        self.ui.menuprojectenable.triggered.connect(self.on_enable_sel_plots)
-        self.ui.menuprojectdisable.triggered.connect(self.on_disable_sel_plots)
-        self.ui.menuprojectgeneratecnc.triggered.connect(lambda: self.generate_cnc_job(self.collection.get_selected()))
-        self.ui.menuprojectviewsource.triggered.connect(self.on_view_source)
-
-        self.ui.menuprojectcopy.triggered.connect(self.on_copy_object)
-        self.ui.menuprojectedit.triggered.connect(self.object2editor)
-
-        self.ui.menuprojectdelete.triggered.connect(self.on_delete)
-        self.ui.menuprojectsave.triggered.connect(self.on_project_context_save)
-        self.ui.menuprojectproperties.triggered.connect(self.obj_properties)
-
-        # ToolBar signals
-        self.connect_toolbar_signals()
-
-        # Context Menu
-        self.ui.popmenu_disable.triggered.connect(lambda: self.toggle_plots(self.collection.get_selected()))
-        self.ui.popmenu_panel_toggle.triggered.connect(self.on_toggle_notebook)
-
-        self.ui.popmenu_new_geo.triggered.connect(self.new_geometry_object)
-        self.ui.popmenu_new_grb.triggered.connect(self.new_gerber_object)
-        self.ui.popmenu_new_exc.triggered.connect(self.new_excellon_object)
-        self.ui.popmenu_new_prj.triggered.connect(self.on_file_new)
-
-        self.ui.zoomfit.triggered.connect(self.on_zoom_fit)
-        self.ui.clearplot.triggered.connect(self.clear_plots)
-        self.ui.replot.triggered.connect(self.plot_all)
-
-        self.ui.popmenu_copy.triggered.connect(self.on_copy_object)
-        self.ui.popmenu_delete.triggered.connect(self.on_delete)
-        self.ui.popmenu_edit.triggered.connect(self.object2editor)
-        self.ui.popmenu_save.triggered.connect(lambda: self.editor2object())
-        self.ui.popmenu_move.triggered.connect(self.obj_move)
-
-        self.ui.popmenu_properties.triggered.connect(self.obj_properties)
-
-        # Preferences Plot Area TAB
-        self.ui.options_combo.activated.connect(self.on_options_combo_change)
-        self.ui.pref_save_button.clicked.connect(self.on_save_button)
-        self.ui.pref_import_button.clicked.connect(self.on_import_preferences)
-        self.ui.pref_export_button.clicked.connect(self.on_export_preferences)
-        self.ui.pref_open_button.clicked.connect(self.on_preferences_open_folder)
-
-        # ##############################
-        # ### GUI PREFERENCES SIGNALS ##
-        # ##############################
-        self.ui.general_options_form.general_app_group.units_radio.group_toggle_fn = self.on_toggle_units
-        self.ui.general_defaults_form.general_app_group.language_apply_btn.clicked.connect(
-            lambda: fcTranslate.on_language_apply_click(self, restart=True)
-        )
-        self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.connect(
-            lambda: self.on_toggle_units(no_pref=False))
-
-
-        # ##############################
-        # ### GUI PREFERENCES SIGNALS ##
-        # ##############################
-
-        # Setting plot colors signals
-        self.ui.general_defaults_form.general_gui_group.pf_color_entry.editingFinished.connect(
-            self.on_pf_color_entry)
-        self.ui.general_defaults_form.general_gui_group.pf_color_button.clicked.connect(
-            self.on_pf_color_button)
-        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_spinner.valueChanged.connect(
-            self.on_pf_color_spinner)
-        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.valueChanged.connect(
-            self.on_pf_color_slider)
-        self.ui.general_defaults_form.general_gui_group.pl_color_entry.editingFinished.connect(
-            self.on_pl_color_entry)
-        self.ui.general_defaults_form.general_gui_group.pl_color_button.clicked.connect(
-            self.on_pl_color_button)
-        # Setting selection (left - right) colors signals
-        self.ui.general_defaults_form.general_gui_group.sf_color_entry.editingFinished.connect(
-            self.on_sf_color_entry)
-        self.ui.general_defaults_form.general_gui_group.sf_color_button.clicked.connect(
-            self.on_sf_color_button)
-        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_spinner.valueChanged.connect(
-            self.on_sf_color_spinner)
-        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_slider.valueChanged.connect(
-            self.on_sf_color_slider)
-        self.ui.general_defaults_form.general_gui_group.sl_color_entry.editingFinished.connect(
-            self.on_sl_color_entry)
-        self.ui.general_defaults_form.general_gui_group.sl_color_button.clicked.connect(
-            self.on_sl_color_button)
-        # Setting selection (right - left) colors signals
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry.editingFinished.connect(
-            self.on_alt_sf_color_entry)
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_button.clicked.connect(
-            self.on_alt_sf_color_button)
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_spinner.valueChanged.connect(
-            self.on_alt_sf_color_spinner)
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.valueChanged.connect(
-            self.on_alt_sf_color_slider)
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry.editingFinished.connect(
-            self.on_alt_sl_color_entry)
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_button.clicked.connect(
-            self.on_alt_sl_color_button)
-        # Setting Editor Draw colors signals
-        self.ui.general_defaults_form.general_gui_group.draw_color_entry.editingFinished.connect(
-            self.on_draw_color_entry)
-        self.ui.general_defaults_form.general_gui_group.draw_color_button.clicked.connect(
-            self.on_draw_color_button)
-
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry.editingFinished.connect(
-            self.on_sel_draw_color_entry)
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_button.clicked.connect(
-            self.on_sel_draw_color_button)
-
-        self.ui.general_defaults_form.general_gui_group.proj_color_entry.editingFinished.connect(
-            self.on_proj_color_entry)
-        self.ui.general_defaults_form.general_gui_group.proj_color_button.clicked.connect(
-            self.on_proj_color_button)
-
-        self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry.editingFinished.connect(
-            self.on_proj_color_dis_entry)
-        self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.clicked.connect(
-            self.on_proj_color_dis_button)
-
-        self.ui.general_defaults_form.general_gui_group.wk_cb.currentIndexChanged.connect(self.on_workspace_modified)
-        self.ui.general_defaults_form.general_gui_group.workspace_cb.stateChanged.connect(self.on_workspace)
-
-        self.ui.general_defaults_form.general_gui_set_group.layout_combo.activated.connect(self.on_layout)
-
-        self.ui.cncjob_defaults_form.cncjob_adv_opt_group.tc_variable_combo.currentIndexChanged[str].connect(
-            self.on_cnc_custom_parameters)
-        self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_entry.editingFinished.connect(
-            self.on_annotation_fontcolor_entry)
-        self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_button.clicked.connect(
-            self.on_annotation_fontcolor_button)
-
-        # Modify G-CODE Plot Area TAB
-        self.ui.code_editor.textChanged.connect(self.handleTextChanged)
-        self.ui.buttonOpen.clicked.connect(self.handleOpen)
-        self.ui.buttonSave.clicked.connect(self.handleSaveGCode)
-        self.ui.buttonPrint.clicked.connect(self.handlePrint)
-        self.ui.buttonPreview.clicked.connect(self.handlePreview)
-        self.ui.buttonFind.clicked.connect(self.handleFindGCode)
-        self.ui.buttonReplace.clicked.connect(self.handleReplaceGCode)
-
-        # Object list
-        self.collection.view.activated.connect(self.on_row_activated)
-
-        # Monitor the checkbox from the Application Defaults Tab and show the TCL shell or not depending on it's value
-        self.ui.general_defaults_form.general_app_group.shell_startup_cb.clicked.connect(self.on_toggle_shell)
-
-        # Load the defaults values into the Excellon Format and Excellon Zeros fields
-        self.ui.excellon_defaults_form.excellon_opt_group.excellon_defaults_button.clicked.connect(
-            self.on_excellon_defaults_button)
-
-        # Load the defaults values into the Excellon Format and Excellon Zeros fields
-        self.ui.excellon_options_form.excellon_opt_group.excellon_defaults_button.clicked.connect(
-            self.on_excellon_options_button)
-
-        # this is a flag to signal to other tools that the ui tooltab is locked and not accessible
-        self.tool_tab_locked = False
-
-        # decide if to show or hide the Notebook side of the screen at startup
-        if self.defaults["global_project_at_startup"] is True:
-            self.ui.splitter.setSizes([1, 1])
-        else:
-            self.ui.splitter.setSizes([0, 1])
-
-        # ###################
-        # ### Other setups ##
-        # ###################
-        # Sets up FlatCAMObj, FCProcess and FCProcessContainer.
-        self.setup_obj_classes()
-        self.setup_recent_items()
-        self.setup_component_editor()
-
-        # ############
-        # ### Shell ##
-        # ############
-
-        # #########################
-        # Auto-complete KEYWORDS ##
-        # #########################
-        self.tcl_commands_list = ['add_circle', 'add_poly', 'add_polygon', 'add_polyline', 'add_rectangle',
-                                  'aligndrill', 'clear',
-                                  'aligndrillgrid', 'cncjob', 'cutout', 'delete', 'drillcncjob',
-                                  'export_gcode',
-                                  'export_svg', 'ext', 'exteriors', 'follow', 'geo_union', 'geocutout', 'get_names',
-                                  'get_sys', 'getsys', 'help', 'import_svg', 'interiors', 'isolate', 'join_excellon',
-                                  'join_excellons', 'join_geometries', 'join_geometry', 'list_sys', 'listsys', 'mill',
-                                  'millholes', 'mirror', 'new', 'new_geometry', 'offset', 'open_excellon', 'open_gcode',
-                                  'open_gerber', 'open_project', 'options', 'paint', 'pan', 'panel', 'panelize', 'plot',
-                                  'save', 'save_project', 'save_sys', 'scale', 'set_active', 'set_sys', 'setsys',
-                                  'skew', 'subtract_poly', 'subtract_rectangle', 'version', 'write_gcode'
-                                  ]
-
-        self.ordinary_keywords = ['name', 'center_x', 'center_y', 'radius', 'x0', 'y0', 'x1', 'y1', 'box', 'axis',
-                                  'holes', 'grid', 'minoffset', 'gridoffset', 'axisoffset', 'dia', 'dist',
-                                  'gridoffsetx', 'gridoffsety', 'columns', 'rows', 'z_cut', 'z_move', 'feedrate',
-                                  'feedrate_rapid', 'tooldia', 'multidepth', 'extracut', 'depthperpass', 'ppname_g',
-                                  'outname', 'margin', 'gaps', 'gapsize', 'tools', 'drillz', 'travelz', 'spindlespeed',
-                                  'toolchange', 'toolchangez', 'endz', 'ppname_e', 'opt_type', 'preamble', 'postamble',
-                                  'filename', 'scale_factor', 'type', 'passes', 'overlap', 'combine', 'use_threads',
-                                  'x', 'y', 'follow', 'all', 'spacing_columns', 'spacing_rows', 'factor', 'value',
-                                  'angle_x', 'angle_y', 'gridx', 'gridy', 'True', 'False'
-                                  ]
-
-        self.tcl_keywords = [
-            "after", "append", "apply", "array", "auto_execok", "auto_import", "auto_load", "auto_mkindex",
-            "auto_qualify", "auto_reset", "bgerror", "binary", "break", "case", "catch", "cd", "chan", "clock", "close",
-            "concat", "continue", "coroutine", "dict", "encoding", "eof", "error", "eval", "exec", "exit", "expr",
-            "fblocked", "fconfigure", "fcopy", "file", "fileevent", "flush", "for", "foreach", "format", "gets", "glob",
-            "global", "history", "if", "incr", "info", "interp", "join", "lappend", "lassign", "lindex", "linsert",
-            "list", "llength", "load", "lrange", "lrepeat", "lreplace", "lreverse", "lsearch", "lset", "lsort",
-            "mathfunc", "mathop", "memory", "my", "namespace", "next", "nextto", "open", "package", "parray", "pid",
-            "pkg_mkIndex", "platform", "proc", "puts", "pwd", "read", "refchan", "regexp", "regsub", "rename", "return",
-            "scan", "seek", "self", "set", "socket", "source", "split", "string", "subst", "switch", "tailcall",
-            "tcl_endOfWord", "tcl_findLibrary", "tcl_startOfNextWord", "tcl_startOfPreviousWord", "tcl_wordBreakAfter",
-            "tcl_wordBreakBefore", "tell", "throw", "time", "tm", "trace", "transchan", "try", "unknown", "unload",
-            "unset", "update", "uplevel", "upvar", "variable", "vwait", "while", "yield", "yieldto", "zlib",
-            "attemptckalloc", "attemptckrealloc", "ckalloc", "ckfree", "ckrealloc", "Tcl_Access", "Tcl_AddErrorInfo",
-            "Tcl_AddObjErrorInfo", "Tcl_AlertNotifier", "Tcl_Alloc", "Tcl_AllocStatBuf", "Tcl_AllowExceptions",
-            "Tcl_AppendAllObjTypes", "Tcl_AppendElement", "Tcl_AppendExportList", "Tcl_AppendFormatToObj",
-            "Tcl_AppendLimitedToObj", "Tcl_AppendObjToErrorInfo", "Tcl_AppendObjToObj", "Tcl_AppendPrintfToObj",
-            "Tcl_AppendResult", "Tcl_AppendResultVA", "Tcl_AppendStringsToObj", "Tcl_AppendStringsToObjVA",
-            "Tcl_AppendToObj", "Tcl_AppendUnicodeToObj", "Tcl_AppInit", "Tcl_AsyncCreate", "Tcl_AsyncDelete",
-            "Tcl_AsyncInvoke", "Tcl_AsyncMark", "Tcl_AsyncReady", "Tcl_AttemptAlloc", "Tcl_AttemptRealloc",
-            "Tcl_AttemptSetObjLength", "Tcl_BackgroundError", "Tcl_BackgroundException", "Tcl_Backslash",
-            "Tcl_BadChannelOption", "Tcl_CallWhenDeleted", "Tcl_Canceled", "Tcl_CancelEval", "Tcl_CancelIdleCall",
-            "Tcl_ChannelBlockModeProc", "Tcl_ChannelBuffered", "Tcl_ChannelClose2Proc", "Tcl_ChannelCloseProc",
-            "Tcl_ChannelFlushProc", "Tcl_ChannelGetHandleProc", "Tcl_ChannelGetOptionProc", "Tcl_ChannelHandlerProc",
-            "Tcl_ChannelInputProc", "Tcl_ChannelName", "Tcl_ChannelOutputProc", "Tcl_ChannelSeekProc",
-            "Tcl_ChannelSetOptionProc", "Tcl_ChannelThreadActionProc", "Tcl_ChannelTruncateProc", "Tcl_ChannelVersion",
-            "Tcl_ChannelWatchProc", "Tcl_ChannelWideSeekProc", "Tcl_Chdir", "Tcl_ClassGetMetadata",
-            "Tcl_ClassSetConstructor", "Tcl_ClassSetDestructor", "Tcl_ClassSetMetadata", "Tcl_ClearChannelHandlers",
-            "Tcl_Close", "Tcl_CommandComplete", "Tcl_CommandTraceInfo", "Tcl_Concat", "Tcl_ConcatObj",
-            "Tcl_ConditionFinalize", "Tcl_ConditionNotify", "Tcl_ConditionWait", "Tcl_ConvertCountedElement",
-            "Tcl_ConvertElement", "Tcl_ConvertToType", "Tcl_CopyObjectInstance", "Tcl_CreateAlias",
-            "Tcl_CreateAliasObj", "Tcl_CreateChannel", "Tcl_CreateChannelHandler", "Tcl_CreateCloseHandler",
-            "Tcl_CreateCommand", "Tcl_CreateEncoding", "Tcl_CreateEnsemble", "Tcl_CreateEventSource",
-            "Tcl_CreateExitHandler", "Tcl_CreateFileHandler", "Tcl_CreateHashEntry", "Tcl_CreateInterp",
-            "Tcl_CreateMathFunc", "Tcl_CreateNamespace", "Tcl_CreateObjCommand", "Tcl_CreateObjTrace",
-            "Tcl_CreateSlave", "Tcl_CreateThread", "Tcl_CreateThreadExitHandler", "Tcl_CreateTimerHandler",
-            "Tcl_CreateTrace", "Tcl_CutChannel", "Tcl_DecrRefCount", "Tcl_DeleteAssocData", "Tcl_DeleteChannelHandler",
-            "Tcl_DeleteCloseHandler", "Tcl_DeleteCommand", "Tcl_DeleteCommandFromToken", "Tcl_DeleteEvents",
-            "Tcl_DeleteEventSource", "Tcl_DeleteExitHandler", "Tcl_DeleteFileHandler", "Tcl_DeleteHashEntry",
-            "Tcl_DeleteHashTable", "Tcl_DeleteInterp", "Tcl_DeleteNamespace", "Tcl_DeleteThreadExitHandler",
-            "Tcl_DeleteTimerHandler", "Tcl_DeleteTrace", "Tcl_DetachChannel", "Tcl_DetachPids", "Tcl_DictObjDone",
-            "Tcl_DictObjFirst", "Tcl_DictObjGet", "Tcl_DictObjNext", "Tcl_DictObjPut", "Tcl_DictObjPutKeyList",
-            "Tcl_DictObjRemove", "Tcl_DictObjRemoveKeyList", "Tcl_DictObjSize", "Tcl_DiscardInterpState",
-            "Tcl_DiscardResult", "Tcl_DontCallWhenDeleted", "Tcl_DoOneEvent", "Tcl_DoWhenIdle", "Tcl_DStringAppend",
-            "Tcl_DStringAppendElement", "Tcl_DStringEndSublist", "Tcl_DStringFree", "Tcl_DStringGetResult",
-            "Tcl_DStringInit", "Tcl_DStringLength", "Tcl_DStringResult", "Tcl_DStringSetLength",
-            "Tcl_DStringStartSublist", "Tcl_DStringTrunc", "Tcl_DStringValue", "Tcl_DumpActiveMemory",
-            "Tcl_DuplicateObj", "Tcl_Eof", "Tcl_ErrnoId", "Tcl_ErrnoMsg", "Tcl_Eval", "Tcl_EvalEx", "Tcl_EvalFile",
-            "Tcl_EvalObjEx", "Tcl_EvalObjv", "Tcl_EvalTokens", "Tcl_EvalTokensStandard", "Tcl_EventuallyFree",
-            "Tcl_Exit", "Tcl_ExitThread", "Tcl_Export", "Tcl_ExposeCommand", "Tcl_ExprBoolean", "Tcl_ExprBooleanObj",
-            "Tcl_ExprDouble", "Tcl_ExprDoubleObj", "Tcl_ExprLong", "Tcl_ExprLongObj", "Tcl_ExprObj", "Tcl_ExprString",
-            "Tcl_ExternalToUtf", "Tcl_ExternalToUtfDString", "Tcl_Finalize", "Tcl_FinalizeNotifier",
-            "Tcl_FinalizeThread", "Tcl_FindCommand", "Tcl_FindEnsemble", "Tcl_FindExecutable", "Tcl_FindHashEntry",
-            "Tcl_FindNamespace", "Tcl_FirstHashEntry", "Tcl_Flush", "Tcl_ForgetImport", "Tcl_Format",
-            "Tcl_Free· Tcl_FreeEncoding", "Tcl_FreeParse", "Tcl_FreeResult", "Tcl_FSAccess", "Tcl_FSChdir",
-            "Tcl_FSConvertToPathType", "Tcl_FSCopyDirectory", "Tcl_FSCopyFile", "Tcl_FSCreateDirectory", "Tcl_FSData",
-            "Tcl_FSDeleteFile", "Tcl_FSEqualPaths", "Tcl_FSEvalFile", "Tcl_FSEvalFileEx", "Tcl_FSFileAttrsGet",
-            "Tcl_FSFileAttrsSet", "Tcl_FSFileAttrStrings", "Tcl_FSFileSystemInfo", "Tcl_FSGetCwd",
-            "Tcl_FSGetFileSystemForPath", "Tcl_FSGetInternalRep", "Tcl_FSGetNativePath", "Tcl_FSGetNormalizedPath",
-            "Tcl_FSGetPathType", "Tcl_FSGetTranslatedPath", "Tcl_FSGetTranslatedStringPath", "Tcl_FSJoinPath",
-            "Tcl_FSJoinToPath", "Tcl_FSLink· Tcl_FSListVolumes", "Tcl_FSLoadFile", "Tcl_FSLstat",
-            "Tcl_FSMatchInDirectory", "Tcl_FSMountsChanged", "Tcl_FSNewNativePath", "Tcl_FSOpenFileChannel",
-            "Tcl_FSPathSeparator", "Tcl_FSRegister", "Tcl_FSRemoveDirectory", "Tcl_FSRenameFile", "Tcl_FSSplitPath",
-            "Tcl_FSStat", "Tcl_FSUnloadFile", "Tcl_FSUnregister", "Tcl_FSUtime", "Tcl_GetAccessTimeFromStat",
-            "Tcl_GetAlias", "Tcl_GetAliasObj", "Tcl_GetAssocData", "Tcl_GetBignumFromObj", "Tcl_GetBlocksFromStat",
-            "Tcl_GetBlockSizeFromStat", "Tcl_GetBoolean", "Tcl_GetBooleanFromObj", "Tcl_GetByteArrayFromObj",
-            "Tcl_GetChangeTimeFromStat", "Tcl_GetChannel", "Tcl_GetChannelBufferSize", "Tcl_GetChannelError",
-            "Tcl_GetChannelErrorInterp", "Tcl_GetChannelHandle", "Tcl_GetChannelInstanceData", "Tcl_GetChannelMode",
-            "Tcl_GetChannelName", "Tcl_GetChannelNames", "Tcl_GetChannelNamesEx", "Tcl_GetChannelOption",
-            "Tcl_GetChannelThread", "Tcl_GetChannelType", "Tcl_GetCharLength", "Tcl_GetClassAsObject",
-            "Tcl_GetCommandFromObj", "Tcl_GetCommandFullName", "Tcl_GetCommandInfo", "Tcl_GetCommandInfoFromToken",
-            "Tcl_GetCommandName", "Tcl_GetCurrentNamespace", "Tcl_GetCurrentThread", "Tcl_GetCwd",
-            "Tcl_GetDefaultEncodingDir", "Tcl_GetDeviceTypeFromStat", "Tcl_GetDouble", "Tcl_GetDoubleFromObj",
-            "Tcl_GetEncoding", "Tcl_GetEncodingFromObj", "Tcl_GetEncodingName", "Tcl_GetEncodingNameFromEnvironment",
-            "Tcl_GetEncodingNames", "Tcl_GetEncodingSearchPath", "Tcl_GetEnsembleFlags", "Tcl_GetEnsembleMappingDict",
-            "Tcl_GetEnsembleNamespace", "Tcl_GetEnsembleParameterList", "Tcl_GetEnsembleSubcommandList",
-            "Tcl_GetEnsembleUnknownHandler", "Tcl_GetErrno", "Tcl_GetErrorLine", "Tcl_GetFSDeviceFromStat",
-            "Tcl_GetFSInodeFromStat", "Tcl_GetGlobalNamespace", "Tcl_GetGroupIdFromStat", "Tcl_GetHashKey",
-            "Tcl_GetHashValue", "Tcl_GetHostName", "Tcl_GetIndexFromObj", "Tcl_GetIndexFromObjStruct", "Tcl_GetInt",
-            "Tcl_GetInterpPath", "Tcl_GetIntFromObj", "Tcl_GetLinkCountFromStat", "Tcl_GetLongFromObj", "Tcl_GetMaster",
-            "Tcl_GetMathFuncInfo", "Tcl_GetModeFromStat", "Tcl_GetModificationTimeFromStat", "Tcl_GetNameOfExecutable",
-            "Tcl_GetNamespaceUnknownHandler", "Tcl_GetObjectAsClass", "Tcl_GetObjectCommand", "Tcl_GetObjectFromObj",
-            "Tcl_GetObjectName", "Tcl_GetObjectNamespace", "Tcl_GetObjResult", "Tcl_GetObjType", "Tcl_GetOpenFile",
-            "Tcl_GetPathType", "Tcl_GetRange", "Tcl_GetRegExpFromObj", "Tcl_GetReturnOptions", "Tcl_Gets",
-            "Tcl_GetServiceMode", "Tcl_GetSizeFromStat", "Tcl_GetSlave", "Tcl_GetsObj", "Tcl_GetStackedChannel",
-            "Tcl_GetStartupScript", "Tcl_GetStdChannel", "Tcl_GetString", "Tcl_GetStringFromObj", "Tcl_GetStringResult",
-            "Tcl_GetThreadData", "Tcl_GetTime", "Tcl_GetTopChannel", "Tcl_GetUniChar", "Tcl_GetUnicode",
-            "Tcl_GetUnicodeFromObj", "Tcl_GetUserIdFromStat", "Tcl_GetVar", "Tcl_GetVar2", "Tcl_GetVar2Ex",
-            "Tcl_GetVersion", "Tcl_GetWideIntFromObj", "Tcl_GlobalEval", "Tcl_GlobalEvalObj", "Tcl_HashStats",
-            "Tcl_HideCommand", "Tcl_Import", "Tcl_IncrRefCount", "Tcl_Init", "Tcl_InitCustomHashTable",
-            "Tcl_InitHashTable", "Tcl_InitMemory", "Tcl_InitNotifier", "Tcl_InitObjHashTable", "Tcl_InitStubs",
-            "Tcl_InputBlocked", "Tcl_InputBuffered", "Tcl_InterpActive", "Tcl_InterpDeleted", "Tcl_InvalidateStringRep",
-            "Tcl_IsChannelExisting", "Tcl_IsChannelRegistered", "Tcl_IsChannelShared", "Tcl_IsEnsemble", "Tcl_IsSafe",
-            "Tcl_IsShared", "Tcl_IsStandardChannel", "Tcl_JoinPath", "Tcl_JoinThread", "Tcl_LimitAddHandler",
-            "Tcl_LimitCheck", "Tcl_LimitExceeded", "Tcl_LimitGetCommands", "Tcl_LimitGetGranularity",
-            "Tcl_LimitGetTime", "Tcl_LimitReady", "Tcl_LimitRemoveHandler", "Tcl_LimitSetCommands",
-            "Tcl_LimitSetGranularity", "Tcl_LimitSetTime", "Tcl_LimitTypeEnabled", "Tcl_LimitTypeExceeded",
-            "Tcl_LimitTypeReset", "Tcl_LimitTypeSet", "Tcl_LinkVar", "Tcl_ListMathFuncs", "Tcl_ListObjAppendElement",
-            "Tcl_ListObjAppendList", "Tcl_ListObjGetElements", "Tcl_ListObjIndex", "Tcl_ListObjLength",
-            "Tcl_ListObjReplace", "Tcl_LogCommandInfo", "Tcl_Main", "Tcl_MakeFileChannel", "Tcl_MakeSafe",
-            "Tcl_MakeTcpClientChannel", "Tcl_Merge", "Tcl_MethodDeclarerClass", "Tcl_MethodDeclarerObject",
-            "Tcl_MethodIsPublic", "Tcl_MethodIsType", "Tcl_MethodName", "Tcl_MutexFinalize", "Tcl_MutexLock",
-            "Tcl_MutexUnlock", "Tcl_NewBignumObj", "Tcl_NewBooleanObj", "Tcl_NewByteArrayObj", "Tcl_NewDictObj",
-            "Tcl_NewDoubleObj", "Tcl_NewInstanceMethod", "Tcl_NewIntObj", "Tcl_NewListObj", "Tcl_NewLongObj",
-            "Tcl_NewMethod", "Tcl_NewObj", "Tcl_NewObjectInstance", "Tcl_NewStringObj", "Tcl_NewUnicodeObj",
-            "Tcl_NewWideIntObj", "Tcl_NextHashEntry", "Tcl_NotifyChannel", "Tcl_NRAddCallback", "Tcl_NRCallObjProc",
-            "Tcl_NRCmdSwap", "Tcl_NRCreateCommand", "Tcl_NREvalObj", "Tcl_NREvalObjv", "Tcl_NumUtfChars",
-            "Tcl_ObjectContextInvokeNext", "Tcl_ObjectContextIsFiltering", "Tcl_ObjectContextMethod",
-            "Tcl_ObjectContextObject", "Tcl_ObjectContextSkippedArgs", "Tcl_ObjectDeleted", "Tcl_ObjectGetMetadata",
-            "Tcl_ObjectGetMethodNameMapper", "Tcl_ObjectSetMetadata", "Tcl_ObjectSetMethodNameMapper", "Tcl_ObjGetVar2",
-            "Tcl_ObjPrintf", "Tcl_ObjSetVar2", "Tcl_OpenCommandChannel", "Tcl_OpenFileChannel", "Tcl_OpenTcpClient",
-            "Tcl_OpenTcpServer", "Tcl_OutputBuffered", "Tcl_Panic", "Tcl_PanicVA", "Tcl_ParseArgsObjv",
-            "Tcl_ParseBraces", "Tcl_ParseCommand", "Tcl_ParseExpr", "Tcl_ParseQuotedString", "Tcl_ParseVar",
-            "Tcl_ParseVarName", "Tcl_PkgPresent", "Tcl_PkgPresentEx", "Tcl_PkgProvide", "Tcl_PkgProvideEx",
-            "Tcl_PkgRequire", "Tcl_PkgRequireEx", "Tcl_PkgRequireProc", "Tcl_PosixError", "Tcl_Preserve",
-            "Tcl_PrintDouble", "Tcl_PutEnv", "Tcl_QueryTimeProc", "Tcl_QueueEvent", "Tcl_Read", "Tcl_ReadChars",
-            "Tcl_ReadRaw", "Tcl_Realloc", "Tcl_ReapDetachedProcs", "Tcl_RecordAndEval", "Tcl_RecordAndEvalObj",
-            "Tcl_RegExpCompile", "Tcl_RegExpExec", "Tcl_RegExpExecObj", "Tcl_RegExpGetInfo", "Tcl_RegExpMatch",
-            "Tcl_RegExpMatchObj", "Tcl_RegExpRange", "Tcl_RegisterChannel", "Tcl_RegisterConfig", "Tcl_RegisterObjType",
-            "Tcl_Release", "Tcl_ResetResult", "Tcl_RestoreInterpState", "Tcl_RestoreResult", "Tcl_SaveInterpState",
-            "Tcl_SaveResult", "Tcl_ScanCountedElement", "Tcl_ScanElement", "Tcl_Seek", "Tcl_ServiceAll",
-            "Tcl_ServiceEvent", "Tcl_ServiceModeHook", "Tcl_SetAssocData", "Tcl_SetBignumObj", "Tcl_SetBooleanObj",
-            "Tcl_SetByteArrayLength", "Tcl_SetByteArrayObj", "Tcl_SetChannelBufferSize", "Tcl_SetChannelError",
-            "Tcl_SetChannelErrorInterp", "Tcl_SetChannelOption", "Tcl_SetCommandInfo", "Tcl_SetCommandInfoFromToken",
-            "Tcl_SetDefaultEncodingDir", "Tcl_SetDoubleObj", "Tcl_SetEncodingSearchPath", "Tcl_SetEnsembleFlags",
-            "Tcl_SetEnsembleMappingDict", "Tcl_SetEnsembleParameterList", "Tcl_SetEnsembleSubcommandList",
-            "Tcl_SetEnsembleUnknownHandler", "Tcl_SetErrno", "Tcl_SetErrorCode", "Tcl_SetErrorCodeVA",
-            "Tcl_SetErrorLine", "Tcl_SetExitProc", "Tcl_SetHashValue", "Tcl_SetIntObj", "Tcl_SetListObj",
-            "Tcl_SetLongObj", "Tcl_SetMainLoop", "Tcl_SetMaxBlockTime", "Tcl_SetNamespaceUnknownHandler",
-            "Tcl_SetNotifier", "Tcl_SetObjErrorCode", "Tcl_SetObjLength", "Tcl_SetObjResult", "Tcl_SetPanicProc",
-            "Tcl_SetRecursionLimit", "Tcl_SetResult", "Tcl_SetReturnOptions", "Tcl_SetServiceMode",
-            "Tcl_SetStartupScript", "Tcl_SetStdChannel", "Tcl_SetStringObj", "Tcl_SetSystemEncoding", "Tcl_SetTimeProc",
-            "Tcl_SetTimer", "Tcl_SetUnicodeObj", "Tcl_SetVar", "Tcl_SetVar2", "Tcl_SetVar2Ex", "Tcl_SetWideIntObj",
-            "Tcl_SignalId", "Tcl_SignalMsg", "Tcl_Sleep", "Tcl_SourceRCFile", "Tcl_SpliceChannel", "Tcl_SplitList",
-            "Tcl_SplitPath", "Tcl_StackChannel", "Tcl_StandardChannels", "Tcl_Stat", "Tcl_StaticPackage",
-            "Tcl_StringCaseMatch", "Tcl_StringMatch", "Tcl_SubstObj", "Tcl_TakeBignumFromObj", "Tcl_Tell",
-            "Tcl_ThreadAlert", "Tcl_ThreadQueueEvent", "Tcl_TraceCommand", "Tcl_TraceVar", "Tcl_TraceVar2",
-            "Tcl_TransferResult", "Tcl_TranslateFileName", "Tcl_TruncateChannel", "Tcl_Ungets", "Tcl_UniChar",
-            "Tcl_UniCharAtIndex", "Tcl_UniCharCaseMatch", "Tcl_UniCharIsAlnum", "Tcl_UniCharIsAlpha",
-            "Tcl_UniCharIsControl", "Tcl_UniCharIsDigit", "Tcl_UniCharIsGraph", "Tcl_UniCharIsLower",
-            "Tcl_UniCharIsPrint", "Tcl_UniCharIsPunct", "Tcl_UniCharIsSpace", "Tcl_UniCharIsUpper",
-            "Tcl_UniCharIsWordChar", "Tcl_UniCharLen", "Tcl_UniCharNcasecmp", "Tcl_UniCharNcmp", "Tcl_UniCharToLower",
-            "Tcl_UniCharToTitle", "Tcl_UniCharToUpper", "Tcl_UniCharToUtf", "Tcl_UniCharToUtfDString", "Tcl_UnlinkVar",
-            "Tcl_UnregisterChannel", "Tcl_UnsetVar", "Tcl_UnsetVar2", "Tcl_UnstackChannel", "Tcl_UntraceCommand",
-            "Tcl_UntraceVar", "Tcl_UntraceVar2", "Tcl_UpdateLinkedVar", "Tcl_UpVar", "Tcl_UpVar2", "Tcl_UtfAtIndex",
-            "Tcl_UtfBackslash", "Tcl_UtfCharComplete", "Tcl_UtfFindFirst", "Tcl_UtfFindLast", "Tcl_UtfNext",
-            "Tcl_UtfPrev", "Tcl_UtfToExternal", "Tcl_UtfToExternalDString", "Tcl_UtfToLower", "Tcl_UtfToTitle",
-            "Tcl_UtfToUniChar", "Tcl_UtfToUniCharDString", "Tcl_UtfToUpper", "Tcl_ValidateAllMemory", "Tcl_VarEval",
-            "Tcl_VarEvalVA", "Tcl_VarTraceInfo", "Tcl_VarTraceInfo2", "Tcl_WaitForEvent", "Tcl_WaitPid",
-            "Tcl_WinTCharToUtf", "Tcl_WinUtfToTChar", "Tcl_Write", "Tcl_WriteChars", "Tcl_WriteObj", "Tcl_WriteRaw",
-            "Tcl_WrongNumArgs", "Tcl_ZlibAdler32", "Tcl_ZlibCRC32", "Tcl_ZlibDeflate", "Tcl_ZlibInflate",
-            "Tcl_ZlibStreamChecksum", "Tcl_ZlibStreamClose", "Tcl_ZlibStreamEof", "Tcl_ZlibStreamGet",
-            "Tcl_ZlibStreamGetCommandName", "Tcl_ZlibStreamInit", "Tcl_ZlibStreamPut", "dde", "http", "msgcat",
-            "registry", "tcltest", "Tcl_AllocHashEntryProc", "Tcl_AppInitProc", "Tcl_ArgvInfo", "Tcl_AsyncProc",
-            "Tcl_ChannelProc", "Tcl_ChannelType", "Tcl_CloneProc", "Tcl_CloseProc", "Tcl_CmdDeleteProc", "Tcl_CmdInfo",
-            "Tcl_CmdObjTraceDeleteProc", "Tcl_CmdObjTraceProc", "Tcl_CmdProc", "Tcl_CmdTraceProc",
-            "Tcl_CommandTraceProc", "Tcl_CompareHashKeysProc", "Tcl_Config", "Tcl_DriverBlockModeProc",
-            "Tcl_DriverClose2Proc", "Tcl_DriverCloseProc", "Tcl_DriverFlushProc", "Tcl_DriverGetHandleProc",
-            "Tcl_DriverGetOptionProc", "Tcl_DriverHandlerProc", "Tcl_DriverInputProc", "Tcl_DriverOutputProc",
-            "Tcl_DriverSeekProc", "Tcl_DriverSetOptionProc", "Tcl_DriverThreadActionProc", "Tcl_DriverTruncateProc",
-            "Tcl_DriverWatchProc", "Tcl_DriverWideSeekProc", "Tcl_DupInternalRepProc", "Tcl_EncodingConvertProc",
-            "Tcl_EncodingFreeProc", "Tcl_EncodingType", "Tcl_Event", "Tcl_EventCheckProc", "Tcl_EventDeleteProc",
-            "Tcl_EventProc", "Tcl_EventSetupProc", "Tcl_ExitProc", "Tcl_FileProc", "Tcl_Filesystem",
-            "Tcl_FreeHashEntryProc", "Tcl_FreeInternalRepProc", "Tcl_FreeProc", "Tcl_FSAccessProc", "Tcl_FSChdirProc",
-            "Tcl_FSCopyDirectoryProc", "Tcl_FSCopyFileProc", "Tcl_FSCreateDirectoryProc", "Tcl_FSCreateInternalRepProc",
-            "Tcl_FSDeleteFileProc", "Tcl_FSDupInternalRepProc", "Tcl_FSFileAttrsGetProc", "Tcl_FSFileAttrsSetProc",
-            "Tcl_FSFilesystemPathTypeProc", "Tcl_FSFilesystemSeparatorProc", "Tcl_FSFreeInternalRepProc",
-            "Tcl_FSGetCwdProc", "Tcl_FSInternalToNormalizedProc", "Tcl_FSLinkProc", "Tcl_FSListVolumesProc",
-            "Tcl_FSLoadFileProc", "Tcl_FSLstatProc", "Tcl_FSMatchInDirectoryProc", "Tcl_FSNormalizePathProc",
-            "Tcl_FSOpenFileChannelProc", "Tcl_FSPathInFilesystemProc", "Tcl_FSRemoveDirectoryProc",
-            "Tcl_FSRenameFileProc", "Tcl_FSStatProc", "Tcl_FSUnloadFileProc", "Tcl_FSUtimeProc", "Tcl_GlobTypeData",
-            "Tcl_HashKeyType", "Tcl_IdleProc", "Tcl_Interp", "Tcl_InterpDeleteProc", "Tcl_LimitHandlerDeleteProc",
-            "Tcl_LimitHandlerProc", "Tcl_MainLoopProc", "Tcl_MathProc", "Tcl_MethodCallProc", "Tcl_MethodDeleteProc",
-            "Tcl_MethodType", "Tcl_NamespaceDeleteProc", "Tcl_NotifierProcs", "Tcl_Obj", "Tcl_ObjCmdProc",
-            "Tcl_ObjectMapMethodNameProc", "Tcl_ObjectMetadataDeleteProc", "Tcl_ObjType", "Tcl_PackageInitProc",
-            "Tcl_PackageUnloadProc", "Tcl_PanicProc", "Tcl_RegExpIndices", "Tcl_RegExpInfo", "Tcl_ScaleTimeProc",
-            "Tcl_SetFromAnyProc", "Tcl_TcpAcceptProc", "Tcl_Time", "Tcl_TimerProc", "Tcl_Token", "Tcl_UpdateStringProc",
-            "Tcl_Value", "Tcl_VarTraceProc", "argc", "argv", "argv0", "auto_path", "env", "errorCode", "errorInfo",
-            "filename", "re_syntax", "safe", "Tcl", "tcl_interactive", "tcl_library", "TCL_MEM_DEBUG",
-            "tcl_nonwordchars", "tcl_patchLevel", "tcl_pkgPath", "tcl_platform", "tcl_precision", "tcl_rcFileName",
-            "tcl_traceCompile", "tcl_traceEval", "tcl_version", "tcl_wordchars"
-        ]
-
-        self.myKeywords = self.tcl_commands_list + self.ordinary_keywords + self.tcl_keywords
-
-        self.shell = FCShell(self, version=self.version)
-        self.shell._edit.set_model_data(self.myKeywords)
-        self.ui.code_editor.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_output("FlatCAM %s (c)2014-2019 Juan Pablo Caram " % self.version)
-        self.shell.append_output(_("(Type help to get started)\n\n"))
-
-        self.init_tcl()
-
-        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"]:
-            self.ui.shell_dock.show()
-        else:
-            self.ui.shell_dock.hide()
-
-        # ########################
-        # ### Tools and Plugins ##
-        # ########################
-
-        self.dblsidedtool = None
-        self.measurement_tool = None
-        self.panelize_tool = None
-        self.film_tool = None
-        self.paste_tool = None
-        self.calculator_tool = None
-        self.sub_tool = None
-        self.move_tool = None
-        self.cutout_tool = None
-        self.ncclear_tool = None
-        self.paint_tool = None
-        self.transform_tool = None
-        self.properties_tool = None
-        self.pdf_tool = None
-        self.image_tool = None
-        self.pcb_wizard_tool = None
-
-        # always install tools only after the shell is initialized because the self.inform.emit() depends on shell
-        self.install_tools()
-
-        # ### System Font Parsing ###
-        # self.f_parse = ParseFont(self)
-        # self.parse_system_fonts()
-
-        # test if the program was started with a script as parameter
-        if self.cmd_line_shellfile:
-            try:
-                with open(self.cmd_line_shellfile, "r") as myfile:
-                    cmd_line_shellfile_text = myfile.read()
-                    self.shell._sysShell.exec_command(cmd_line_shellfile_text)
-            except Exception as ext:
-                print("ERROR: ", ext)
-                sys.exit(2)
-
-        # ##########################
-        # ### Check for updates ####
-        # ##########################
-
-        # Separate thread (Not worker)
-        # Check for updates on startup but only if the user consent and the app is not in Beta version
-        if (self.beta is False or self.beta is None) and \
-                self.ui.general_defaults_form.general_gui_group.version_check_cb.get_value() is True:
-            App.log.info("Checking for updates in backgroud (this is version %s)." % str(self.version))
-
-            self.thr2 = QtCore.QThread()
-            self.worker_task.emit({'fcn': self.version_check,
-                                   'params': []})
-            self.thr2.start(QtCore.QThread.LowPriority)
-
-        # ###################################
-        # ### Variables for global usage ####
-        # ###################################
-
-        # coordinates for relative position display
-        self.rel_point1 = (0, 0)
-        self.rel_point2 = (0, 0)
-
-        # variable to store coordinates
-        self.pos = (0, 0)
-        self.pos_jump = (0, 0)
-
-        # decide if we have a double click or single click
-        self.doubleclick = False
-
-        # variable to store if a command is active (then the var is not None) and which one it is
-        self.command_active = None
-        # variable to store the status of moving selection action
-        # None value means that it's not an selection action
-        # True value = a selection from left to right
-        # False value = a selection from right to left
-        self.selection_type = None
-
-        # List to store the objects that are currently loaded in FlatCAM
-        # This list is updated on each object creation or object delete
-        self.all_objects_list = []
-
-        # List to store the objects that are selected
-        self.sel_objects_list = []
-
-        # holds the key modifier if pressed (CTRL, SHIFT or ALT)
-        self.key_modifiers = None
-
-        # Variable to hold the status of the axis
-        self.toggle_axis = True
-
-        # Variable to store the status of the fullscreen event
-        self.toggle_fscreen = False
-
-        # Variable to store the status of the code editor
-        self.toggle_codeeditor = False
-
-        # Variable to be used for situations when we don't want the LMB click on canvas to auto open the Project Tab
-        self.click_noproject = False
-
-        self.cursor = None
-
-        # Variable to store the GCODE that was edited
-        self.gcode_edited = ""
-
-        # if Preferences are changed in the Edit -> Preferences tab the value will be set to True
-        self.preferences_changed_flag = False
-
-        self.grb_list = ['gbr', 'ger', 'gtl', 'gbl', 'gts', 'gbs', 'gtp', 'gbp', 'gto', 'gbo', 'gm1', 'gm2', 'gm3',
-                         'gko', 'cmp', 'sol', 'stc', 'sts', 'plc', 'pls', 'crc', 'crs', 'tsm', 'bsm', 'ly2', 'ly15',
-                         'dim', 'mil', 'grb', 'top', 'bot', 'smt', 'smb', 'sst', 'ssb', 'spt', 'spb', 'pho', 'gdo',
-                         'art', 'gbd', 'gb0', 'gb1', 'gb2', 'gb3', 'g4', 'gb5', 'gb6', 'gb7', 'gb8', 'gb9'
-                         ]
-        self.exc_list = ['drl', 'txt', 'xln', 'drd', 'tap', 'exc', 'ncd']
-        self.gcode_list = ['nc', 'ncc', 'tap', 'gcode', 'cnc', 'ecs', 'fnc', 'dnc', 'ncg', 'gc', 'fan', 'fgc', 'din',
-                           'xpi', 'hnc', 'h', 'i', 'ncp', 'min', 'gcd', 'rol', 'mpr', 'ply', 'out', 'eia', 'plt', 'sbp',
-                           'mpf'
-                           ]
-        self.svg_list = ['svg']
-        self.dxf_list = ['dxf']
-        self.pdf_list = ['pdf']
-        self.prj_list = ['flatprj']
-
-        # global variable used by NCC Tool to signal that some polygons could not be cleared, if True
-        # flag for polygons not cleared
-        self.poly_not_cleared = False
-
-        # VisPy visuals
-        self.hover_shapes = ShapeCollection(parent=self.plotcanvas.vispy_canvas.view.scene, layers=1)
-        self.isHovering = False
-        self.notHovering = True
-
-        # #########################################################
-        # ### Save defaults to factory_defaults.FlatConfig file ###
-        # ### It's done only once after install ###################
-        # #########################################################
-        factory_file = open(self.data_path + '/factory_defaults.FlatConfig')
-        fac_def_from_file = factory_file.read()
-        factory_defaults = json.loads(fac_def_from_file)
-
-        # if the file contain an empty dictionary then save the factory defaults into the file
-        if not factory_defaults:
-            self.save_factory_defaults(silent=False)
-            # ONLY AT FIRST STARTUP INIT THE GUI LAYOUT TO 'COMPACT'
-            initial_lay = 'compact'
-            self.on_layout(lay=initial_lay)
-            # Set the combobox in Preferences to the current layout
-            idx = self.ui.general_defaults_form.general_gui_set_group.layout_combo.findText(initial_lay)
-            self.ui.general_defaults_form.general_gui_set_group.layout_combo.setCurrentIndex(idx)
-
-        factory_file.close()
-
-        # and then make the  factory_defaults.FlatConfig file read_only os it can't be modified after creation.
-        filename_factory = self.data_path + '/factory_defaults.FlatConfig'
-        os.chmod(filename_factory, S_IREAD | S_IRGRP | S_IROTH)
-
-        # Post-GUI initialization: Experimental attempt
-        # to perform unit tests on the GUI.
-        # if post_gui is not None:
-        #     post_gui(self)
-
-        App.log.debug("END of constructor. Releasing control.")
-
-        # accept a project file as command line parameter
-        # the path/file_name must be enclosed in quotes if it contain spaces
-        for argument in App.args:
-            if '.FlatPrj' in argument:
-                try:
-                    project_name = str(argument)
-
-                    if project_name == "":
-                        self.inform.emit(_("Open cancelled."))
-                    else:
-                        # self.open_project(project_name)
-                        run_from_arg = True
-                        self.worker_task.emit({'fcn': self.open_project,
-                                               'params': [project_name, run_from_arg]})
-                except Exception as e:
-                    log.debug("Could not open FlatCAM project file as App parameter due: %s" % str(e))
-
-            if '.FlatConfig' in argument:
-                try:
-                    file_name = str(argument)
-
-                    if file_name == "":
-                        self.inform.emit(_("Open Config file failed."))
-                    else:
-                        # run_from_arg = True
-                        # self.worker_task.emit({'fcn': self.open_config_file,
-                        #                        'params': [file_name, run_from_arg]})
-                        self.open_config_file(file_name, run_from_arg=True)
-                except Exception as e:
-                    log.debug("Could not open FlatCAM Config file as App parameter due: %s" % str(e))
-
-            if '.FlatScript' in argument:
-                try:
-                    file_name = str(argument)
-
-                    if file_name == "":
-                        self.inform.emit(_("Open Script file failed."))
-                    else:
-                        # run_from_arg = True
-                        # self.worker_task.emit({'fcn': self.open_script_file,
-                        #                        'params': [file_name, run_from_arg]})
-                        self.on_filerunscript(name=file_name)
-                except Exception as e:
-                    log.debug("Could not open FlatCAM Script file as App parameter due: %s" % str(e))
-
-    def set_ui_title(self, name):
-        self.ui.setWindowTitle('FlatCAM %s %s - %s    %s' %
-                               (self.version,
-                                ('BETA' if self.beta else ''),
-                                platform.architecture()[0],
-                                name)
-                            )
-
-    def defaults_read_form(self):
-        for option in self.defaults_form_fields:
-            try:
-                self.defaults[option] = self.defaults_form_fields[option].get_value()
-            except Exception as e:
-                log.debug("App.defaults_read_form() --> %s" % str(e))
-
-    def defaults_write_form(self, factor=None, fl_units=None):
-        for option in self.defaults:
-            self.defaults_write_form_field(option, factor=factor, units=fl_units)
-            # try:
-            #     self.defaults_form_fields[option].set_value(self.defaults[option])
-            # except KeyError:
-            #     #self.log.debug("defaults_write_form(): No field for: %s" % option)
-            #     # TODO: Rethink this?
-            #     pass
-
-    def defaults_write_form_field(self, field, factor=None, units=None):
-        try:
-            if factor is None:
-                if units is None:
-                    self.defaults_form_fields[field].set_value(self.defaults[field])
-                elif units == 'IN' and (field == 'global_gridx' or field == 'global_gridy'):
-                    self.defaults_form_fields[field].set_value(self.defaults[field], decimals=6)
-                elif units == 'MM' and (field == 'global_gridx' or field == 'global_gridy'):
-                    self.defaults_form_fields[field].set_value(self.defaults[field], decimals=4)
-            else:
-                if units is None:
-                    self.defaults_form_fields[field].set_value(self.defaults[field] * factor)
-                elif units == 'IN' and (field == 'global_gridx' or field == 'global_gridy'):
-                    self.defaults_form_fields[field].set_value((self.defaults[field] * factor), decimals=6)
-                elif units == 'MM' and (field == 'global_gridx' or field == 'global_gridy'):
-                    self.defaults_form_fields[field].set_value((self.defaults[field] * factor), decimals=4)
-        except KeyError:
-            # self.log.debug("defaults_write_form(): No field for: %s" % option)
-            # TODO: Rethink this?
-            pass
-        except AttributeError:
-            log.debug(field)
-
-    def clear_pool(self):
-        self.pool.close()
-
-        self.pool = Pool()
-        self.pool_recreated.emit(self.pool)
-
-        gc.collect()
-
-    # the order that the tools are installed is important as they can depend on each other install position
-    def install_tools(self):
-        self.dblsidedtool = DblSidedTool(self)
-        self.dblsidedtool.install(icon=QtGui.QIcon('share/doubleside16.png'), separator=True)
-
-        self.measurement_tool = Measurement(self)
-        self.measurement_tool.install(icon=QtGui.QIcon('share/measure16.png'), separator=True)
-
-        self.panelize_tool = Panelize(self)
-        self.panelize_tool.install(icon=QtGui.QIcon('share/panel16.png'))
-
-        self.film_tool = Film(self)
-        self.film_tool.install(icon=QtGui.QIcon('share/film16.png'))
-
-        self.paste_tool = SolderPaste(self)
-        self.paste_tool.install(icon=QtGui.QIcon('share/solderpastebis32.png'))
-
-        self.calculator_tool = ToolCalculator(self)
-        self.calculator_tool.install(icon=QtGui.QIcon('share/calculator24.png'), separator=True)
-
-        self.sub_tool = ToolSub(self)
-        self.sub_tool.install(icon=QtGui.QIcon('share/sub32.png'), pos=self.ui.menutool, separator=True)
-
-        self.move_tool = ToolMove(self)
-        self.move_tool.install(icon=QtGui.QIcon('share/move16.png'), pos=self.ui.menuedit,
-                               before=self.ui.menueditorigin)
-
-        self.cutout_tool = CutOut(self)
-        self.cutout_tool.install(icon=QtGui.QIcon('share/cut16_bis.png'), pos=self.ui.menutool,
-                                 before=self.measurement_tool.menuAction)
-
-        self.ncclear_tool = NonCopperClear(self)
-        self.ncclear_tool.install(icon=QtGui.QIcon('share/ncc16.png'), pos=self.ui.menutool,
-                                  before=self.measurement_tool.menuAction, separator=True)
-
-        self.paint_tool = ToolPaint(self)
-        self.paint_tool.install(icon=QtGui.QIcon('share/paint16.png'), pos=self.ui.menutool,
-                                before=self.measurement_tool.menuAction, separator=True)
-
-        self.transform_tool = ToolTransform(self)
-        self.transform_tool.install(icon=QtGui.QIcon('share/transform.png'), pos=self.ui.menuoptions, separator=True)
-
-        self.properties_tool = Properties(self)
-        self.properties_tool.install(icon=QtGui.QIcon('share/properties32.png'), pos=self.ui.menuoptions)
-
-        self.pdf_tool = ToolPDF(self)
-        self.pdf_tool.install(icon=QtGui.QIcon('share/pdf32.png'), pos=self.ui.menufileimport,
-                              separator=True)
-
-        self.image_tool = ToolImage(self)
-        self.image_tool.install(icon=QtGui.QIcon('share/image32.png'), pos=self.ui.menufileimport,
-                                separator=True)
-        self.pcb_wizard_tool = PcbWizard(self)
-        self.pcb_wizard_tool.install(icon=QtGui.QIcon('share/drill32.png'), pos=self.ui.menufileimport)
-
-        self.log.debug("Tools are installed.")
-
-    def remove_tools(self):
-        for act in self.ui.menutool.actions():
-            self.ui.menutool.removeAction(act)
-
-    def init_tools(self):
-        log.debug("init_tools()")
-        # delete the data currently in the Tools Tab and the Tab itself
-        widget = QtWidgets.QTabWidget.widget(self.ui.notebook, 2)
-        if widget is not None:
-            widget.deleteLater()
-        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)
-
-        # reinstall all the Tools as some may have been removed when the data was removed from the Tools Tab
-        # first remove all of them
-        self.remove_tools()
-        # second re add the TCL Shell action to the Tools menu and reconnect it to ist slot function
-        self.ui.menutoolshell = self.ui.menutool.addAction(QtGui.QIcon('share/shell16.png'), '&Command Line\tS')
-        self.ui.menutoolshell.triggered.connect(self.on_toggle_shell)
-        # third install all of them
-        self.install_tools()
-        self.log.debug("Tools are initialized.")
-
-    # def parse_system_fonts(self):
-    #     self.worker_task.emit({'fcn': self.f_parse.get_fonts_by_types,
-    #                            'params': []})
-
-    def connect_toolbar_signals(self):
-        # Toolbar
-        # self.ui.file_new_btn.triggered.connect(self.on_file_new)
-        self.ui.file_open_btn.triggered.connect(self.on_file_openproject)
-        self.ui.file_save_btn.triggered.connect(self.on_file_saveproject)
-        self.ui.file_open_gerber_btn.triggered.connect(self.on_fileopengerber)
-        self.ui.file_open_excellon_btn.triggered.connect(self.on_fileopenexcellon)
-
-        self.ui.clear_plot_btn.triggered.connect(self.clear_plots)
-        self.ui.replot_btn.triggered.connect(self.plot_all)
-        self.ui.zoom_fit_btn.triggered.connect(self.on_zoom_fit)
-        self.ui.zoom_in_btn.triggered.connect(lambda: self.plotcanvas.zoom(1 / 1.5))
-        self.ui.zoom_out_btn.triggered.connect(lambda: self.plotcanvas.zoom(1.5))
-
-        self.ui.newgeo_btn.triggered.connect(self.new_geometry_object)
-        self.ui.newgrb_btn.triggered.connect(self.new_gerber_object)
-        self.ui.newexc_btn.triggered.connect(self.new_excellon_object)
-        self.ui.editgeo_btn.triggered.connect(self.object2editor)
-        self.ui.update_obj_btn.triggered.connect(lambda: self.editor2object())
-        self.ui.delete_btn.triggered.connect(self.on_delete)
-        self.ui.shell_btn.triggered.connect(self.on_toggle_shell)
-
-        # Tools Toolbar Signals
-        self.ui.dblsided_btn.triggered.connect(lambda: self.dblsidedtool.run(toggle=True))
-        self.ui.cutout_btn.triggered.connect(lambda: self.cutout_tool.run(toggle=True))
-        self.ui.ncc_btn.triggered.connect(lambda: self.ncclear_tool.run(toggle=True))
-        self.ui.paint_btn.triggered.connect(lambda: self.paint_tool.run(toggle=True))
-
-        self.ui.panelize_btn.triggered.connect(lambda: self.panelize_tool.run(toggle=True))
-        self.ui.film_btn.triggered.connect(lambda: self.film_tool.run(toggle=True))
-        self.ui.solder_btn.triggered.connect(lambda: self.paste_tool.run(toggle=True))
-        self.ui.sub_btn.triggered.connect(lambda: self.sub_tool.run(toggle=True))
-
-        self.ui.calculators_btn.triggered.connect(lambda: self.calculator_tool.run(toggle=True))
-        self.ui.transform_btn.triggered.connect(lambda: self.transform_tool.run(toggle=True))
-
-    def object2editor(self):
-        """
-        Send the current Geometry or Excellon object (if any) into the editor.
-
-        :return: None
-        """
-        self.report_usage("object2editor()")
-
-        edited_object = self.collection.get_active()
-
-        if isinstance(edited_object, FlatCAMGerber) or isinstance(edited_object, FlatCAMGeometry) or \
-                isinstance(edited_object, FlatCAMExcellon):
-            pass
-        else:
-            self.inform.emit(_("[WARNING_NOTCL] Select a Geometry, Gerber or Excellon Object to edit."))
-            return
-
-        if isinstance(edited_object, FlatCAMGeometry):
-            # store the Geometry Editor Toolbar visibility before entering in the Editor
-            self.geo_editor.toolbar_old_state = True if self.ui.geo_edit_toolbar.isVisible() else False
-
-            if edited_object.multigeo is True:
-                edited_tools = [int(x.text()) for x in edited_object.ui.geo_tools_table.selectedItems()]
-                if len(edited_tools) > 1:
-                    self.inform.emit(_("[WARNING_NOTCL] Simultanoeus editing of tools geometry in a MultiGeo Geometry "
-                                       "is not possible.\n"
-                                       "Edit only one geometry at a time."))
-
-                # determine the tool dia of the selected tool
-                selected_tooldia = float(edited_object.ui.geo_tools_table.item((edited_tools[0] - 1), 1).text())
-
-                # now find the key in the edited_object.tools that has this tooldia
-                multi_tool = 1
-                for tool in edited_object.tools:
-                    if edited_object.tools[tool]['tooldia'] == selected_tooldia:
-                        multi_tool = tool
-                        break
-
-                self.geo_editor.edit_fcgeometry(edited_object, multigeo_tool=multi_tool)
-            else:
-                self.geo_editor.edit_fcgeometry(edited_object)
-
-            # we set the notebook to hidden
-            self.ui.splitter.setSizes([0, 1])
-
-            # set call source to the Editor we go into
-            self.call_source = 'geo_editor'
-
-        elif isinstance(edited_object, FlatCAMExcellon):
-            # store the Excellon Editor Toolbar visibility before entering in the Editor
-            self.exc_editor.toolbar_old_state = True if self.ui.exc_edit_toolbar.isVisible() else False
-            self.exc_editor.edit_fcexcellon(edited_object)
-
-            # set call source to the Editor we go into
-            self.call_source = 'exc_editor'
-
-            if self.ui.splitter.sizes()[0] == 0:
-                self.ui.splitter.setSizes([1, 1])
-
-        elif isinstance(edited_object, FlatCAMGerber):
-            # store the Gerber Editor Toolbar visibility before entering in the Editor
-            self.grb_editor.toolbar_old_state = True if self.ui.grb_edit_toolbar.isVisible() else False
-            self.grb_editor.edit_fcgerber(edited_object)
-
-            # set call source to the Editor we go into
-            self.call_source = 'grb_editor'
-
-            if self.ui.splitter.sizes()[0] == 0:
-                self.ui.splitter.setSizes([1, 1])
-
-        # # make sure that we can't select another object while in Editor Mode:
-        # self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
-        self.ui.project_frame.setDisabled(True)
-
-        # delete any selection shape that might be active as they are not relevant in Editor
-        self.delete_selection_shape()
-
-        self.ui.plot_tab_area.setTabText(0, "EDITOR Area")
-        self.ui.plot_tab_area.protectTab(0)
-        self.inform.emit(_("[WARNING_NOTCL] Editor is activated ..."))
-
-        self.should_we_save = True
-
-    def editor2object(self, cleanup=None):
-        """
-        Transfers the Geometry or Excellon from the editor to the current object.
-
-        :return: None
-        """
-        self.report_usage("editor2object()")
-
-        # do not update a geometry or excellon object unless it comes out of an editor
-        if self.call_source != 'app':
-            edited_obj = self.collection.get_active()
-
-            if cleanup is None:
-                msgbox = QtWidgets.QMessageBox()
-                msgbox.setText(_("Do you want to save the edited object?"))
-                msgbox.setWindowTitle(_("Close Editor"))
-                msgbox.setWindowIcon(QtGui.QIcon('share/save_as.png'))
-
-                bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.YesRole)
-                bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole)
-                bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.RejectRole)
-
-                msgbox.setDefaultButton(bt_yes)
-                msgbox.exec_()
-                response = msgbox.clickedButton()
-
-                if response == bt_yes:
-                    # clean the Tools Tab
-                    self.ui.tool_scroll_area.takeWidget()
-                    self.ui.tool_scroll_area.setWidget(QtWidgets.QWidget())
-                    self.ui.notebook.setTabText(2, "Tool")
-
-                    if isinstance(edited_obj, FlatCAMGeometry):
-                        obj_type = "Geometry"
-                        if cleanup is None:
-                            self.geo_editor.update_fcgeometry(edited_obj)
-                            self.geo_editor.update_options(edited_obj)
-                        self.geo_editor.deactivate()
-
-                        # update the geo object options so it is including the bounding box values
-                        try:
-                            xmin, ymin, xmax, ymax = edited_obj.bounds()
-                            edited_obj.options['xmin'] = xmin
-                            edited_obj.options['ymin'] = ymin
-                            edited_obj.options['xmax'] = xmax
-                            edited_obj.options['ymax'] = ymax
-                        except AttributeError as e:
-                            self.inform.emit(_("[WARNING] Object empty after edit."))
-                            log.debug("App.editor2object() --> Geometry --> %s" % str(e))
-                    elif isinstance(edited_obj, FlatCAMGerber):
-                        obj_type = "Gerber"
-                        if cleanup is None:
-                            self.grb_editor.update_fcgerber()
-                            self.grb_editor.update_options(edited_obj)
-                        self.grb_editor.deactivate_grb_editor()
-
-                        # delete the old object (the source object) if it was an empty one
-                        if len(edited_obj.solid_geometry) == 0:
-                            old_name = edited_obj.options['name']
-                            self.collection.set_active(old_name)
-                            self.collection.delete_active()
-
-                    elif isinstance(edited_obj, FlatCAMExcellon):
-                        obj_type = "Excellon"
-                        if cleanup is None:
-                            self.exc_editor.update_fcexcellon(edited_obj)
-                            self.exc_editor.update_options(edited_obj)
-                        self.exc_editor.deactivate()
-                    else:
-                        self.inform.emit(_("[WARNING_NOTCL] Select a Gerber, Geometry or Excellon Object to update."))
-                        return
-
-                    self.inform.emit(_("[selected] %s is updated, returning to App...") % obj_type)
-                elif response == bt_no:
-                    # clean the Tools Tab
-                    self.ui.tool_scroll_area.takeWidget()
-                    self.ui.tool_scroll_area.setWidget(QtWidgets.QWidget())
-                    self.ui.notebook.setTabText(2, "Tool")
-
-                    if isinstance(edited_obj, FlatCAMGeometry):
-                        self.geo_editor.deactivate()
-                    elif isinstance(edited_obj, FlatCAMGerber):
-                        self.grb_editor.deactivate_grb_editor()
-                    elif isinstance(edited_obj, FlatCAMExcellon):
-                        self.exc_editor.deactivate()
-                        # set focus on the project tab
-                        self.ui.notebook.setCurrentWidget(self.ui.project_tab)
-                    else:
-                        self.inform.emit(_("[WARNING_NOTCL] Select a Gerber, Geometry or Excellon Object to update."))
-                        return
-                elif response == bt_cancel:
-                    return
-            else:
-                if isinstance(edited_obj, FlatCAMGeometry):
-                    self.geo_editor.deactivate()
-                elif isinstance(edited_obj, FlatCAMGerber):
-                    self.grb_editor.deactivate_grb_editor()
-                elif isinstance(edited_obj, FlatCAMExcellon):
-                    self.exc_editor.deactivate()
-                else:
-                    self.inform.emit(_("[WARNING_NOTCL] Select a Gerber, Geometry or Excellon Object to update."))
-                    return
-
-            # if notebook is hidden we show it
-            if self.ui.splitter.sizes()[0] == 0:
-                self.ui.splitter.setSizes([1, 1])
-
-            # restore the call_source to app
-            self.call_source = 'app'
-
-            edited_obj.plot()
-            self.ui.plot_tab_area.setTabText(0, "Plot Area")
-            self.ui.plot_tab_area.protectTab(0)
-
-            # make sure that we reenable the selection on Project Tab after returning from Editor Mode:
-            # self.collection.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
-            self.ui.project_frame.setDisabled(False)
-
-    def get_last_folder(self):
-        return self.defaults["global_last_folder"]
-
-    def get_last_save_folder(self):
-        loc = self.defaults["global_last_save_folder"]
-        if loc is None:
-            loc = self.defaults["global_last_folder"]
-        if loc is None:
-            loc = os.path.dirname(__file__)
-        return loc
-
-    def report_usage(self, resource):
-        """
-        Increments usage counter for the given resource
-        in self.defaults['global_stats'].
-
-        :param resource: Name of the resource.
-        :return: None
-        """
-
-        if resource in self.defaults['global_stats']:
-            self.defaults['global_stats'][resource] += 1
-        else:
-            self.defaults['global_stats'][resource] = 1
-
-    def init_tcl(self):
-        if hasattr(self, 'tcl'):
-            # 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 ssuccessfully 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 warning: 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):
-        """
-        Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
-        Also handles execution in separated threads
-
-        :param text:
-        :return: output if there was any
-        """
-
-        self.report_usage('exec_command')
-
-        result = self.exec_command_test(text, False)
-
-        # MS: added this method call so the geometry is updated once the TCL command is executed
-        self.plot_all()
-
-        return result
-
-    def exec_command_test(self, text, reraise=True):
-        """
-        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 unitttests).
-        :return: Output from the command
-        """
-
-        text = str(text)
-
-        try:
-            self.shell.open_proccessing()  # Disables input box.
-            result = self.tcl.eval(str(text))
-            if result != 'None':
-                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'))
-            self.shell.append_error('ERROR: ' + result + '\n')
-            # Show error in console and just return or in test raise exception
-            if reraise:
-                raise e
-
-        finally:
-            self.shell.close_proccessing()
-            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
-        also on the shell.
-
-        :param msg: Text to write.
-        :return: None
-        """
-
-        # Type of message in brackets at the beginning of the message.
-        match = re.search("\[([^\]]+)\](.*)", msg)
-        if match:
-            level = match.group(1)
-            msg_ = match.group(2)
-            self.ui.fcinfo.set_status(str(msg_), level=level)
-
-            if level.lower() == "error":
-                self.shell_message(msg, error=True, show=True)
-            elif level.lower() == "warning":
-                self.shell_message(msg, warning=True, show=True)
-
-            elif level.lower() == "error_notcl":
-                self.shell_message(msg, error=True, show=False)
-
-            elif level.lower() == "warning_notcl":
-                self.shell_message(msg, warning=True, show=False)
-
-            elif level.lower() == "success":
-                self.shell_message(msg, success=True, show=False)
-
-            elif level.lower() == "selected":
-                self.shell_message(msg, selected=True, show=False)
-
-            else:
-                self.shell_message(msg, show=False)
-
-        else:
-            self.ui.fcinfo.set_status(str(msg), level="info")
-
-            # make sure that if the message is to clear the infobar with a space
-            # is not printed over and over on the shell
-            if msg != '':
-                self.shell_message(msg)
-
-    def restore_toolbar_view(self):
-        tb = self.defaults["global_toolbar_view"]
-        if tb & 1:
-            self.ui.toolbarfile.setVisible(True)
-        else:
-            self.ui.toolbarfile.setVisible(False)
-
-        if tb & 2:
-            self.ui.toolbargeo.setVisible(True)
-        else:
-            self.ui.toolbargeo.setVisible(False)
-
-        if tb & 4:
-            self.ui.toolbarview.setVisible(True)
-        else:
-            self.ui.toolbarview.setVisible(False)
-
-        if tb & 8:
-            self.ui.toolbartools.setVisible(True)
-        else:
-            self.ui.toolbartools.setVisible(False)
-
-        if tb & 16:
-            self.ui.exc_edit_toolbar.setVisible(True)
-        else:
-            self.ui.exc_edit_toolbar.setVisible(False)
-
-        if tb & 32:
-            self.ui.geo_edit_toolbar.setVisible(True)
-        else:
-            self.ui.geo_edit_toolbar.setVisible(False)
-
-        if tb & 64:
-            self.ui.grb_edit_toolbar.setVisible(True)
-        else:
-            self.ui.grb_edit_toolbar.setVisible(False)
-
-        if tb & 128:
-            self.ui.snap_toolbar.setVisible(True)
-        else:
-            self.ui.snap_toolbar.setVisible(False)
-
-        if tb & 256:
-            self.ui.toolbarshell.setVisible(True)
-        else:
-            self.ui.toolbarshell.setVisible(False)
-
-    def load_defaults(self, filename):
-        """
-        Loads the aplication's default settings from current_defaults.FlatConfig into
-        ``self.defaults``.
-
-        :return: None
-        """
-        try:
-            f = open(self.data_path + "/" + filename + ".FlatConfig")
-            options = f.read()
-            f.close()
-        except IOError:
-            self.log.error("Could not load defaults file.")
-            self.inform.emit(_("[ERROR] Could not load defaults file."))
-            # in case the defaults file can't be loaded, show all toolbars
-            self.defaults["global_toolbar_view"] = 511
-            return
-
-        try:
-            defaults = json.loads(options)
-        except:
-            # in case the defaults file can't be loaded, show all toolbars
-            self.defaults["global_toolbar_view"] = 511
-            e = sys.exc_info()[0]
-            App.log.error(str(e))
-            self.inform.emit(_("[ERROR] Failed to parse defaults file."))
-            return
-        self.defaults.update(defaults)
-        log.debug("FlatCAM defaults loaded from: %s" % filename)
-
-        # restore the toolbar view
-        self.restore_toolbar_view()
-
-    def on_import_preferences(self):
-        """
-        Loads the aplication's factory default settings from factory_defaults.FlatConfig into
-        ``self.defaults``.
-
-        :return: None
-        """
-
-        self.report_usage("on_import_preferences")
-        App.log.debug("on_import_preferences()")
-
-        filter_ = "Config File (*.FlatConfig);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Preferences"),
-                                                                 directory=self.data_path,
-                                                                 filter=filter_)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Preferences"),
-                                                                 filter=filter_)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] FlatCAM preferences import cancelled."))
-        else:
-            try:
-                f = open(filename)
-                options = f.read()
-                f.close()
-            except IOError:
-                self.log.error("Could not load defaults file.")
-                self.inform.emit(_("[ERROR_NOTCL] Could not load defaults file."))
-                return
-
-            try:
-                defaults_from_file = json.loads(options)
-            except Exception as e:
-                e = sys.exc_info()[0]
-                App.log.error(str(e))
-                self.inform.emit(_("[ERROR_NOTCL] Failed to parse defaults file."))
-                return
-            self.defaults.update(defaults_from_file)
-            self.on_preferences_edited()
-            self.inform.emit(_("[success] Imported Defaults from %s") % filename)
-
-    def on_export_preferences(self):
-        self.report_usage("on_export_preferences")
-        App.log.debug("on_export_preferences()")
-
-        defaults_file_content = None
-
-        self.date = str(datetime.today()).rpartition('.')[0]
-        self.date = ''.join(c for c in self.date if c not in ':-')
-        self.date = self.date.replace(' ', '_')
-
-        filter__ = "Config File (*.FlatConfig);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export FlatCAM Preferences"),
-                directory=self.data_path + '/preferences_' + self.date,
-                filter=filter__
-            )
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export FlatCAM Preferences"),
-                                                                 filter=filter__)
-
-        filename = str(filename)
-        defaults_from_file = {}
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] FlatCAM preferences export cancelled."))
-            return
-        else:
-            try:
-                f = open(filename, 'w')
-                defaults_file_content = f.read()
-                f.close()
-            except PermissionError:
-                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
-                                   "Most likely another app is holding the file open and not accessible."))
-                return
-            except IOError:
-                App.log.debug('Creating a new preferences file ...')
-                f = open(filename, 'w')
-                json.dump({}, f)
-                f.close()
-            except:
-                e = sys.exc_info()[0]
-                App.log.error("Could not load defaults file.")
-                App.log.error(str(e))
-                self.inform.emit(_("[ERROR_NOTCL] Could not load defaults file."))
-                return
-
-            try:
-                defaults_from_file = json.loads(defaults_file_content)
-            except:
-                App.log.warning("Trying to read an empty Preferences file. Continue.")
-
-            # Update options
-            self.defaults_read_form()
-            defaults_from_file.update(self.defaults)
-            self.propagate_defaults(silent=True)
-
-            # Save update options
-            try:
-                f = open(filename, "w")
-                json.dump(defaults_from_file, f)
-                f.close()
-            except:
-                self.inform.emit(_("[ERROR_NOTCL] Failed to write defaults to file."))
-                return
-        if self.defaults["global_open_style"] is False:
-            self.file_opened.emit("preferences", filename)
-        self.file_saved.emit("preferences", filename)
-        self.inform.emit("[success] Exported Defaults to %s" % filename)
-
-    def on_preferences_open_folder(self):
-        self.report_usage("on_preferences_open_folder()")
-
-        if sys.platform == 'win32':
-            subprocess.Popen('explorer %s' % self.data_path)
-        elif sys.platform == 'darwin':
-            os.system('open "%s"' % self.data_path)
-        else:
-            subprocess.Popen(['xdg-open', self.data_path])
-        self.inform.emit("[success] FlatCAM Preferences Folder opened.")
-
-    def save_geometry(self, x, y, width, height, notebook_width):
-        self.defaults["global_def_win_x"] = x
-        self.defaults["global_def_win_y"] = y
-        self.defaults["global_def_win_w"] = width
-        self.defaults["global_def_win_h"] = height
-        self.defaults["global_def_notebook_width"] = notebook_width
-        self.save_defaults()
-
-    def message_dialog(self, title, message, kind="info"):
-        icon = {"info": QtWidgets.QMessageBox.Information,
-                "warning": QtWidgets.QMessageBox.Warning,
-                "error": QtWidgets.QMessageBox.Critical}[str(kind)]
-        dlg = QtWidgets.QMessageBox(icon, title, message, parent=self.ui)
-        dlg.setText(message)
-        dlg.exec_()
-
-    def register_recent(self, kind, filename):
-
-        self.log.debug("register_recent()")
-        self.log.debug("   %s" % kind)
-        self.log.debug("   %s" % filename)
-
-        record = {'kind': str(kind), 'filename': str(filename)}
-        if record in self.recent:
-            return
-        if record in self.recent_projects:
-            return
-        if record['kind'] == 'project':
-            self.recent_projects.insert(0, record)
-        else:
-            self.recent.insert(0, record)
-
-        if len(self.recent) > self.defaults['global_recent_limit']:  # Limit reached
-            self.recent.pop()
-
-        if len(self.recent_projects) > self.defaults['global_recent_limit']:  # Limit reached
-            self.recent_projects.pop()
-
-        try:
-            f = open(self.data_path + '/recent.json', 'w')
-        except IOError:
-            App.log.error("Failed to open recent items file for writing.")
-            self.inform.emit(_('[ERROR_NOTCL] Failed to open recent files file for writing.'))
-            return
-
-        json.dump(self.recent, f, default=to_dict, indent=2, sort_keys=True)
-        f.close()
-
-        try:
-            fp = open(self.data_path + '/recent_projects.json', 'w')
-        except IOError:
-            App.log.error("Failed to open recent items file for writing.")
-            self.inform.emit(_('[ERROR_NOTCL] Failed to open recent projects file for writing.'))
-            return
-
-        json.dump(self.recent_projects, fp, default=to_dict, indent=2, sort_keys=True)
-        fp.close()
-
-        # Re-build the recent items menu
-        self.setup_recent_items()
-
-    def new_object(self, kind, name, initialize, active=True, fit=True, plot=True, autoselected=True):
-        """
-        Creates a new specialized FlatCAMObj and attaches it to the application,
-        this is, updates the GUI accordingly, any other records and plots it.
-        This method is thread-safe.
-
-        Notes:
-            * If the name is in use, the self.collection will modify it
-              when appending it to the collection. There is no need to handle
-              name conflicts here.
-
-        :param kind: The kind of object to create. One of 'gerber', 'excellon', 'cncjob' and 'geometry'.
-        :type kind: str
-        :param name: Name for the object.
-        :type name: str
-        :param initialize: Function to run after creation of the object but before it is attached to the application.
-        The function is called with 2 parameters: the new object and the App instance.
-        :type initialize: function
-        :return: None
-        :rtype: None
-        """
-
-        App.log.debug("new_object()")
-        obj_plot = plot
-        obj_autoselected = autoselected
-
-        t0 = time.time()  # Debug
-
-        # ## Create object
-        classdict = {
-            "gerber": FlatCAMGerber,
-            "excellon": FlatCAMExcellon,
-            "cncjob": FlatCAMCNCjob,
-            "geometry": FlatCAMGeometry
-        }
-
-        App.log.debug("Calling object constructor...")
-        obj = classdict[kind](name)
-        obj.units = self.options["units"]  # TODO: The constructor should look at defaults.
-
-        # Set options from "Project options" form
-        self.options_read_form()
-
-        # IMPORTANT
-        # The key names in defaults and options dictionary's are not random:
-        # they have to have in name first the type of the object (geometry, excellon, cncjob and gerber) or how it's
-        # called here, the 'kind' followed by an underline. The function called above (self.options_read_form()) copy
-        # the options from project options form into the self.options. After that, below, depending on the type of
-        # object that is created, it will strip the name of the object and the underline (if the original key was
-        # let's say "excellon_toolchange", it will strip the excellon_) and to the obj.options the key will become
-        # "toolchange"
-        for option in self.options:
-            if option.find(kind + "_") == 0:
-                oname = option[len(kind) + 1:]
-                obj.options[oname] = self.options[option]
-
-        obj.isHovering = False
-        obj.notHovering = True
-
-        # Initialize as per user request
-        # User must take care to implement initialize
-        # in a thread-safe way as is is likely that we
-        # have been invoked in a separate thread.
-        t1 = time.time()
-        self.log.debug("%f seconds before initialize()." % (t1 - t0))
-        try:
-            return_value = initialize(obj, self)
-        except Exception as e:
-            msg = _("[ERROR_NOTCL] An internal error has ocurred. See shell.\n")
-            msg += _("Object ({kind}) failed because: {error} \n\n").format(kind=kind, error=str(e))
-            msg += traceback.format_exc()
-            self.inform.emit(msg)
-
-            # if str(e) == "Empty Geometry":
-            #     self.inform.emit("[ERROR_NOTCL] )
-            # else:
-            #     self.inform.emit("[ERROR] Object (%s) failed because: %s" % (kind, str(e)))
-            return "fail"
-
-        t2 = time.time()
-        self.log.debug("%f seconds executing initialize()." % (t2 - t1))
-
-        if return_value == 'fail':
-            log.debug("Object (%s) parsing and/or geometry creation failed." % kind)
-            return "fail"
-
-        # Check units and convert if necessary
-        # This condition CAN be true because initialize() can change obj.units
-        if self.options["units"].upper() != obj.units.upper():
-            self.inform.emit(_("Converting units to ") + self.options["units"] + ".")
-            obj.convert_units(self.options["units"])
-            t3 = time.time()
-            self.log.debug("%f seconds converting units." % (t3 - t2))
-
-        # Create the bounding box for the object and then add the results to the obj.options
-        try:
-            xmin, ymin, xmax, ymax = obj.bounds()
-            obj.options['xmin'] = xmin
-            obj.options['ymin'] = ymin
-            obj.options['xmax'] = xmax
-            obj.options['ymax'] = ymax
-        except:
-            log.warning("The object has no bounds properties.")
-            # don't plot objects with no bounds, there is nothing to plot
-            self.plot = False
-            pass
-
-        FlatCAMApp.App.log.debug("Moving new object back to main thread.")
-
-        # Move the object to the main thread and let the app know that it is available.
-        obj.moveToThread(self.main_thread)
-        self.object_created.emit(obj, obj_plot, obj_autoselected)
-
-        return obj
-
-    def new_excellon_object(self):
-        self.report_usage("new_excellon_object()")
-
-        self.new_object('excellon', 'new_exc', lambda x, y: None, plot=False)
-
-    def new_geometry_object(self):
-        self.report_usage("new_geometry_object()")
-
-        def initialize(obj, self):
-            obj.multitool = False
-
-        self.new_object('geometry', 'new_geo', initialize, plot=False)
-
-    def new_gerber_object(self):
-        self.report_usage("new_gerber_object()")
-
-        def initialize(grb_obj, self):
-            grb_obj.multitool = False
-            grb_obj.source_file = []
-            grb_obj.multigeo = False
-            grb_obj.follow = False
-            grb_obj.apertures = {}
-            grb_obj.solid_geometry = []
-
-            try:
-                grb_obj.options['xmin'] = 0
-                grb_obj.options['ymin'] = 0
-                grb_obj.options['xmax'] = 0
-                grb_obj.options['ymax'] = 0
-            except KeyError:
-                pass
-
-        self.new_object('gerber', 'new_grb', initialize, plot=False)
-
-    def on_object_created(self, obj, plot, autoselect):
-        """
-        Event callback for object creation.
-
-        :param obj: The newly created FlatCAM object.
-        :return: None
-        """
-        t0 = time.time()  # DEBUG
-        self.log.debug("on_object_created()")
-
-        # The Collection might change the name if there is a collision
-        self.collection.append(obj)
-
-        # after adding the object to the collection always update the list of objects that are in the collection
-        self.all_objects_list = self.collection.get_list()
-
-        # self.inform.emit('[selected] %s created & selected: %s' %
-        #                  (str(obj.kind).capitalize(), str(obj.options['name'])))
-        if obj.kind == 'gerber':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='green', name=str(obj.options['name'])))
-        elif obj.kind == 'excellon':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='brown', name=str(obj.options['name'])))
-        elif obj.kind == 'cncjob':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='blue', name=str(obj.options['name'])))
-        elif obj.kind == 'geometry':
-            self.inform.emit(_('[selected] {kind} created/selected: <span style="color:{color};">{name}</span>').format(
-                kind=obj.kind.capitalize(), color='red', name=str(obj.options['name'])))
-
-        # update the SHELL auto-completer model with the name of the new object
-        self.myKeywords.append(obj.options['name'])
-        self.shell._edit.set_model_data(self.myKeywords)
-        self.ui.code_editor.set_model_data(self.myKeywords)
-
-        if autoselect:
-            # select the just opened object but deselect the previous ones
-            self.collection.set_all_inactive()
-            self.collection.set_active(obj.options["name"])
-        else:
-            self.collection.set_all_inactive()
-
-        # here it is done the object plotting
-        def worker_task(t_obj):
-            with self.proc_container.new("Plotting"):
-                if isinstance(t_obj, FlatCAMCNCjob):
-                    t_obj.plot(kind=self.defaults["cncjob_plot_kind"])
-                else:
-                    t_obj.plot()
-                t1 = time.time()  # DEBUG
-                self.log.debug("%f seconds adding object and plotting." % (t1 - t0))
-                self.object_plotted.emit(t_obj)
-
-        # Send to worker
-        # self.worker.add_task(worker_task, [self])
-        if plot is True:
-            self.worker_task.emit({'fcn': worker_task, 'params': [obj]})
-
-    def on_object_changed(self, obj):
-        # update the bounding box data from obj.options
-        xmin, ymin, xmax, ymax = obj.bounds()
-        obj.options['xmin'] = xmin
-        obj.options['ymin'] = ymin
-        obj.options['xmax'] = xmax
-        obj.options['ymax'] = ymax
-
-        log.debug("Object changed, updating the bounding box data on self.options")
-        # delete the old selection shape
-        self.delete_selection_shape()
-        self.should_we_save = True
-
-    def on_object_plotted(self, obj):
-        self.on_zoom_fit(None)
-
-    def options_read_form(self):
-        for option in self.options_form_fields:
-            self.options[option] = self.options_form_fields[option].get_value()
-
-    def options_write_form(self):
-        for option in self.options:
-            self.options_write_form_field(option)
-
-    def options_write_form_field(self, field):
-        try:
-            self.options_form_fields[field].set_value(self.options[field])
-        except KeyError:
-            # Changed from error to debug. This allows to have data stored
-            # which is not user-editable.
-            # self.log.debug("options_write_form_field(): No field for: %s" % field)
-            pass
-
-    def on_about(self):
-        """
-        Displays the "about" dialog.
-
-        :return: None
-        """
-        self.report_usage("on_about")
-
-        version = self.version
-        version_date = self.version_date
-        beta = self.beta
-
-        class AboutDialog(QtWidgets.QDialog):
-            def __init__(self, parent=None):
-                QtWidgets.QDialog.__init__(self, parent)
-
-                # Icon and title
-                self.setWindowIcon(parent.app_icon)
-                self.setWindowTitle("FlatCAM")
-
-                layout1 = QtWidgets.QVBoxLayout()
-                self.setLayout(layout1)
-
-                layout2 = QtWidgets.QHBoxLayout()
-                layout1.addLayout(layout2)
-
-                logo = QtWidgets.QLabel()
-                logo.setPixmap(QtGui.QPixmap('share/flatcam_icon256.png'))
-                layout2.addWidget(logo, stretch=0)
-
-                title = QtWidgets.QLabel(
-                    _(
-                        "<font size=8><B>FlatCAM</B></font><BR>"
-                        "Version {version} {beta} ({date}) - {arch} <BR>"
-                        "<BR>"
-                        "2D Computer-Aided Printed Circuit Board<BR>"
-                        "Manufacturing.<BR>"
-                        "<BR>"
-                        "(c) 2014-2019 <B>Juan Pablo Caram</B><BR>"
-                        "<BR>"
-                        "<B> Main Contributors:</B><BR>"
-                        "Denis Hayrullin<BR>"
-                        "Kamil Sopko<BR>"
-                        "Marius Stanciu<BR>"
-                        "Matthieu Berthomé<BR>"
-                        "and many others found "
-                        "<a href = \"https://bitbucket.org/jpcgt/flatcam/pull-requests/?state=MERGED\">here.</a><BR>"
-                        "<BR>"
-                        "Development is done "
-                        "<a href = \"https://bitbucket.org/jpcgt/flatcam/src/Beta/\">here.</a><BR>"
-                        "DOWNLOAD area "
-                        "<a href = \"https://bitbucket.org/jpcgt/flatcam/downloads/\">here.</a><BR>"
-                        ""
-                    ).format(version=version,
-                             beta=('BETA' if beta else ''),
-                             date=version_date,
-                             arch=platform.architecture()[0])
-                )
-                title.setOpenExternalLinks(True)
-
-                layout2.addWidget(title, stretch=1)
-
-                layout3 = QtWidgets.QHBoxLayout()
-                layout1.addLayout(layout3)
-                layout3.addStretch()
-                okbtn = QtWidgets.QPushButton(_("Close"))
-                layout3.addWidget(okbtn)
-
-                okbtn.clicked.connect(self.accept)
-
-        AboutDialog(self.ui).exec_()
-
-    def on_file_savedefaults(self):
-        """
-        Callback for menu item File->Save Defaults. Saves application default options
-        ``self.defaults`` to current_defaults.FlatConfig.
-
-        :return: None
-        """
-
-        self.save_defaults()
-
-    # def on_app_exit(self):
-    #     self.report_usage("on_app_exit()")
-    #
-    #     if self.collection.get_list():
-    #         msgbox = QtWidgets.QMessageBox()
-    #         # msgbox.setText("<B>Save changes ...</B>")
-    #         msgbox.setText("There are files/objects opened in FlatCAM. "
-    #                        "\n"
-    #                        "Do you want to Save the project?")
-    #         msgbox.setWindowTitle("Save changes")
-    #         msgbox.setWindowIcon(QtGui.QIcon('share/save_as.png'))
-    #         msgbox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No |
-    #                                   QtWidgets.QMessageBox.Cancel)
-    #         msgbox.setDefaultButton(QtWidgets.QMessageBox.Yes)
-    #
-    #         response = msgbox.exec_()
-    #
-    #         if response == QtWidgets.QMessageBox.Yes:
-    #             self.on_file_saveprojectas(thread=False)
-    #         elif response == QtWidgets.QMessageBox.Cancel:
-    #             return
-    #         self.save_defaults()
-    #     else:
-    #         self.save_defaults()
-    #     log.debug("Application defaults saved ... Exit event.")
-    #     QtWidgets.qApp.quit()
-
-    def save_defaults(self, silent=False):
-        """
-        Saves application default options
-        ``self.defaults`` to current_defaults.FlatConfig.
-
-        :return: None
-        """
-        self.report_usage("save_defaults")
-
-        # Read options from file
-        try:
-            f = open(self.data_path + "/current_defaults.FlatConfig")
-            defaults_file_content = f.read()
-            f.close()
-        except:
-            e = sys.exc_info()[0]
-            App.log.error("Could not load defaults file.")
-            App.log.error(str(e))
-            self.inform.emit(_("[ERROR_NOTCL] Could not load defaults file."))
-            return
-
-        try:
-            defaults = json.loads(defaults_file_content)
-        except:
-            e = sys.exc_info()[0]
-            App.log.error("Failed to parse defaults file.")
-            App.log.error(str(e))
-            self.inform.emit(_("[ERROR_NOTCL] Failed to parse defaults file."))
-            return
-
-        # Update options
-        self.defaults_read_form()
-        defaults.update(self.defaults)
-        self.propagate_defaults(silent=True)
-
-        # Save the toolbar view
-        tb_status = 0
-        if self.ui.toolbarfile.isVisible():
-            tb_status += 1
-
-        if self.ui.toolbargeo.isVisible():
-            tb_status += 2
-
-        if self.ui.toolbarview.isVisible():
-            tb_status += 4
-
-        if self.ui.toolbartools.isVisible():
-            tb_status += 8
-
-        if self.ui.exc_edit_toolbar.isVisible():
-            tb_status += 16
-
-        if self.ui.geo_edit_toolbar.isVisible():
-            tb_status += 32
-
-        if self.ui.grb_edit_toolbar.isVisible():
-            tb_status += 64
-
-        if self.ui.snap_toolbar.isVisible():
-            tb_status += 128
-
-        if self.ui.toolbarshell.isVisible():
-            tb_status += 256
-
-        self.defaults["global_toolbar_view"] = tb_status
-
-        # Save update options
-        try:
-            f = open(self.data_path + "/current_defaults.FlatConfig", "w")
-            json.dump(defaults, f, default=to_dict, indent=2, sort_keys=True)
-            f.close()
-        except:
-            self.inform.emit(_("[ERROR_NOTCL] Failed to write defaults to file."))
-            return
-
-        if not silent:
-            self.inform.emit(_("[success] Defaults saved."))
-
-    def save_factory_defaults(self, silent=False):
-        """
-                Saves application factory default options
-                ``self.defaults`` to factory_defaults.FlatConfig.
-                It's a one time job done just after the first install.
-
-                :return: None
-                """
-        self.report_usage("save_factory_defaults")
-
-        # Read options from file
-        try:
-            f_f_def = open(self.data_path + "/factory_defaults.FlatConfig")
-            factory_defaults_file_content = f_f_def.read()
-            f_f_def.close()
-        except:
-            e = sys.exc_info()[0]
-            App.log.error("Could not load factory defaults file.")
-            App.log.error(str(e))
-            self.inform.emit(_("[ERROR_NOTCL] Could not load factory defaults file."))
-            return
-
-        try:
-            factory_defaults = json.loads(factory_defaults_file_content)
-        except:
-            e = sys.exc_info()[0]
-            App.log.error("Failed to parse factory defaults file.")
-            App.log.error(str(e))
-            self.inform.emit(_("[ERROR_NOTCL] Failed to parse factory defaults file."))
-            return
-
-        # Update options
-        self.defaults_read_form()
-        factory_defaults.update(self.defaults)
-        self.propagate_defaults(silent=True)
-
-        # Save update options
-        try:
-            f_f_def_s = open(self.data_path + "/factory_defaults.FlatConfig", "w")
-            json.dump(factory_defaults, f_f_def_s, default=to_dict, indent=2, sort_keys=True)
-            f_f_def_s.close()
-        except:
-            self.inform.emit(_("[ERROR_NOTCL] Failed to write factory defaults to file."))
-            return
-
-        if silent is False:
-            self.inform.emit(_("Factory defaults saved."))
-
-    def final_save(self):
-
-        if self.save_in_progress:
-            self.inform.emit(_("[WARNING_NOTCL] Application is saving the project. Please wait ..."))
-            return
-
-        if self.should_we_save and self.collection.get_list():
-            msgbox = QtWidgets.QMessageBox()
-            msgbox.setText(_("There are files/objects modified in FlatCAM. "
-                             "\n"
-                             "Do you want to Save the project?"))
-            msgbox.setWindowTitle(_("Save changes"))
-            msgbox.setWindowIcon(QtGui.QIcon('share/save_as.png'))
-            bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.YesRole)
-            bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole)
-            bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.RejectRole)
-
-            msgbox.setDefaultButton(bt_yes)
-            msgbox.exec_()
-            response = msgbox.clickedButton()
-
-            if response == bt_yes:
-                self.on_file_saveprojectas(thread=True, quit=True)
-            elif response == bt_no:
-                self.quit_application()
-            elif response == bt_cancel:
-                return
-        else:
-            self.quit_application()
-
-    def quit_application(self):
-        self.save_defaults()
-        log.debug("App.final_save() --> App Defaults saved.")
-
-        # save toolbar state to file
-        settings = QSettings("Open Source", "FlatCAM")
-        settings.setValue('saved_gui_state', self.ui.saveState())
-        settings.setValue('maximized_gui', self.ui.isMaximized())
-        settings.setValue('language', self.ui.general_defaults_form.general_app_group.language_cb.get_value())
-
-        # This will write the setting to the platform specific storage.
-        del settings
-        log.debug("App.final_save() --> App UI state saved.")
-        QtWidgets.qApp.quit()
-
-    def on_toggle_shell(self):
-        """
-        toggle shell if is  visible close it if  closed open it
-        :return:
-        """
-        self.report_usage("on_toggle_shell()")
-
-        if self.ui.shell_dock.isVisible():
-            self.ui.shell_dock.hide()
-        else:
-            self.ui.shell_dock.show()
-
-    def on_edit_join(self, name=None):
-        """
-        Callback for Edit->Join. Joins the selected geometry objects into
-        a new one.
-
-        :return: None
-        """
-        self.report_usage("on_edit_join()")
-
-        obj_name_single = str(name) if name else "Combo_SingleGeo"
-        obj_name_multi = str(name) if name else "Combo_MultiGeo"
-
-        tooldias = []
-        geo_type_list = set()
-
-        objs = self.collection.get_selected()
-        for obj in objs:
-            geo_type_list.add(obj.multigeo)
-
-        # if len(geo_type_list) == 1 means that all list elements are the same
-        if len(geo_type_list) != 1:
-            self.inform.emit(_("[ERROR] Failed join. The Geometry objects are of different types.\n"
-                               "At least one is MultiGeo type and the other is SingleGeo type. A possibility is to "
-                               "convert from one to another and retry joining \n"
-                               "but in the case of converting from MultiGeo to SingleGeo, informations may be lost and "
-                               "the result may not be what was expected. \n"
-                               "Check the generated GCODE."))
-            return
-
-        # if at least one True object is in the list then due of the previous check, all list elements are True objects
-        if True in geo_type_list:
-            def initialize(obj, app):
-                FlatCAMGeometry.merge(self, geo_list=objs, geo_final=obj, multigeo=True)
-
-                # rename all the ['name] key in obj.tools[tooluid]['data'] to the obj_name_multi
-                for v in obj.tools.values():
-                    v['data']['name'] = obj_name_multi
-            self.new_object("geometry", obj_name_multi, initialize)
-        else:
-            def initialize(obj, app):
-                FlatCAMGeometry.merge(self, geo_list=objs, geo_final=obj, multigeo=False)
-
-                # rename all the ['name] key in obj.tools[tooluid]['data'] to the obj_name_multi
-                for v in obj.tools.values():
-                    v['data']['name'] = obj_name_single
-            self.new_object("geometry", obj_name_single, initialize)
-
-        self.should_we_save = True
-
-    def on_edit_join_exc(self):
-        """
-        Callback for Edit->Join Excellon. Joins the selected excellon objects into
-        a new one.
-
-        :return: None
-        """
-        self.report_usage("on_edit_join_exc()")
-
-        objs = self.collection.get_selected()
-
-        for obj in objs:
-            if not isinstance(obj, FlatCAMExcellon):
-                self.inform.emit(_("[ERROR_NOTCL] Failed. Excellon joining works only on Excellon objects."))
-                return
-
-        def initialize(obj, app):
-            FlatCAMExcellon.merge(self, exc_list=objs, exc_final=obj)
-
-        self.new_object("excellon", 'Combo_Excellon', initialize)
-        self.should_we_save = True
-
-    def on_edit_join_grb(self):
-        """
-                Callback for Edit->Join Gerber. Joins the selected Gerber objects into
-                a new one.
-
-                :return: None
-                """
-        self.report_usage("on_edit_join_grb()")
-
-        objs = self.collection.get_selected()
-
-        for obj in objs:
-            if not isinstance(obj, FlatCAMGerber):
-                self.inform.emit(_("[ERROR_NOTCL] Failed. Gerber joining works only on Gerber objects."))
-                return
-
-        def initialize(obj, app):
-            FlatCAMGerber.merge(self, grb_list=objs, grb_final=obj)
-
-        self.new_object("gerber", 'Combo_Gerber', initialize)
-        self.should_we_save = True
-
-    def on_convert_singlegeo_to_multigeo(self):
-        self.report_usage("on_convert_singlegeo_to_multigeo()")
-
-        obj = self.collection.get_active()
-
-        if obj is None:
-            self.inform.emit(_("[ERROR_NOTCL] Failed. Select a Geometry Object and try again."))
-            return
-
-        if not isinstance(obj, FlatCAMGeometry):
-            self.inform.emit(_("[ERROR_NOTCL] Expected a FlatCAMGeometry, got %s") % type(obj))
-            return
-
-        obj.multigeo = True
-        for tooluid, dict_value in obj.tools.items():
-            dict_value['solid_geometry'] = deepcopy(obj.solid_geometry)
-        if not isinstance(obj.solid_geometry, list):
-            obj.solid_geometry = [obj.solid_geometry]
-        obj.solid_geometry[:] = []
-        obj.plot()
-
-        self.should_we_save = True
-
-        self.inform.emit(_("[success] A Geometry object was converted to MultiGeo type."))
-
-    def on_convert_multigeo_to_singlegeo(self):
-        self.report_usage("on_convert_multigeo_to_singlegeo()")
-
-        obj = self.collection.get_active()
-
-        if obj is None:
-            self.inform.emit(_("[ERROR_NOTCL] Failed. Select a Geometry Object and try again."))
-            return
-
-        if not isinstance(obj, FlatCAMGeometry):
-            self.inform.emit(_("[ERROR_NOTCL] Expected a FlatCAMGeometry, got %s") % type(obj))
-            return
-
-        obj.multigeo = False
-        total_solid_geometry = []
-        for tooluid, dict_value in obj.tools.items():
-            total_solid_geometry += deepcopy(dict_value['solid_geometry'])
-            # clear the original geometry
-            dict_value['solid_geometry'][:] = []
-        obj.solid_geometry = deepcopy(total_solid_geometry)
-        obj.plot()
-
-        self.should_we_save = True
-
-        self.inform.emit(_("[success] A Geometry object was converted to SingleGeo type."))
-
-    def on_options_dict_change(self, field):
-        self.options_write_form_field(field)
-
-        if field == "units":
-            self.set_screen_units(self.options['units'])
-
-    def on_defaults_dict_change(self, field):
-        self.defaults_write_form_field(field)
-
-        if field == "units":
-            self.set_screen_units(self.defaults['units'])
-
-    def set_screen_units(self, units):
-        self.ui.units_label.setText("[" + units.lower() + "]")
-
-    def on_toggle_units(self, no_pref=False):
-        """
-        Callback for the Units radio-button change in the Options tab.
-        Changes the application's default units or the current project's units.
-        If changing the project's units, the change propagates to all of
-        the objects in the project.
-
-        :return: None
-        """
-
-        self.report_usage("on_toggle_units")
-
-        if self.toggle_units_ignore:
-            return
-
-        new_units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        # If option is the same, then ignore
-        if new_units == self.defaults["units"].upper():
-            self.log.debug("on_toggle_units(): Same as defaults, so ignoring.")
-            return
-
-        # Options to scale
-        dimensions = ['gerber_isotooldia', 'gerber_noncoppermargin', 'gerber_bboxmargin',
-
-                      'excellon_drillz',  'excellon_travelz', "excellon_toolchangexy",
-                      'excellon_feedrate', 'excellon_feedrate_rapid', 'excellon_toolchangez',
-                      'excellon_tooldia', 'excellon_slot_tooldia', 'excellon_endz', "excellon_feedrate_probe",
-                      "excellon_z_pdepth",
-
-                      'geometry_cutz',  "geometry_depthperpass", 'geometry_travelz', 'geometry_feedrate',
-                      'geometry_feedrate_rapid', "geometry_toolchangez", "geometry_feedrate_z",
-                      "geometry_toolchangexy", 'geometry_cnctooldia', 'geometry_endz', "geometry_z_pdepth",
-                      "geometry_feedrate_probe",
-
-                      'cncjob_tooldia',
-
-                      'tools_paintmargin', 'tools_painttooldia', 'tools_paintoverlap',
-                      "tools_ncctools", "tools_nccoverlap", "tools_nccmargin",
-                      "tools_2sided_drilldia", "tools_film_boundary",
-                      "tools_cutouttooldia", 'tools_cutoutmargin', 'tools_cutoutgapsize',
-                      "tools_panelize_constrainx", "tools_panelize_constrainy",
-                      "tools_calc_vshape_tip_dia", "tools_calc_vshape_cut_z",
-                      "tools_transform_skew_x", "tools_transform_skew_y", "tools_transform_offset_x",
-                      "tools_transform_offset_y",
-
-                      "tools_solderpaste_tools", "tools_solderpaste_new", "tools_solderpaste_z_start",
-                      "tools_solderpaste_z_dispense", "tools_solderpaste_z_stop", "tools_solderpaste_z_travel",
-                      "tools_solderpaste_z_toolchange", "tools_solderpaste_xy_toolchange", "tools_solderpaste_frxy",
-                      "tools_solderpaste_frz", "tools_solderpaste_frz_dispense",
-
-                      'global_gridx', 'global_gridy', 'global_snap_max']
-
-        def scale_options(sfactor):
-            for dim in dimensions:
-                if dim == 'excellon_toolchangexy':
-                    coordinates = self.defaults["excellon_toolchangexy"].split(",")
-                    coords_xy = [float(eval(a)) for a in coordinates if a != '']
-                    coords_xy[0] *= sfactor
-                    coords_xy[1] *= sfactor
-                    self.options['excellon_toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
-                elif dim == 'geometry_toolchangexy':
-                    coordinates = self.defaults["geometry_toolchangexy"].split(",")
-                    coords_xy = [float(eval(a)) for a in coordinates if a != '']
-                    coords_xy[0] *= sfactor
-                    coords_xy[1] *= sfactor
-                    self.options['geometry_toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
-                elif dim == 'geometry_cnctooldia':
-                    tools_diameters = []
-                    try:
-                        tools_string = self.defaults["geometry_cnctooldia"].split(",")
-                        tools_diameters = [eval(a) for a in tools_string if a != '']
-                    except Exception as e:
-                        log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
-
-                    self.options['geometry_cnctooldia'] = ''
-                    for t in range(len(tools_diameters)):
-                        tools_diameters[t] *= sfactor
-                        self.options['geometry_cnctooldia'] += "%f," % tools_diameters[t]
-                elif dim == 'tools_ncctools':
-                    ncctools = []
-                    try:
-                        tools_string = self.defaults["tools_ncctools"].split(",")
-                        ncctools = [eval(a) for a in tools_string if a != '']
-                    except Exception as e:
-                        log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
-
-                    self.options['tools_ncctools'] = ''
-                    for t in range(len(ncctools)):
-                        ncctools[t] *= sfactor
-                        self.options['tools_ncctools'] += "%f," % ncctools[t]
-                elif dim == 'tools_solderpaste_tools':
-                    sptools = []
-                    try:
-                        tools_string = self.defaults["tools_solderpaste_tools"].split(",")
-                        sptools = [eval(a) for a in tools_string if a != '']
-                    except Exception as e:
-                        log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
-
-                    self.options['tools_solderpaste_tools'] = ""
-                    for t in range(len(sptools)):
-                        sptools[t] *= sfactor
-                        self.options['tools_solderpaste_tools'] += "%f," % sptools[t]
-                elif dim == 'tools_solderpaste_xy_toolchange':
-                    coordinates = self.defaults["tools_solderpaste_xy_toolchange"].split(",")
-                    sp_coords = [float(eval(a)) for a in coordinates if a != '']
-                    sp_coords[0] *= sfactor
-                    sp_coords[1] *= sfactor
-                    self.options['tools_solderpaste_xy_toolchange'] = "%f, %f" % (sp_coords[0], sp_coords[1])
-                elif dim == 'global_gridx' or dim == 'global_gridy':
-                    if new_units == 'IN':
-                        val = 0.1
-                        try:
-                            val = float(self.defaults[dim]) * sfactor
-                        except Exception as e:
-                            log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
-
-                        self.options[dim] = float('%.6f' % val)
-                    else:
-                        val = 0.1
-                        try:
-                            val = float(self.defaults[dim]) * sfactor
-                        except Exception as e:
-                            log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
-
-                        self.options[dim] = float('%.4f' % val)
-                else:
-                    val = 0.1
-                    try:
-                        val = float(self.options[dim]) * sfactor
-                    except Exception as e:
-                        log.debug('App.on_toggle_units().scale_options() --> %s' % str(e))
-
-                    self.options[dim] = val
-
-        def scale_defaults(sfactor):
-            for dim in dimensions:
-                if dim == 'excellon_toolchangexy':
-                    coordinates = self.defaults["excellon_toolchangexy"].split(",")
-                    coords_xy = [float(eval(a)) for a in coordinates if a != '']
-                    coords_xy[0] *= sfactor
-                    coords_xy[1] *= sfactor
-                    self.defaults['excellon_toolchangexy'] = "%.4f, %.4f" % (coords_xy[0], coords_xy[1])
-                elif dim == 'geometry_toolchangexy':
-                    coordinates = self.defaults["geometry_toolchangexy"].split(",")
-                    coords_xy = [float(eval(a)) for a in coordinates if a != '']
-                    coords_xy[0] *= sfactor
-                    coords_xy[1] *= sfactor
-                    self.defaults['geometry_toolchangexy'] = "%.4f, %.4f" % (coords_xy[0], coords_xy[1])
-                elif dim == 'geometry_cnctooldia':
-                    tools_diameters = []
-                    try:
-                        tools_string = self.defaults["geometry_cnctooldia"].split(",")
-                        tools_diameters = [eval(a) for a in tools_string if a != '']
-                    except Exception as e:
-                        log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
-
-                    self.defaults['geometry_cnctooldia'] = ''
-                    for t in range(len(tools_diameters)):
-                        tools_diameters[t] *= sfactor
-                        self.defaults['geometry_cnctooldia'] += "%.4f," % tools_diameters[t]
-                elif dim == 'tools_ncctools':
-                    ncctools = []
-                    try:
-                        tools_string = self.defaults["tools_ncctools"].split(",")
-                        ncctools = [eval(a) for a in tools_string if a != '']
-                    except Exception as e:
-                        log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
-
-                    self.defaults['tools_ncctools'] = ''
-                    for t in range(len(ncctools)):
-                        ncctools[t] *= sfactor
-                        self.defaults['tools_ncctools'] += "%.4f," % ncctools[t]
-                elif dim == 'tools_solderpaste_tools':
-                    sptools = []
-                    try:
-                        tools_string = self.defaults["tools_solderpaste_tools"].split(",")
-                        sptools = [eval(a) for a in tools_string if a != '']
-                    except Exception as e:
-                        log.debug("App.on_toggle_units().scale_options() --> %s" % str(e))
-
-                    self.defaults['tools_solderpaste_tools'] = ""
-                    for t in range(len(sptools)):
-                        sptools[t] *= sfactor
-                        self.defaults['tools_solderpaste_tools'] += "%.4f," % sptools[t]
-                elif dim == 'tools_solderpaste_xy_toolchange':
-                    coordinates = self.defaults["tools_solderpaste_xy_toolchange"].split(",")
-                    sp_coords = [float(eval(a)) for a in coordinates if a != '']
-                    sp_coords[0] *= sfactor
-                    sp_coords[1] *= sfactor
-                    self.defaults['tools_solderpaste_xy_toolchange'] = "%.4f, %.4f" % (sp_coords[0], sp_coords[1])
-                elif dim == 'global_gridx' or dim == 'global_gridy':
-                    if new_units == 'IN':
-                        val = 0.1
-                        try:
-                            val = float(self.defaults[dim]) * sfactor
-                        except Exception as e:
-                            log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
-
-                        self.defaults[dim] = float('%.6f' % val)
-                    else:
-                        val = 0.1
-                        try:
-                            val = float(self.defaults[dim]) * sfactor
-                        except Exception as e:
-                            log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
-
-                        self.defaults[dim] = float('%.4f' % val)
-                else:
-                    val = 0.1
-                    try:
-                        val = float(self.defaults[dim]) * sfactor
-                    except Exception as e:
-                        log.debug('App.on_toggle_units().scale_defaults() --> %s' % str(e))
-
-                    self.defaults[dim] = val
-
-        # The scaling factor depending on choice of units.
-        factor = 1/25.4
-        if new_units == 'MM':
-            factor = 25.4
-
-        # Changing project units. Warn user.
-        msgbox = QtWidgets.QMessageBox()
-        msgbox.setWindowTitle(_("Toggle Units"))
-        msgbox.setWindowIcon(QtGui.QIcon('share/toggle_units32.png'))
-        msgbox.setText(_("<B>Change project units ...</B>"))
-        msgbox.setInformativeText(_("Changing the units of the project causes all geometrical "
-                                    "properties of all objects to be scaled accordingly.\nContinue?"))
-        bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole)
-        bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.RejectRole)
-
-        msgbox.setDefaultButton(bt_ok)
-        msgbox.exec_()
-        response = msgbox.clickedButton()
-
-        if response == bt_ok:
-            if no_pref is False:
-                self.options_read_form()
-                scale_options(factor)
-                self.options_write_form()
-
-                self.defaults_read_form()
-                scale_defaults(factor)
-                self.defaults_write_form(fl_units=new_units)
-
-                # save the defaults to file, some may assume that the conversion is enough and it's not
-                self.on_save_button()
-
-            self.should_we_save = True
-
-            # change this only if the workspace is active
-            if self.defaults['global_workspace'] is True:
-                self.plotcanvas.draw_workspace()
-
-            # adjust the grid values on the main toolbar
-            dec = 6 if new_units == 'IN'else 4
-            val_x = float(self.ui.grid_gap_x_entry.get_value()) * factor
-            self.ui.grid_gap_x_entry.set_value(val_x, decimals=dec)
-            if not self.ui.grid_gap_link_cb.isChecked():
-                val_y = float(self.ui.grid_gap_y_entry.get_value()) * factor
-                self.ui.grid_gap_y_entry.set_value(val_y, decimals=dec)
-
-            for obj in self.collection.get_list():
-                obj.convert_units(new_units)
-
-                # make that the properties stored in the object are also updated
-                self.object_changed.emit(obj)
-                obj.build_ui()
-
-            current = self.collection.get_active()
-            if current is not None:
-                # the transfer of converted values to the UI form for Geometry is done local in the FlatCAMObj.py
-                if not isinstance(current, FlatCAMGeometry):
-                    current.to_form()
-
-            self.plot_all()
-            self.inform.emit(_("[success] Converted units to %s") % new_units)
-            # self.ui.units_label.setText("[" + self.options["units"] + "]")
-            self.set_screen_units(new_units)
-        else:
-            # Undo toggling
-            self.toggle_units_ignore = True
-            if self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
-                self.ui.general_defaults_form.general_app_group.units_radio.set_value('IN')
-            else:
-                self.ui.general_defaults_form.general_app_group.units_radio.set_value('MM')
-            self.toggle_units_ignore = False
-            self.inform.emit(_("[WARNING_NOTCL] Units conversion cancelled."))
-
-        self.options_read_form()
-        self.defaults_read_form()
-
-    def on_toggle_units_click(self):
-        try:
-            self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        if self.defaults["units"] == 'MM':
-            self.ui.general_defaults_form.general_app_group.units_radio.set_value("IN")
-        else:
-            self.ui.general_defaults_form.general_app_group.units_radio.set_value("MM")
-        self.on_toggle_units(no_pref=True)
-
-        self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.connect(
-            lambda: self.on_toggle_units(no_pref=False))
-
-    def on_fullscreen(self):
-        self.report_usage("on_fullscreen()")
-
-        if self.toggle_fscreen is False:
-            if sys.platform == 'win32':
-                self.ui.showFullScreen()
-            for tb in self.ui.findChildren(QtWidgets.QToolBar):
-                tb.setVisible(False)
-            self.ui.splitter_left.setVisible(False)
-            self.toggle_fscreen = True
-        else:
-            if sys.platform == 'win32':
-                self.ui.showNormal()
-            self.restore_toolbar_view()
-            self.ui.splitter_left.setVisible(True)
-            self.toggle_fscreen = False
-
-    def on_toggle_plotarea(self):
-        self.report_usage("on_toggle_plotarea()")
-
-        try:
-            name = self.ui.plot_tab_area.widget(0).objectName()
-        except AttributeError:
-            self.ui.plot_tab_area.addTab(self.ui.plot_tab, "Plot Area")
-            # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON
-            self.ui.plot_tab_area.protectTab(0)
-            return
-
-        if name != 'plotarea':
-            self.ui.plot_tab_area.insertTab(0, self.ui.plot_tab, "Plot Area")
-            # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON
-            self.ui.plot_tab_area.protectTab(0)
-        else:
-            self.ui.plot_tab_area.closeTab(0)
-
-    def on_toggle_notebook(self):
-        if self.ui.splitter.sizes()[0] == 0:
-            self.ui.splitter.setSizes([1, 1])
-        else:
-            self.ui.splitter.setSizes([0, 1])
-
-    def on_toggle_axis(self):
-        self.report_usage("on_toggle_axis()")
-
-        if self.toggle_axis is False:
-            self.plotcanvas.v_line.set_data(color=(0.70, 0.3, 0.3, 1.0))
-            self.plotcanvas.h_line.set_data(color=(0.70, 0.3, 0.3, 1.0))
-            self.plotcanvas.redraw()
-            self.toggle_axis = True
-        else:
-            self.plotcanvas.v_line.set_data(color=(0.0, 0.0, 0.0, 0.0))
-
-            self.plotcanvas.h_line.set_data(color=(0.0, 0.0, 0.0, 0.0))
-            self.plotcanvas.redraw()
-            self.toggle_axis = False
-
-    def on_toggle_grid(self):
-        self.report_usage("on_toggle_grid()")
-
-        self.ui.grid_snap_btn.trigger()
-
-    def on_options_combo_change(self, sel):
-        """
-        Called when the combo box to choose between application defaults and
-        project option changes value. The corresponding variables are
-        copied to the UI.
-
-        :param sel: The option index that was chosen.
-        :return: None
-        """
-
-        # combo_sel = self.ui.notebook.combo_options.get_active()
-        App.log.debug("Options --> %s" % sel)
-
-        # form = [self.defaults_form, self.options_form][sel]
-        # self.ui.notebook.options_contents.pack_start(form, False, False, 1)
-
-        if sel == 0:
-            self.gen_form = self.ui.general_defaults_form
-            self.ger_form = self.ui.gerber_defaults_form
-            self.exc_form = self.ui.excellon_defaults_form
-            self.geo_form = self.ui.geometry_defaults_form
-            self.cnc_form = self.ui.cncjob_defaults_form
-            self.tools_form = self.ui.tools_defaults_form
-        elif sel == 1:
-            self.gen_form = self.ui.general_options_form
-            self.ger_form = self.ui.gerber_options_form
-            self.exc_form = self.ui.excellon_options_form
-            self.geo_form = self.ui.geometry_options_form
-            self.cnc_form = self.ui.cncjob_options_form
-            self.tools_form = self.ui.tools_options_form
-        else:
-            return
-
-        try:
-            self.ui.general_scroll_area.takeWidget()
-        except:
-            self.log.debug("Nothing to remove")
-        self.ui.general_scroll_area.setWidget(self.gen_form)
-        self.gen_form.show()
-
-        try:
-            self.ui.gerber_scroll_area.takeWidget()
-        except:
-            self.log.debug("Nothing to remove")
-        self.ui.gerber_scroll_area.setWidget(self.ger_form)
-        self.ger_form.show()
-
-        try:
-            self.ui.excellon_scroll_area.takeWidget()
-        except:
-            self.log.debug("Nothing to remove")
-        self.ui.excellon_scroll_area.setWidget(self.exc_form)
-        self.exc_form.show()
-
-        try:
-            self.ui.geometry_scroll_area.takeWidget()
-        except:
-            self.log.debug("Nothing to remove")
-        self.ui.geometry_scroll_area.setWidget(self.geo_form)
-        self.geo_form.show()
-
-        try:
-            self.ui.cncjob_scroll_area.takeWidget()
-        except:
-            self.log.debug("Nothing to remove")
-        self.ui.cncjob_scroll_area.setWidget(self.cnc_form)
-        self.cnc_form.show()
-
-        try:
-            self.ui.tools_scroll_area.takeWidget()
-        except:
-            self.log.debug("Nothing to remove")
-        self.ui.tools_scroll_area.setWidget(self.tools_form)
-        self.tools_form.show()
-
-        self.log.debug("Finished GUI form initialization.")
-
-        # self.options2form()
-
-    def on_excellon_defaults_button(self):
-        self.defaults_form_fields["excellon_format_lower_in"].set_value('4')
-        self.defaults_form_fields["excellon_format_upper_in"].set_value('2')
-        self.defaults_form_fields["excellon_format_lower_mm"].set_value('3')
-        self.defaults_form_fields["excellon_format_upper_mm"].set_value('3')
-        self.defaults_form_fields["excellon_zeros"].set_value('L')
-        self.defaults_form_fields["excellon_units"].set_value('INCH')
-        log.debug("Excellon app defaults loaded ...")
-
-    def on_excellon_options_button(self):
-
-        self.options_form_fields["excellon_format_lower_in"].set_value('4')
-        self.options_form_fields["excellon_format_upper_in"].set_value('2')
-        self.options_form_fields["excellon_format_lower_mm"].set_value('3')
-        self.options_form_fields["excellon_format_upper_mm"].set_value('3')
-        self.options_form_fields["excellon_zeros"].set_value('L')
-        self.options_form_fields["excellon_units"].set_value('INCH')
-        log.debug("Excellon options defaults loaded ...")
-
-    # Setting plot colors handlers
-    def on_pf_color_entry(self):
-        self.defaults['global_plot_fill'] = self.ui.general_defaults_form.general_gui_group.pf_color_entry.get_value()[:7] + \
-                                            self.defaults['global_plot_fill'][7:9]
-        self.ui.general_defaults_form.general_gui_group.pf_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_plot_fill'])[:7])
-
-    def on_pf_color_button(self):
-        current_color = QtGui.QColor(self.defaults['global_plot_fill'][:7])
-
-        c_dialog = QtWidgets.QColorDialog()
-        plot_fill_color = c_dialog.getColor(initial=current_color)
-
-        if plot_fill_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.pf_color_button.setStyleSheet(
-            "background-color:%s" % str(plot_fill_color.name()))
-
-        new_val = str(plot_fill_color.name()) + str(self.defaults['global_plot_fill'][7:9])
-        self.ui.general_defaults_form.general_gui_group.pf_color_entry.set_value(new_val)
-        self.defaults['global_plot_fill'] = new_val
-
-    def on_pf_color_spinner(self):
-        spinner_value = self.ui.general_defaults_form.general_gui_group.pf_color_alpha_spinner.value()
-        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.setValue(spinner_value)
-        self.defaults['global_plot_fill'] = self.defaults['global_plot_fill'][:7] + \
-                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
-        self.defaults['global_plot_line'] = self.defaults['global_plot_line'][:7] + \
-                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
-
-    def on_pf_color_slider(self):
-        slider_value = self.ui.general_defaults_form.general_gui_group.pf_color_alpha_slider.value()
-        self.ui.general_defaults_form.general_gui_group.pf_color_alpha_spinner.setValue(slider_value)
-
-    def on_pl_color_entry(self):
-        self.defaults['global_plot_line'] = self.ui.general_defaults_form.general_gui_group.pl_color_entry.get_value()[:7] + \
-                                            self.defaults['global_plot_line'][7:9]
-        self.ui.general_defaults_form.general_gui_group.pl_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_plot_line'])[:7])
-
-    def on_pl_color_button(self):
-        current_color = QtGui.QColor(self.defaults['global_plot_line'][:7])
-        # print(current_color)
-
-        c_dialog = QtWidgets.QColorDialog()
-        plot_line_color = c_dialog.getColor(initial=current_color)
-
-        if plot_line_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.pl_color_button.setStyleSheet(
-            "background-color:%s" % str(plot_line_color.name()))
-
-        new_val_line = str(plot_line_color.name()) + str(self.defaults['global_plot_line'][7:9])
-        self.ui.general_defaults_form.general_gui_group.pl_color_entry.set_value(new_val_line)
-        self.defaults['global_plot_line'] = new_val_line
-
-    # Setting selection colors (left - right) handlers
-    def on_sf_color_entry(self):
-        self.defaults['global_sel_fill'] = self.ui.general_defaults_form.general_gui_group.sf_color_entry.get_value()[:7] + \
-                                            self.defaults['global_sel_fill'][7:9]
-        self.ui.general_defaults_form.general_gui_group.sf_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_sel_fill'])[:7])
-
-    def on_sf_color_button(self):
-        current_color = QtGui.QColor(self.defaults['global_sel_fill'][:7])
-
-        c_dialog = QtWidgets.QColorDialog()
-        plot_fill_color = c_dialog.getColor(initial=current_color)
-
-        if plot_fill_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.sf_color_button.setStyleSheet(
-            "background-color:%s" % str(plot_fill_color.name()))
-
-        new_val = str(plot_fill_color.name()) + str(self.defaults['global_sel_fill'][7:9])
-        self.ui.general_defaults_form.general_gui_group.sf_color_entry.set_value(new_val)
-        self.defaults['global_sel_fill'] = new_val
-
-    def on_sf_color_spinner(self):
-        spinner_value = self.ui.general_defaults_form.general_gui_group.sf_color_alpha_spinner.value()
-        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_slider.setValue(spinner_value)
-        self.defaults['global_sel_fill'] = self.defaults['global_sel_fill'][:7] + \
-                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
-        self.defaults['global_sel_line'] = self.defaults['global_sel_line'][:7] + \
-                                            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
-
-    def on_sf_color_slider(self):
-        slider_value = self.ui.general_defaults_form.general_gui_group.sf_color_alpha_slider.value()
-        self.ui.general_defaults_form.general_gui_group.sf_color_alpha_spinner.setValue(slider_value)
-
-    def on_sl_color_entry(self):
-        self.defaults['global_sel_line'] = self.ui.general_defaults_form.general_gui_group.sl_color_entry.get_value()[:7] + \
-                                            self.defaults['global_sel_line'][7:9]
-        self.ui.general_defaults_form.general_gui_group.sl_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_sel_line'])[:7])
-
-    def on_sl_color_button(self):
-        current_color = QtGui.QColor(self.defaults['global_sel_line'][:7])
-
-        c_dialog = QtWidgets.QColorDialog()
-        plot_line_color = c_dialog.getColor(initial=current_color)
-
-        if plot_line_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.sl_color_button.setStyleSheet(
-            "background-color:%s" % str(plot_line_color.name()))
-
-        new_val_line = str(plot_line_color.name()) + str(self.defaults['global_sel_line'][7:9])
-        self.ui.general_defaults_form.general_gui_group.sl_color_entry.set_value(new_val_line)
-        self.defaults['global_sel_line'] = new_val_line
-
-    # Setting selection colors (right - left) handlers
-    def on_alt_sf_color_entry(self):
-        self.defaults['global_alt_sel_fill'] = self.ui.general_defaults_form.general_gui_group \
-                                   .alt_sf_color_entry.get_value()[:7] + self.defaults['global_alt_sel_fill'][7:9]
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_alt_sel_fill'])[:7])
-
-    def on_alt_sf_color_button(self):
-        current_color = QtGui.QColor(self.defaults['global_alt_sel_fill'][:7])
-
-        c_dialog = QtWidgets.QColorDialog()
-        plot_fill_color = c_dialog.getColor(initial=current_color)
-
-        if plot_fill_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_button.setStyleSheet(
-            "background-color:%s" % str(plot_fill_color.name()))
-
-        new_val = str(plot_fill_color.name()) + str(self.defaults['global_alt_sel_fill'][7:9])
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry.set_value(new_val)
-        self.defaults['global_alt_sel_fill'] = new_val
-
-    def on_alt_sf_color_spinner(self):
-        spinner_value = self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_spinner.value()
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.setValue(spinner_value)
-        self.defaults['global_alt_sel_fill'] = self.defaults['global_alt_sel_fill'][:7] + \
-                                               (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
-        self.defaults['global_alt_sel_line'] = self.defaults['global_alt_sel_line'][:7] + \
-                                               (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
-
-    def on_alt_sf_color_slider(self):
-        slider_value = self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_slider.value()
-        self.ui.general_defaults_form.general_gui_group.alt_sf_color_alpha_spinner.setValue(slider_value)
-
-    def on_alt_sl_color_entry(self):
-        self.defaults['global_alt_sel_line'] = \
-            self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry.get_value()[:7] + \
-            self.defaults['global_alt_sel_line'][7:9]
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_alt_sel_line'])[:7])
-
-    def on_alt_sl_color_button(self):
-        current_color = QtGui.QColor(self.defaults['global_alt_sel_line'][:7])
-
-        c_dialog = QtWidgets.QColorDialog()
-        plot_line_color = c_dialog.getColor(initial=current_color)
-
-        if plot_line_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_button.setStyleSheet(
-            "background-color:%s" % str(plot_line_color.name()))
-
-        new_val_line = str(plot_line_color.name()) + str(self.defaults['global_alt_sel_line'][7:9])
-        self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry.set_value(new_val_line)
-        self.defaults['global_alt_sel_line'] = new_val_line
-
-    # Setting Editor colors
-    def on_draw_color_entry(self):
-        self.defaults['global_draw_color'] = self.ui.general_defaults_form.general_gui_group \
-                                                   .draw_color_entry.get_value()
-        self.ui.general_defaults_form.general_gui_group.draw_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_draw_color']))
-
-    def on_draw_color_button(self):
-        current_color = QtGui.QColor(self.defaults['global_draw_color'])
-
-        c_dialog = QtWidgets.QColorDialog()
-        draw_color = c_dialog.getColor(initial=current_color)
-
-        if draw_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.draw_color_button.setStyleSheet(
-            "background-color:%s" % str(draw_color.name()))
-
-        new_val = str(draw_color.name())
-        self.ui.general_defaults_form.general_gui_group.draw_color_entry.set_value(new_val)
-        self.defaults['global_draw_color'] = new_val
-
-    def on_sel_draw_color_entry(self):
-        self.defaults['global_sel_draw_color'] = self.ui.general_defaults_form.general_gui_group \
-                                                   .sel_draw_color_entry.get_value()
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_sel_draw_color']))
-
-    def on_sel_draw_color_button(self):
-        current_color = QtGui.QColor(self.defaults['global_sel_draw_color'])
-
-        c_dialog = QtWidgets.QColorDialog()
-        sel_draw_color = c_dialog.getColor(initial=current_color)
-
-        if sel_draw_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_button.setStyleSheet(
-            "background-color:%s" % str(sel_draw_color.name()))
-
-        new_val_sel = str(sel_draw_color.name())
-        self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry.set_value(new_val_sel)
-        self.defaults['global_sel_draw_color'] = new_val_sel
-
-    def on_proj_color_entry(self):
-        self.defaults['global_proj_item_color'] = self.ui.general_defaults_form.general_gui_group \
-                                                   .proj_color_entry.get_value()
-        self.ui.general_defaults_form.general_gui_group.proj_color_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_proj_item_color']))
-
-    def on_proj_color_button(self):
-        current_color = QtGui.QColor(self.defaults['global_proj_item_color'])
-
-        c_dialog = QtWidgets.QColorDialog()
-        proj_color = c_dialog.getColor(initial=current_color)
-
-        if proj_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.proj_color_button.setStyleSheet(
-            "background-color:%s" % str(proj_color.name()))
-
-        new_val_sel = str(proj_color.name())
-        self.ui.general_defaults_form.general_gui_group.proj_color_entry.set_value(new_val_sel)
-        self.defaults['global_proj_item_color'] = new_val_sel
-
-    def on_proj_color_dis_entry(self):
-        self.defaults['global_proj_item_dis_color'] = self.ui.general_defaults_form.general_gui_group \
-                                                   .proj_color_dis_entry.get_value()
-        self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['global_proj_item_dis_color']))
-
-    def on_proj_color_dis_button(self):
-        current_color = QtGui.QColor(self.defaults['global_proj_item_dis_color'])
-
-        c_dialog = QtWidgets.QColorDialog()
-        proj_color = c_dialog.getColor(initial=current_color)
-
-        if proj_color.isValid() is False:
-            return
-
-        self.ui.general_defaults_form.general_gui_group.proj_color_dis_button.setStyleSheet(
-            "background-color:%s" % str(proj_color.name()))
-
-        new_val_sel = str(proj_color.name())
-        self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry.set_value(new_val_sel)
-        self.defaults['global_proj_item_dis_color'] = new_val_sel
-
-    def on_annotation_fontcolor_entry(self):
-        self.defaults['cncjob_annotation_fontcolor'] = \
-            self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_entry.get_value()
-        self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_button.setStyleSheet(
-            "background-color:%s" % str(self.defaults['cncjob_annotation_fontcolor']))
-
-    def on_annotation_fontcolor_button(self):
-        current_color = QtGui.QColor(self.defaults['cncjob_annotation_fontcolor'])
-
-        c_dialog = QtWidgets.QColorDialog()
-        annotation_color = c_dialog.getColor(initial=current_color)
-
-        if annotation_color.isValid() is False:
-            return
-
-        self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_button.setStyleSheet(
-            "background-color:%s" % str(annotation_color.name()))
-
-        new_val_sel = str(annotation_color.name())
-        self.ui.cncjob_defaults_form.cncjob_gen_group.annotation_fontcolor_entry.set_value(new_val_sel)
-        self.defaults['global_proj_item_dis_color'] = new_val_sel
-
-    def on_deselect_all(self):
-        self.collection.set_all_inactive()
-        self.delete_selection_shape()
-
-    def on_workspace_modified(self):
-        self.save_defaults(silent=True)
-        self.plotcanvas.draw_workspace()
-
-    def on_workspace(self):
-        self.report_usage("on_workspace()")
-
-        if self.ui.general_defaults_form.general_gui_group.workspace_cb.isChecked():
-            self.plotcanvas.restore_workspace()
-        else:
-            self.plotcanvas.delete_workspace()
-
-        self.save_defaults(silent=True)
-
-    def on_workspace_menu(self):
-        if self.ui.general_defaults_form.general_gui_group.workspace_cb.isChecked():
-            self.ui.general_defaults_form.general_gui_group.workspace_cb.setChecked(False)
-        else:
-            self.ui.general_defaults_form.general_gui_group.workspace_cb.setChecked(True)
-        self.on_workspace()
-
-    def on_layout(self, index=None, lay=None):
-        self.report_usage("on_layout()")
-        if lay:
-            current_layout = lay
-        else:
-            current_layout = self.ui.general_defaults_form.general_gui_set_group.layout_combo.get_value()
-
-        settings = QSettings("Open Source", "FlatCAM")
-        settings.setValue('layout', current_layout)
-
-        # This will write the setting to the platform specific storage.
-        del settings
-
-        # first remove the toolbars:
-        try:
-            self.ui.removeToolBar(self.ui.toolbarfile)
-            self.ui.removeToolBar(self.ui.toolbargeo)
-            self.ui.removeToolBar(self.ui.toolbarview)
-            self.ui.removeToolBar(self.ui.toolbarshell)
-            self.ui.removeToolBar(self.ui.toolbartools)
-            self.ui.removeToolBar(self.ui.exc_edit_toolbar)
-            self.ui.removeToolBar(self.ui.geo_edit_toolbar)
-            self.ui.removeToolBar(self.ui.grb_edit_toolbar)
-            self.ui.removeToolBar(self.ui.snap_toolbar)
-            self.ui.removeToolBar(self.ui.toolbarshell)
-        except Exception as e:
-            pass
-
-        if current_layout == 'standard':
-            # ## TOOLBAR INSTALLATION # ##
-            self.ui.toolbarfile = QtWidgets.QToolBar('File Toolbar')
-            self.ui.toolbarfile.setObjectName('File_TB')
-            self.ui.addToolBar(self.ui.toolbarfile)
-
-            self.ui.toolbargeo = QtWidgets.QToolBar('Edit Toolbar')
-            self.ui.toolbargeo.setObjectName('Edit_TB')
-            self.ui.addToolBar(self.ui.toolbargeo)
-
-            self.ui.toolbarview = QtWidgets.QToolBar('View Toolbar')
-            self.ui.toolbarview.setObjectName('View_TB')
-            self.ui.addToolBar(self.ui.toolbarview)
-
-            self.ui.toolbarshell = QtWidgets.QToolBar('Shell Toolbar')
-            self.ui.toolbarshell.setObjectName('Shell_TB')
-            self.ui.addToolBar(self.ui.toolbarshell)
-
-            self.ui.toolbartools = QtWidgets.QToolBar('Tools Toolbar')
-            self.ui.toolbartools.setObjectName('Tools_TB')
-            self.ui.addToolBar(self.ui.toolbartools)
-
-            self.ui.exc_edit_toolbar = QtWidgets.QToolBar('Excellon Editor Toolbar')
-            self.ui.exc_edit_toolbar.setVisible(False)
-            self.ui.exc_edit_toolbar.setObjectName('ExcEditor_TB')
-            self.ui.addToolBar(self.ui.exc_edit_toolbar)
-
-            self.ui.geo_edit_toolbar = QtWidgets.QToolBar('Geometry Editor Toolbar')
-            self.ui.geo_edit_toolbar.setVisible(False)
-            self.ui.geo_edit_toolbar.setObjectName('GeoEditor_TB')
-            self.ui.addToolBar(self.ui.geo_edit_toolbar)
-
-            self.ui.grb_edit_toolbar = QtWidgets.QToolBar('Gerber Editor Toolbar')
-            self.ui.grb_edit_toolbar.setVisible(False)
-            self.ui.grb_edit_toolbar.setObjectName('GrbEditor_TB')
-            self.ui.addToolBar(self.ui.grb_edit_toolbar)
-
-            self.ui.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar')
-            self.ui.snap_toolbar.setObjectName('Snap_TB')
-            # self.ui.snap_toolbar.setMaximumHeight(30)
-            self.ui.addToolBar(self.ui.snap_toolbar)
-
-            self.ui.corner_snap_btn.setVisible(False)
-            self.ui.snap_magnet.setVisible(False)
-        elif current_layout == 'compact':
-            # ## TOOLBAR INSTALLATION # ##
-            self.ui.toolbarfile = QtWidgets.QToolBar('File Toolbar')
-            self.ui.toolbarfile.setObjectName('File_TB')
-            self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.toolbarfile)
-            self.ui.toolbargeo = QtWidgets.QToolBar('Edit Toolbar')
-            self.ui.toolbargeo.setObjectName('Edit_TB')
-            self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.toolbargeo)
-            self.ui.toolbarview = QtWidgets.QToolBar('View Toolbar')
-            self.ui.toolbarview.setObjectName('View_TB')
-            self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.toolbarview)
-
-            self.ui.toolbarshell = QtWidgets.QToolBar('Shell Toolbar')
-            self.ui.toolbarshell.setObjectName('Shell_TB')
-            self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.toolbarshell)
-
-            self.ui.toolbartools = QtWidgets.QToolBar('Tools Toolbar')
-            self.ui.toolbartools.setObjectName('Tools_TB')
-            self.ui.addToolBar(Qt.LeftToolBarArea, self.ui.toolbartools)
-
-            self.ui.geo_edit_toolbar = QtWidgets.QToolBar('Geometry Editor Toolbar')
-            # self.ui.geo_edit_toolbar.setVisible(False)
-            self.ui.geo_edit_toolbar.setObjectName('GeoEditor_TB')
-            self.ui.addToolBar(Qt.RightToolBarArea, self.ui.geo_edit_toolbar)
-
-            self.ui.grb_edit_toolbar = QtWidgets.QToolBar('Gerber Editor Toolbar')
-            # self.ui.grb_edit_toolbar.setVisible(False)
-            self.ui.grb_edit_toolbar.setObjectName('GrbEditor_TB')
-            self.ui.addToolBar(Qt.RightToolBarArea, self.ui.grb_edit_toolbar)
-
-            self.ui.exc_edit_toolbar = QtWidgets.QToolBar('Excellon Editor Toolbar')
-            self.ui.exc_edit_toolbar.setObjectName('ExcEditor_TB')
-            self.ui.addToolBar(Qt.RightToolBarArea, self.ui.exc_edit_toolbar)
-
-            self.ui.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar')
-            self.ui.snap_toolbar.setObjectName('Snap_TB')
-            self.ui.snap_toolbar.setMaximumHeight(30)
-            self.ui.splitter_left.addWidget(self.ui.snap_toolbar)
-
-            self.ui.corner_snap_btn.setVisible(True)
-            self.ui.snap_magnet.setVisible(True)
-
-        # add all the actions to the toolbars
-        self.ui.populate_toolbars()
-
-        # reconnect all the signals to the toolbar actions
-        self.connect_toolbar_signals()
-
-        self.ui.grid_snap_btn.setChecked(True)
-        self.ui.grid_gap_x_entry.setText(str(self.defaults["global_gridx"]))
-        self.ui.grid_gap_y_entry.setText(str(self.defaults["global_gridy"]))
-        self.ui.snap_max_dist_entry.setText(str(self.defaults["global_snap_max"]))
-        self.ui.grid_gap_link_cb.setChecked(True)
-
-    def on_cnc_custom_parameters(self, signal_text):
-        if signal_text == 'Parameters':
-            return
-        else:
-            self.ui.cncjob_defaults_form.cncjob_adv_opt_group.toolchange_text.insertPlainText('%%%s%%' % signal_text)
-
-    def on_save_button(self):
-        log.debug("App.on_save_button() --> Saving preferences to file.")
-        self.preferences_changed_flag = False
-
-        self.save_defaults(silent=False)
-        # load the defaults so they are updated into the app
-        self.load_defaults(filename='current_defaults')
-        # Re-fresh project options
-        self.on_options_app2project()
-
-    def handlePrint(self):
-        self.report_usage("handlePrint()")
-
-        dialog = QtPrintSupport.QPrintDialog()
-        if dialog.exec_() == QtWidgets.QDialog.Accepted:
-            self.ui.code_editor.document().print_(dialog.printer())
-
-    def handlePreview(self):
-        self.report_usage("handlePreview()")
-
-        dialog = QtPrintSupport.QPrintPreviewDialog()
-        dialog.paintRequested.connect(self.ui.code_editor.print_)
-        dialog.exec_()
-
-    def handleTextChanged(self):
-        # enable = not self.ui.code_editor.document().isEmpty()
-        # self.ui.buttonPrint.setEnabled(enable)
-        # self.ui.buttonPreview.setEnabled(enable)
-        pass
-
-    def handleOpen(self, filt=None):
-        self.report_usage("handleOpen()")
-
-        if filt:
-            _filter_ = filt
-        else:
-            _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
-                       "All Files (*.*)"
-
-        path, _f = QtWidgets.QFileDialog.getOpenFileName(
-            caption=_('Open file'), directory=self.get_last_folder(), filter=_filter_)
-
-        if path:
-            file = QtCore.QFile(path)
-            if file.open(QtCore.QIODevice.ReadOnly):
-                stream = QtCore.QTextStream(file)
-                self.gcode_edited = stream.readAll()
-                self.ui.code_editor.setPlainText(self.gcode_edited)
-                file.close()
-
-    def handleSaveGCode(self, name=None, filt=None):
-        self.report_usage("handleSaveGCode()")
-
-        if filt:
-            _filter_ = filt
-        else:
-            _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
-                       "All Files (*.*)"
-
-        if name:
-            obj_name = name
-        else:
-            try:
-                obj_name = self.collection.get_active().options['name']
-            except AttributeError:
-                obj_name = 'file'
-                if filt is None:
-                    _filter_ = "FlatConfig Files (*.FlatConfig);;All Files (*.*)"
-
-        try:
-            filename = str(QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export G-Code ..."),
-                directory=self.defaults["global_last_folder"] + '/' + str(obj_name),
-                filter=_filter_
-            )[0])
-        except TypeError:
-            filename = str(QtWidgets.QFileDialog.getSaveFileName(caption=_("Export G-Code ..."), filter=_filter_)[0])
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] Export Code cancelled."))
-            return
-        else:
-            try:
-                my_gcode = self.ui.code_editor.toPlainText()
-                with open(filename, 'w') as f:
-                    for line in my_gcode:
-                        f.write(line)
-            except FileNotFoundError:
-                self.inform.emit(_("[WARNING] No such file or directory"))
-                return
-            except PermissionError:
-                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
-                                   "Most likely another app is holding the file open and not accessible."))
-                return
-
-        # Just for adding it to the recent files list.
-        if self.defaults["global_open_style"] is False:
-            self.file_opened.emit("cncjob", filename)
-        self.file_saved.emit("cncjob", filename)
-        self.inform.emit(_("Saved to: %s") % filename)
-
-    def handleFindGCode(self):
-        self.report_usage("handleFindGCode()")
-
-        flags = QtGui.QTextDocument.FindCaseSensitively
-        text_to_be_found = self.ui.entryFind.get_value()
-
-        r = self.ui.code_editor.find(str(text_to_be_found), flags)
-        if r is False:
-            self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
-
-    def handleReplaceGCode(self):
-        self.report_usage("handleReplaceGCode()")
-
-        old = self.ui.entryFind.get_value()
-        new = self.ui.entryReplace.get_value()
-
-        if self.ui.sel_all_cb.isChecked():
-            while True:
-                cursor = self.ui.code_editor.textCursor()
-                cursor.beginEditBlock()
-                flags = QtGui.QTextDocument.FindCaseSensitively
-                # self.ui.editor is the QPlainTextEdit
-                r = self.ui.code_editor.find(str(old), flags)
-                if r:
-                    qc = self.ui.code_editor.textCursor()
-                    if qc.hasSelection():
-                        qc.insertText(new)
-                else:
-                    self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
-                    break
-            # Mark end of undo block
-            cursor.endEditBlock()
-        else:
-            cursor = self.ui.code_editor.textCursor()
-            cursor.beginEditBlock()
-            qc = self.ui.code_editor.textCursor()
-            if qc.hasSelection():
-                qc.insertText(new)
-            # Mark end of undo block
-            cursor.endEditBlock()
-
-    def on_tool_add_keypress(self):
-        # ## Current application units in Upper Case
-        self.units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        notebook_widget_name = self.ui.notebook.currentWidget().objectName()
-
-        # work only if the notebook tab on focus is the Selected_Tab and only if the object is Geometry
-        if notebook_widget_name == 'selected_tab':
-            if str(type(self.collection.get_active())) == "<class 'FlatCAMObj.FlatCAMGeometry'>":
-                # Tool add works for Geometry only if Advanced is True in Preferences
-                if self.defaults["global_app_level"] == 'a':
-                    tool_add_popup = FCInputDialog(title="New Tool ...",
-                                                   text='Enter a Tool Diameter:',
-                                                   min=0.0000, max=99.9999, decimals=4)
-                    tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
-
-                    val, ok = tool_add_popup.get_value()
-                    if ok:
-                        if float(val) == 0:
-                            self.inform.emit(
-                                _("[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format."))
-                            return
-                        self.collection.get_active().on_tool_add(dia=float(val))
-                    else:
-                        self.inform.emit(
-                            _("[WARNING_NOTCL] Adding Tool cancelled ..."))
-                else:
-                    msgbox = QtWidgets.QMessageBox()
-                    msgbox.setText(_("Adding Tool works only when Advanced is checked.\n"
-                                   "Go to Preferences -> General - Show Advanced Options."))
-                    msgbox.setWindowTitle("Tool adding ...")
-                    msgbox.setWindowIcon(QtGui.QIcon('share/warning.png'))
-                    bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole)
-
-                    msgbox.setDefaultButton(bt_ok)
-                    msgbox.exec_()
-
-        # work only if the notebook tab on focus is the Tools_Tab
-        if notebook_widget_name == 'tool_tab':
-            tool_widget = self.ui.tool_scroll_area.widget().objectName()
-
-            tool_add_popup = FCInputDialog(title="New Tool ...",
-                                           text='Enter a Tool Diameter:',
-                                           min=0.0000, max=99.9999, decimals=4)
-            tool_add_popup.setWindowIcon(QtGui.QIcon('share/letter_t_32.png'))
-
-            val, ok = tool_add_popup.get_value()
-
-            # and only if the tool is NCC Tool
-            if tool_widget == self.ncclear_tool.toolName:
-                if ok:
-                    if float(val) == 0:
-                        self.inform.emit(
-                            _("[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format."))
-                        return
-                    self.ncclear_tool.on_tool_add(dia=float(val))
-                else:
-                    self.inform.emit(
-                        _("[WARNING_NOTCL] Adding Tool cancelled ..."))
-            # and only if the tool is Paint Area Tool
-            elif tool_widget == self.paint_tool.toolName:
-                if ok:
-                    if float(val) == 0:
-                        self.inform.emit(
-                            _("[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format."))
-                        return
-                    self.paint_tool.on_tool_add(dia=float(val))
-                else:
-                    self.inform.emit(
-                        _("[WARNING_NOTCL] Adding Tool cancelled ..."))
-            # and only if the tool is Solder Paste Dispensing Tool
-            elif tool_widget == self.paste_tool.toolName:
-                if ok:
-                    if float(val) == 0:
-                        self.inform.emit(
-                            _("[WARNING_NOTCL] Please enter a tool diameter with non-zero value, in Float format."))
-                        return
-                    self.paste_tool.on_tool_add(dia=float(val))
-                else:
-                    self.inform.emit(
-                        _("[WARNING_NOTCL] Adding Tool cancelled ..."))
-
-
-    # It's meant to delete tools in tool tables via a 'Delete' shortcut key but only if certain conditions are met
-    # See description bellow.
-    def on_delete_keypress(self):
-        notebook_widget_name = self.ui.notebook.currentWidget().objectName()
-
-        # work only if the notebook tab on focus is the Selected_Tab and only if the object is Geometry
-        if notebook_widget_name == 'selected_tab':
-            if str(type(self.collection.get_active())) == "<class 'FlatCAMObj.FlatCAMGeometry'>":
-                self.collection.get_active().on_tool_delete()
-
-        # work only if the notebook tab on focus is the Tools_Tab
-        elif notebook_widget_name == 'tool_tab':
-            tool_widget = self.ui.tool_scroll_area.widget().objectName()
-
-            # and only if the tool is NCC Tool
-            if tool_widget == self.ncclear_tool.toolName:
-                self.ncclear_tool.on_tool_delete()
-
-            # and only if the tool is Paint Tool
-            elif tool_widget == self.paint_tool.toolName:
-                self.paint_tool.on_tool_delete()
-
-            # and only if the tool is Solder Paste Dispensing Tool
-            elif tool_widget == self.paste_tool.toolName:
-                self.paste_tool.on_tool_delete()
-        else:
-            self.on_delete()
-
-    # It's meant to delete selected objects. It work also activated by a shortcut key 'Delete' same as above so in
-    # some screens you have to be careful where you hover with your mouse.
-    # Hovering over Selected tab, if the selected tab is a Geometry it will delete tools in tool table. But even if
-    # there is a Selected tab in focus with a Geometry inside, if you hover over canvas it will delete an object.
-    # Complicated, I know :)
-    def on_delete(self):
-        """
-        Delete the currently selected FlatCAMObjs.
-
-        :return: None
-        """
-        self.report_usage("on_delete()")
-
-        # Make sure that the deletion will happen only after the Editor is no longer active otherwise we might delete
-        # a geometry object before we update it.
-        if self.geo_editor.editor_active is False and self.exc_editor.editor_active is False:
-            if self.collection.get_active():
-                self.log.debug("App.on_delete()")
-
-                while (self.collection.get_active()):
-                    obj_active = self.collection.get_active()
-                    # if the deleted object is FlatCAMGerber then make sure to delete the possible mark shapes
-                    if isinstance(obj_active, FlatCAMGerber):
-                        for el in obj_active.mark_shapes:
-                            obj_active.mark_shapes[el].clear(update=True)
-                            obj_active.mark_shapes[el].enabled = False
-                            obj_active.mark_shapes[el] = None
-                    self.delete_first_selected()
-
-                self.inform.emit(_("Object(s) deleted ..."))
-                # make sure that the selection shape is deleted, too
-                self.delete_selection_shape()
-            else:
-                self.inform.emit(_("Failed. No object(s) selected..."))
-        else:
-            self.inform.emit(_("Save the work in Editor and try again ..."))
-
-    def delete_first_selected(self):
-        # Keep this for later
-        try:
-            sel_obj = self.collection.get_active()
-            name = sel_obj.options["name"]
-        except AttributeError:
-            self.log.debug("Nothing selected for deletion")
-            return
-
-        # Remove plot
-        # self.plotcanvas.figure.delaxes(self.collection.get_active().axes)
-        # self.plotcanvas.auto_adjust_axes()
-
-        # Clear form
-        self.setup_component_editor()
-
-        # Remove from dictionary
-        self.collection.delete_active()
-
-        self.inform.emit("Object deleted: %s" % name)
-
-    def on_set_origin(self):
-        """
-        Set the origin to the left mouse click position
-
-        :return: None
-        """
-
-        # display the message for the user
-        # and ask him to click on the desired position
-        self.report_usage("on_set_origin()")
-
-        self.inform.emit(_('Click to set the origin ...'))
-        self.plotcanvas.vis_connect('mouse_press', self.on_set_zero_click)
-
-    def on_jump_to(self, custom_location=None, fit_center=True):
-        """
-        Jump to a location by setting the mouse cursor location
-        :return:
-
-        """
-        self.report_usage("on_jump_to()")
-
-        if not custom_location:
-            dia_box = Dialog_box(title=_("Jump to ..."),
-                                 label=_("Enter the coordinates in format X,Y:"),
-                                 icon=QtGui.QIcon('share/jump_to16.png'))
-
-            if dia_box.ok is True:
-                try:
-                    location = eval(dia_box.location)
-                    if not isinstance(location, tuple):
-                        self.inform.emit(_("Wrong coordinates. Enter coordinates in format: X,Y"))
-                        return
-                except:
-                    return
-            else:
-                return
-        else:
-            location = custom_location
-
-        if fit_center:
-            self.plotcanvas.fit_center(loc=location)
-
-        cursor = QtGui.QCursor()
-
-        canvas_origin = self.plotcanvas.vispy_canvas.native.mapToGlobal(QtCore.QPoint(0, 0))
-        jump_loc = self.plotcanvas.vispy_canvas.translate_coords_2((location[0], location[1]))
-
-        cursor.setPos(canvas_origin.x() + jump_loc[0], (canvas_origin.y() + jump_loc[1]))
-        self.inform.emit(_("[success] Done."))
-
-    def on_copy_object(self):
-        self.report_usage("on_copy_object()")
-
-        def initialize(obj_init, app):
-            obj_init.solid_geometry = obj.solid_geometry
-            try:
-                obj_init.follow_geometry = obj.follow_geometry
-            except AttributeError:
-                pass
-            try:
-                obj_init.apertures = obj.apertures
-            except AttributeError:
-                pass
-
-            try:
-                if obj.tools:
-                    obj_init.tools = obj.tools
-            except Exception as e:
-                log.debug("App.on_copy_object() --> %s" % str(e))
-
-        def initialize_excellon(obj_init, app):
-            obj_init.tools = obj.tools
-
-            # drills are offset, so they need to be deep copied
-            obj_init.drills = deepcopy(obj.drills)
-            # slots are offset, so they need to be deep copied
-            obj_init.slots = deepcopy(obj.slots)
-            obj_init.create_geometry()
-
-        for obj in self.collection.get_selected():
-            obj_name = obj.options["name"]
-
-            try:
-                if isinstance(obj, FlatCAMExcellon):
-                    self.new_object("excellon", str(obj_name) + "_copy", initialize_excellon)
-                elif isinstance(obj,FlatCAMGerber):
-                    self.new_object("gerber", str(obj_name) + "_copy", initialize)
-                elif isinstance(obj,FlatCAMGeometry):
-                    self.new_object("geometry", str(obj_name) + "_copy", initialize)
-            except Exception as e:
-                return "Operation failed: %s" % str(e)
-
-    def on_copy_object2(self, custom_name):
-
-        def initialize_geometry(obj_init, app):
-            obj_init.solid_geometry = obj.solid_geometry
-            try:
-                obj_init.follow_geometry = obj.follow_geometry
-            except AttributeError:
-                pass
-            try:
-                obj_init.apertures = obj.apertures
-            except AttributeError:
-                pass
-
-            try:
-                if obj.tools:
-                    obj_init.tools = obj.tools
-            except Exception as e:
-                log.debug("on_copy_object2() --> %s" % str(e))
-
-        def initialize_gerber(obj_init, app):
-            obj_init.solid_geometry = obj.solid_geometry
-            obj_init.apertures = deepcopy(obj.apertures)
-            obj_init.aperture_macros = deepcopy(obj.aperture_macros)
-
-        def initialize_excellon(obj_init, app):
-            obj_init.tools = obj.tools
-            # drills are offset, so they need to be deep copied
-            obj_init.drills = deepcopy(obj.drills)
-            # slots are offset, so they need to be deep copied
-            obj_init.slots = deepcopy(obj.slots)
-            obj_init.create_geometry()
-
-        for obj in self.collection.get_selected():
-            obj_name = obj.options["name"]
-            try:
-                if isinstance(obj, FlatCAMExcellon):
-                    self.new_object("excellon", str(obj_name) + custom_name, initialize_excellon)
-                elif isinstance(obj,FlatCAMGerber):
-                    self.new_object("gerber", str(obj_name) + custom_name, initialize_gerber)
-                elif isinstance(obj,FlatCAMGeometry):
-                    self.new_object("geometry", str(obj_name) + custom_name, initialize_geometry)
-            except Exception as e:
-                return "Operation failed: %s" % str(e)
-
-    def on_rename_object(self, text):
-        self.report_usage("on_rename_object()")
-
-        named_obj = self.collection.get_active()
-        for obj in named_obj:
-            if obj is list:
-                self.on_rename_object(text)
-            else:
-                try:
-                    obj.options['name'] = text
-                except Exception as e:
-                    log.warning("App.on_rename_object() --> Could not rename the object in the list. --> %s" % str(e))
-
-    def convert_any2geo(self):
-        self.report_usage("convert_any2geo()")
-
-        def initialize(obj_init, app):
-            obj_init.solid_geometry = obj.solid_geometry
-            try:
-                obj_init.follow_geometry = obj.follow_geometry
-            except AttributeError:
-                pass
-            try:
-                obj_init.apertures = obj.apertures
-            except AttributeError:
-                pass
-
-            try:
-                if obj.tools:
-                    obj_init.tools = obj.tools
-            except AttributeError:
-                pass
-
-        def initialize_excellon(obj_init, app):
-            # objs = self.collection.get_selected()
-            # FlatCAMGeometry.merge(objs, obj)
-            solid_geo = []
-            for tool in obj.tools:
-                for geo in obj.tools[tool]['solid_geometry']:
-                    solid_geo.append(geo)
-            obj_init.solid_geometry = deepcopy(solid_geo)
-
-        if not self.collection.get_selected():
-            log.warning("App.convert_any2geo --> No object selected")
-            self.inform.emit(_("[WARNING_NOTCL] No object is selected. Select an object and try again."))
-            return
-
-        for obj in self.collection.get_selected():
-            obj_name = obj.options["name"]
-
-            try:
-                if isinstance(obj, FlatCAMExcellon):
-                    self.new_object("geometry", str(obj_name) + "_conv", initialize_excellon)
-                else:
-                    self.new_object("geometry", str(obj_name) + "_conv", initialize)
-            except Exception as e:
-                return "Operation failed: %s" % str(e)
-
-    def convert_any2gerber(self):
-        self.report_usage("convert_any2gerber()")
-
-        def initialize_geometry(obj_init, app):
-            apertures = {}
-            apid = 0
-
-            apertures[str(apid)] = {}
-            apertures[str(apid)]['geometry'] = []
-            for obj_orig in obj.solid_geometry:
-                new_elem = dict()
-                new_elem['solid'] = obj_orig
-                new_elem['follow'] = obj_orig.exterior
-                apertures[str(apid)]['geometry'].append(deepcopy(new_elem))
-            apertures[str(apid)]['size'] = 0.0
-            apertures[str(apid)]['type'] = 'C'
-
-            obj_init.solid_geometry = deepcopy(obj.solid_geometry)
-            obj_init.apertures = deepcopy(apertures)
-
-        def initialize_excellon(obj_init, app):
-            apertures = {}
-
-            apid = 10
-            for tool in obj.tools:
-                apertures[str(apid)] = {}
-                apertures[str(apid)]['geometry'] = []
-                for geo in obj.tools[tool]['solid_geometry']:
-                    new_el = dict()
-                    new_el['solid'] = geo
-                    new_el['follow'] = geo.exterior
-                    apertures[str(apid)]['geometry'].append(deepcopy(new_el))
-
-                apertures[str(apid)]['size'] = float(obj.tools[tool]['C'])
-                apertures[str(apid)]['type'] = 'C'
-                apid += 1
-
-            # create solid_geometry
-            solid_geometry = []
-            for apid in apertures:
-                for geo_el in apertures[apid]['geometry']:
-                    solid_geometry.append(geo_el['solid'])
-
-            solid_geometry = MultiPolygon(solid_geometry)
-            solid_geometry = solid_geometry.buffer(0.0000001)
-
-            obj_init.solid_geometry = deepcopy(solid_geometry)
-            obj_init.apertures = deepcopy(apertures)
-            # clear the working objects (perhaps not necessary due of Python GC)
-            apertures.clear()
-
-        if not self.collection.get_selected():
-            log.warning("App.convert_any2gerber --> No object selected")
-            self.inform.emit(_("[WARNING_NOTCL] No object is selected. Select an object and try again."))
-            return
-
-        for obj in self.collection.get_selected():
-
-            obj_name = obj.options["name"]
-
-            try:
-                if isinstance(obj, FlatCAMExcellon):
-                    self.new_object("gerber", str(obj_name) + "_conv", initialize_excellon)
-                elif isinstance(obj, FlatCAMGeometry):
-                    self.new_object("gerber", str(obj_name) + "_conv", initialize_geometry)
-                else:
-                    log.warning("App.convert_any2gerber --> This is no vaild object for conversion.")
-
-            except Exception as e:
-                return "Operation failed: %s" % str(e)
-
-    def on_set_zero_click(self, event):
-        #this function will be available only for mouse left click
-        pos =[]
-        pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
-        if event.button == 1:
-            if self.grid_status() == True:
-                pos = self.geo_editor.snap(pos_canvas[0], pos_canvas[1])
-            else:
-                pos = pos_canvas
-
-            x = 0 - pos[0]
-            y = 0 - pos[1]
-            for obj in self.collection.get_list():
-                obj.offset((x,y))
-                self.object_changed.emit(obj)
-                obj.plot()
-                # Update the object bounding box options
-                a, b, c, d = obj.bounds()
-                obj.options['xmin'] = a
-                obj.options['ymin'] = b
-                obj.options['xmax'] = c
-                obj.options['ymax'] = d
-            # self.plot_all(zoom=False)
-            self.inform.emit(_('[success] Origin set ...'))
-            self.plotcanvas.fit_view()
-            self.plotcanvas.vis_disconnect('mouse_press', self.on_set_zero_click)
-            self.should_we_save = True
-
-    def on_selectall(self):
-        self.report_usage("on_selectall()")
-
-        # delete the possible selection box around a possible selected object
-        self.delete_selection_shape()
-        for name in self.collection.get_names():
-            self.collection.set_active(name)
-            curr_sel_obj = self.collection.get_by_name(name)
-            # create the selection box around the selected object
-            if self.defaults['global_selection_shape'] is True:
-                self.draw_selection_shape(curr_sel_obj)
-
-    def on_preferences(self):
-        # add the tab if it was closed
-        self.ui.plot_tab_area.addTab(self.ui.preferences_tab, _("Preferences"))
-
-        # delete the absolute and relative position and messages in the infobar
-        self.ui.position_label.setText("")
-        self.ui.rel_position_label.setText("")
-
-        # Switch plot_area to preferences page
-        self.ui.plot_tab_area.setCurrentWidget(self.ui.preferences_tab)
-        self.ui.show()
-
-        # this disconnect() is done so the slot will be connected only once
-        try:
-            self.ui.plot_tab_area.tab_closed_signal.disconnect(self.on_preferences_closed)
-        except (TypeError, AttributeError):
-            pass
-        self.ui.plot_tab_area.tab_closed_signal.connect(self.on_preferences_closed)
-
-        # detect changes in the preferences
-        for idx in range(self.ui.pref_tab_area.count()):
-            for tb in self.ui.pref_tab_area.widget(idx).findChildren(QtCore.QObject):
-                try:
-                    try:
-                        tb.textEdited.disconnect(self.on_preferences_edited)
-                    except (TypeError, AttributeError):
-                        pass
-                    tb.textEdited.connect(self.on_preferences_edited)
-                except AttributeError:
-                    pass
-
-                try:
-                    try:
-                        tb.modificationChanged.disconnect(self.on_preferences_edited)
-                    except (TypeError, AttributeError):
-                        pass
-                    tb.modificationChanged.connect(self.on_preferences_edited)
-                except AttributeError:
-                    pass
-
-                try:
-                    try:
-                        tb.toggled.disconnect(self.on_preferences_edited)
-                    except (TypeError, AttributeError):
-                        pass
-                    tb.toggled.connect(self.on_preferences_edited)
-                except AttributeError:
-                    pass
-
-                try:
-                    try:
-                        tb.valueChanged.disconnect(self.on_preferences_edited)
-                    except (TypeError, AttributeError):
-                        pass
-                    tb.valueChanged.connect(self.on_preferences_edited)
-                except AttributeError:
-                    pass
-
-                try:
-                    try:
-                        tb.currentIndexChanged.disconnect(self.on_preferences_edited)
-                    except (TypeError, AttributeError):
-                        pass
-                    tb.currentIndexChanged.connect(self.on_preferences_edited)
-                except AttributeError:
-                    pass
-
-    def on_preferences_edited(self):
-        self.inform.emit(_("[WARNING_NOTCL] Preferences edited but not saved."))
-        self.preferences_changed_flag = True
-
-    def on_preferences_closed(self):
-        # disconnect
-        for idx in range(self.ui.pref_tab_area.count()):
-            for tb in self.ui.pref_tab_area.widget(idx).findChildren(QtCore.QObject):
-                try:
-                    tb.textEdited.disconnect(self.on_preferences_edited)
-                except (TypeError, AttributeError):
-                    pass
-
-                try:
-                    tb.modificationChanged.disconnect(self.on_preferences_edited)
-                except (TypeError, AttributeError):
-                    pass
-
-                try:
-                    tb.toggled.disconnect(self.on_preferences_edited)
-                except (TypeError, AttributeError):
-                    pass
-
-                try:
-                    tb.valueChanged.disconnect(self.on_preferences_edited)
-                except (TypeError, AttributeError):
-                    pass
-
-                try:
-                    tb.currentIndexChanged.disconnect(self.on_preferences_edited)
-                except (TypeError, AttributeError):
-                    pass
-
-        if self.preferences_changed_flag is True:
-            msgbox = QtWidgets.QMessageBox()
-            msgbox.setText(_("One or more values are changed.\n"
-                             "Do you want to save the Preferences?"))
-            msgbox.setWindowTitle(_("Save Preferences"))
-            msgbox.setWindowIcon(QtGui.QIcon('share/save_as.png'))
-
-            bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.YesRole)
-            bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole)
-
-            msgbox.setDefaultButton(bt_yes)
-            msgbox.exec_()
-            response = msgbox.clickedButton()
-
-            if response == bt_yes:
-                self.on_save_button()
-                self.inform.emit(_("[success] Defaults saved."))
-            else:
-                self.preferences_changed_flag = False
-                return
-
-    def on_flipy(self):
-        self.report_usage("on_flipy()")
-
-        obj_list = self.collection.get_selected()
-        xminlist = []
-        yminlist = []
-        xmaxlist = []
-        ymaxlist = []
-
-        if not obj_list:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected to Flip on Y axis."))
-        else:
-            try:
-                # first get a bounding box to fit all
-                for obj in obj_list:
-                    xmin, ymin, xmax, ymax = obj.bounds()
-                    xminlist.append(xmin)
-                    yminlist.append(ymin)
-                    xmaxlist.append(xmax)
-                    ymaxlist.append(ymax)
-
-                # get the minimum x,y and maximum x,y for all objects selected
-                xminimal = min(xminlist)
-                yminimal = min(yminlist)
-                xmaximal = max(xmaxlist)
-                ymaximal = max(ymaxlist)
-
-                px = 0.5 * (xminimal + xmaximal)
-                py = 0.5 * (yminimal + ymaximal)
-
-                # execute mirroring
-                for obj in obj_list:
-                    obj.mirror('X', [px, py])
-                    obj.plot()
-                    self.object_changed.emit(obj)
-                self.inform.emit(_("[success] Flip on Y axis done."))
-            except Exception as e:
-                self.inform.emit(_("[ERROR_NOTCL] Due of %s, Flip action was not executed.") % str(e))
-                return
-
-    def on_flipx(self):
-        self.report_usage("on_flipx()")
-
-        obj_list = self.collection.get_selected()
-        xminlist = []
-        yminlist = []
-        xmaxlist = []
-        ymaxlist = []
-
-        if not obj_list:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected to Flip on X axis."))
-        else:
-            try:
-                # first get a bounding box to fit all
-                for obj in obj_list:
-                    xmin, ymin, xmax, ymax = obj.bounds()
-                    xminlist.append(xmin)
-                    yminlist.append(ymin)
-                    xmaxlist.append(xmax)
-                    ymaxlist.append(ymax)
-
-                # get the minimum x,y and maximum x,y for all objects selected
-                xminimal = min(xminlist)
-                yminimal = min(yminlist)
-                xmaximal = max(xmaxlist)
-                ymaximal = max(ymaxlist)
-
-                px = 0.5 * (xminimal + xmaximal)
-                py = 0.5 * (yminimal + ymaximal)
-
-                # execute mirroring
-                for obj in obj_list:
-                    obj.mirror('Y', [px, py])
-                    obj.plot()
-                    self.object_changed.emit(obj)
-                self.inform.emit(_("[success] Flip on X axis done."))
-            except Exception as e:
-                self.inform.emit(_("[ERROR_NOTCL] Due of %s, Flip action was not executed.") % str(e))
-                return
-
-    def on_rotate(self, silent=False, preset=None):
-        self.report_usage("on_rotate()")
-
-        obj_list = self.collection.get_selected()
-        xminlist = []
-        yminlist = []
-        xmaxlist = []
-        ymaxlist = []
-
-        if not obj_list:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected to Rotate."))
-        else:
-            if silent is False:
-                rotatebox = FCInputDialog(title=_("Transform"), text=_("Enter the Angle value:"),
-                                          min=-360, max=360, decimals=4,
-                                          init_val=float(self.defaults['tools_transform_rotate']))
-                num, ok = rotatebox.get_value()
-            else:
-                num = preset
-                ok = True
-
-            if ok:
-                try:
-                    # first get a bounding box to fit all
-                    for obj in obj_list:
-                        xmin, ymin, xmax, ymax = obj.bounds()
-                        xminlist.append(xmin)
-                        yminlist.append(ymin)
-                        xmaxlist.append(xmax)
-                        ymaxlist.append(ymax)
-
-                    # get the minimum x,y and maximum x,y for all objects selected
-                    xminimal = min(xminlist)
-                    yminimal = min(yminlist)
-                    xmaximal = max(xmaxlist)
-                    ymaximal = max(ymaxlist)
-                    px = 0.5 * (xminimal + xmaximal)
-                    py = 0.5 * (yminimal + ymaximal)
-
-                    for sel_obj in obj_list:
-                        sel_obj.rotate(-float(num), point=(px, py))
-                        sel_obj.plot()
-                        self.object_changed.emit(sel_obj)
-                    self.inform.emit(_("[success] Rotation done."))
-                except Exception as e:
-                    self.inform.emit(_("[ERROR_NOTCL] Due of %s, rotation movement was not executed.") % str(e))
-                    return
-
-    def on_skewx(self):
-        self.report_usage("on_skewx()")
-
-        obj_list = self.collection.get_selected()
-        xminlist = []
-        yminlist = []
-
-        if not obj_list:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected to Skew/Shear on X axis."))
-        else:
-            skewxbox = FCInputDialog(title=_("Transform"), text=_("Enter the Angle value:"),
-                                     min=-360, max=360, decimals=4,
-                                     init_val=float(self.defaults['tools_transform_skew_x']))
-            num, ok = skewxbox.get_value()
-            if ok:
-                # first get a bounding box to fit all
-                for obj in obj_list:
-                    xmin, ymin, xmax, ymax = obj.bounds()
-                    xminlist.append(xmin)
-                    yminlist.append(ymin)
-
-                # get the minimum x,y and maximum x,y for all objects selected
-                xminimal = min(xminlist)
-                yminimal = min(yminlist)
-
-                for obj in obj_list:
-                    obj.skew(num, 0, point=(xminimal, yminimal))
-                    obj.plot()
-                    self.object_changed.emit(obj)
-                self.inform.emit(_("[success] Skew on X axis done."))
-
-    def on_skewy(self):
-        self.report_usage("on_skewy()")
-
-        obj_list = self.collection.get_selected()
-        xminlist = []
-        yminlist = []
-
-        if not obj_list:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected to Skew/Shear on Y axis."))
-        else:
-            skewybox = FCInputDialog(title=_("Transform"), text=_("Enter the Angle value:"),
-                                     min=-360, max=360, decimals=4,
-                                     init_val=float(self.defaults['tools_transform_skew_y']))
-            num, ok = skewybox.get_value()
-            if ok:
-                # first get a bounding box to fit all
-                for obj in obj_list:
-                    xmin, ymin, xmax, ymax = obj.bounds()
-                    xminlist.append(xmin)
-                    yminlist.append(ymin)
-
-                # get the minimum x,y and maximum x,y for all objects selected
-                xminimal = min(xminlist)
-                yminimal = min(yminlist)
-
-                for obj in obj_list:
-                    obj.skew(0, num, point=(xminimal, yminimal))
-                    obj.plot()
-                    self.object_changed.emit(obj)
-                self.inform.emit(_("[success] Skew on Y axis done."))
-
-    def on_plots_updated(self):
-        """
-        Callback used to report when the plots have changed.
-        Adjust axes and zooms to fit.
-
-        :return: None
-        """
-        self.plotcanvas.vispy_canvas.update()           # TODO: Need update canvas?
-        self.on_zoom_fit(None)
-        self.collection.update_view()
-
-    # TODO: Rework toolbar 'clear', 'replot' functions
-    def on_toolbar_replot(self):
-        """
-        Callback for toolbar button. Re-plots all objects.
-
-        :return: None
-        """
-
-        self.report_usage("on_toolbar_replot")
-        self.log.debug("on_toolbar_replot()")
-
-        try:
-            self.collection.get_active().read_form()
-        except AttributeError:
-            self.log.debug("on_toolbar_replot(): AttributeError")
-            pass
-
-        self.plot_all()
-
-    def on_row_activated(self, index):
-        if index.isValid():
-            if index.internalPointer().parent_item != self.collection.root_item:
-                self.ui.notebook.setCurrentWidget(self.ui.selected_tab)
-        self.collection.on_item_activated(index)
-
-    def grid_status(self):
-        if self.ui.grid_snap_btn.isChecked():
-            return 1
-        else:
-            return 0
-
-    def populate_cmenu_grids(self):
-        units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
-
-        self.ui.cmenu_gridmenu.clear()
-        sorted_list = sorted(self.defaults["global_grid_context_menu"][str(units)])
-
-        grid_toggle = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon('share/grid32_menu.png'), _("Grid On/Off"))
-        grid_toggle.setCheckable(True)
-        if self.grid_status():
-            grid_toggle.setChecked(True)
-        else:
-            grid_toggle.setChecked(False)
-
-        self.ui.cmenu_gridmenu.addSeparator()
-        for grid in sorted_list:
-            action = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon('share/grid32_menu.png'), "%s" % str(grid))
-            action.triggered.connect(self.set_grid)
-
-        self.ui.cmenu_gridmenu.addSeparator()
-        grid_add = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon('share/plus32.png'), _("Add"))
-        grid_delete = self.ui.cmenu_gridmenu.addAction(QtGui.QIcon('share/delete32.png'), _("Delete"))
-        grid_add.triggered.connect(self.on_grid_add)
-        grid_delete.triggered.connect(self.on_grid_delete)
-        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())
-
-    def on_grid_add(self):
-        # ## Current application units in lower Case
-        units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
-
-        grid_add_popup = FCInputDialog(title=_("New Grid ..."),
-                                       text=_('Enter a Grid Value:'),
-                                       min=0.0000, max=99.9999, decimals=4)
-        grid_add_popup.setWindowIcon(QtGui.QIcon('share/plus32.png'))
-
-        val, ok = grid_add_popup.get_value()
-        if ok:
-            if float(val) == 0:
-                self.inform.emit(
-                    _("[WARNING_NOTCL] Please enter a grid value with non-zero value, in Float format."))
-                return
-            else:
-                if val not in self.defaults["global_grid_context_menu"][str(units)]:
-                    self.defaults["global_grid_context_menu"][str(units)].append(val)
-                    self.inform.emit(
-                        _("[success] New Grid added ..."))
-                else:
-                    self.inform.emit(
-                        _("[WARNING_NOTCL] Grid already exists ..."))
-        else:
-            self.inform.emit(
-                _("[WARNING_NOTCL] Adding New Grid cancelled ..."))
-
-    def on_grid_delete(self):
-        # ## Current application units in lower Case
-        units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().lower()
-
-        grid_del_popup = FCInputDialog(title="Delete Grid ...",
-                                       text='Enter a Grid Value:',
-                                       min=0.0000, max=99.9999, decimals=4)
-        grid_del_popup.setWindowIcon(QtGui.QIcon('share/delete32.png'))
-
-        val, ok = grid_del_popup.get_value()
-        if ok:
-            if float(val) == 0:
-                self.inform.emit(
-                    _("[WARNING_NOTCL] Please enter a grid value with non-zero value, in Float format."))
-                return
-            else:
-                try:
-                    self.defaults["global_grid_context_menu"][str(units)].remove(val)
-                except ValueError:
-                    self.inform.emit(
-                        _("[ERROR_NOTCL] Grid Value does not exist ..."))
-                    return
-                self.inform.emit(
-                    _("[success] Grid Value deleted ..."))
-        else:
-            self.inform.emit(
-                _("[WARNING_NOTCL] Delete Grid value cancelled ..."))
-
-    def on_shortcut_list(self):
-        self.report_usage("on_shortcut_list()")
-
-        # add the tab if it was closed
-        self.ui.plot_tab_area.addTab(self.ui.shortcuts_tab, _("Key Shortcut List"))
-
-        # delete the absolute and relative position and messages in the infobar
-        self.ui.position_label.setText("")
-        self.ui.rel_position_label.setText("")
-
-        # Switch plot_area to preferences page
-        self.ui.plot_tab_area.setCurrentWidget(self.ui.shortcuts_tab)
-        self.ui.show()
-
-    def on_select_tab(self, name):
-        # if the splitter is hidden, display it, else hide it but only if the current widget is the same
-        if self.ui.splitter.sizes()[0] == 0:
-            self.ui.splitter.setSizes([1, 1])
-        else:
-            if self.ui.notebook.currentWidget().objectName() == name + '_tab':
-                self.ui.splitter.setSizes([0, 1])
-
-        if name == 'project':
-            self.ui.notebook.setCurrentWidget(self.ui.project_tab)
-        elif name == 'selected':
-            self.ui.notebook.setCurrentWidget(self.ui.selected_tab)
-        elif name == 'tool':
-            self.ui.notebook.setCurrentWidget(self.ui.tool_tab)
-
-    def on_copy_name(self):
-        self.report_usage("on_copy_name()")
-
-        obj = self.collection.get_active()
-        try:
-            name = obj.options["name"]
-        except AttributeError:
-            log.debug("on_copy_name() --> No object selected to copy it's name")
-            self.inform.emit(_("[WARNING_NOTCL] No object selected to copy it's name"))
-            return
-
-        self.clipboard.setText(name)
-        self.inform.emit(_("Name copied on clipboard ..."))
-
-    def on_mouse_click_over_plot(self, event):
-        """
-        Default actions are:
-        :param event: Contains information about the event, like which button
-            was clicked, the pixel coordinates and the axes coordinates.
-        :return: None
-        """
-        self.pos = []
-
-        # So it can receive key presses
-        self.plotcanvas.vispy_canvas.native.setFocus()
-        # Set the mouse button for panning
-        self.plotcanvas.vispy_canvas.view.camera.pan_button_setting = self.defaults['global_pan_button']
-
-        self.pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
-        self.pos = (self.pos_canvas[0], self.pos_canvas[1])
-        self.app_cursor.enabled = False
-
-        if self.grid_status():
-            self.pos = self.geo_editor.snap(self.pos_canvas[0], self.pos_canvas[1])
-            self.app_cursor.enabled = True
-
-        try:
-            modifiers = QtWidgets.QApplication.keyboardModifiers()
-
-            if event.button == 1:
-                # Reset here the relative coordinates so there is a new reference on the click position
-                if self.rel_point1 is None:
-                    self.rel_point1 = self.pos
-                else:
-                    self.rel_point2 = copy(self.rel_point1)
-                    self.rel_point1 = self.pos
-
-                # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard
-                if modifiers == QtCore.Qt.ShiftModifier:
-                    # do not auto open the Project Tab
-                    self.click_noproject = True
-
-                    self.clipboard.setText(self.defaults["global_point_clipboard_format"] % (self.pos[0], self.pos[1]))
-                    self.inform.emit(_("[success] Coordinates copied to clipboard."))
-                    return
-
-            self.on_mouse_move_over_plot(event, origin_click=True)
-        except Exception as e:
-            App.log.debug("App.on_mouse_click_over_plot() --> Outside plot? --> %s" % str(e))
-
-    def on_double_click_over_plot(self, event):
-        self.doubleclick = True
-
-    def on_mouse_move_over_plot(self, event, origin_click=None):
-        """
-        Callback for the mouse motion event over the plot.
-
-        :param event: Contains information about the event.
-        :param origin_click
-        :return: None
-        """
-
-        # So it can receive key presses
-        self.plotcanvas.vispy_canvas.native.setFocus()
-        self.pos_jump = event.pos
-
-        self.ui.popMenu.mouse_is_panning = False
-
-        if origin_click != True:
-            # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
-            if event.button == 2 and event.is_dragging == 1:
-                self.ui.popMenu.mouse_is_panning = True
-                return
-
-        if self.rel_point1 is not None:
-            try:  # May fail in case mouse not within axes
-                pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
-                if self.grid_status() == True:
-                    pos = self.geo_editor.snap(pos_canvas[0], pos_canvas[1])
-                    self.app_cursor.enabled = True
-                    # Update cursor
-                    self.app_cursor.set_data(np.asarray([(pos[0], pos[1])]), symbol='++', edge_color='black', size=20)
-                else:
-                    pos = (pos_canvas[0], pos_canvas[1])
-                    self.app_cursor.enabled = False
-
-                self.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
-                                               "<b>Y</b>: %.4f" % (pos[0], pos[1]))
-
-                dx = pos[0] - self.rel_point1[0]
-                dy = pos[1] - self.rel_point1[1]
-                self.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (dx, dy))
-                self.mouse = [pos[0], pos[1]]
-
-                # if the mouse is moved and the LMB is clicked then the action is a selection
-                if event.is_dragging == 1 and event.button == 1:
-                    self.delete_selection_shape()
-                    if dx < 0:
-                        self.draw_moving_selection_shape(self.pos, pos, color=self.defaults['global_alt_sel_line'],
-                                                     face_color=self.defaults['global_alt_sel_fill'])
-                        self.selection_type = False
-                    else:
-                        self.draw_moving_selection_shape(self.pos, pos)
-                        self.selection_type = True
-
-                # hover effect - enabled in Preferences -> General -> GUI Settings
-                if self.defaults['global_hover']:
-                    for obj in self.collection.get_list():
-                        try:
-                            # select the object(s) only if it is enabled (plotted)
-                            if obj.options['plot']:
-                                if obj not in self.collection.get_selected():
-                                    poly_obj = Polygon(
-                                        [(obj.options['xmin'], obj.options['ymin']),
-                                         (obj.options['xmax'], obj.options['ymin']),
-                                         (obj.options['xmax'], obj.options['ymax']),
-                                         (obj.options['xmin'], obj.options['ymax'])]
-                                    )
-                                    if Point(pos).within(poly_obj):
-                                        if obj.isHovering is False:
-                                            obj.isHovering = True
-                                            obj.notHovering = True
-                                            # create the selection box around the selected object
-                                            self.draw_hover_shape(obj, color='#d1e0e0')
-                                    else:
-                                        if obj.notHovering is True:
-                                            obj.notHovering = False
-                                            obj.isHovering = False
-                                            self.delete_hover_shape()
-                        except:
-                            # the Exception here will happen if we try to select on screen and we have an
-                            # newly (and empty) just created Geometry or Excellon object that do not have the
-                            # xmin, xmax, ymin, ymax options.
-                            # In this case poly_obj creation (see above) will fail
-                            pass
-
-            except:
-                self.ui.position_label.setText("")
-                self.ui.rel_position_label.setText("")
-                self.mouse = None
-
-    def on_mouse_click_release_over_plot(self, event):
-        """
-        Callback for the mouse click release over plot. This event is generated by the Matplotlib backend
-        and has been registered in ''self.__init__()''.
-        :param event: contains information about the event.
-        :return:
-        """
-
-        pos_canvas = self.plotcanvas.vispy_canvas.translate_coords(event.pos)
-        if self.grid_status():
-            pos = self.geo_editor.snap(pos_canvas[0], pos_canvas[1])
-        else:
-            pos = (pos_canvas[0], pos_canvas[1])
-
-        # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
-        # canvas menu
-        try:
-            if event.button == 2:  # right click
-                if self.ui.popMenu.mouse_is_panning is False:
-
-                    self.cursor = QtGui.QCursor()
-                    self.populate_cmenu_grids()
-                    self.ui.popMenu.popup(self.cursor.pos())
-
-        except Exception as e:
-            log.warning("Error: %s" % str(e))
-            return
-
-        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
-        # selection and then select a type of selection ("enclosing" or "touching")
-        try:
-            if event.button == 1:  # left click
-                if self.doubleclick is True:
-                    self.doubleclick = False
-                    if self.collection.get_selected():
-                        self.ui.notebook.setCurrentWidget(self.ui.selected_tab)
-                        if self.ui.splitter.sizes()[0] == 0:
-                            self.ui.splitter.setSizes([1, 1])
-
-                        # delete the selection shape(S) as it may be in the way
-                        self.delete_selection_shape()
-                        self.delete_hover_shape()
-
-                else:
-                    if self.selection_type is not None:
-                        self.selection_area_handler(self.pos, pos, self.selection_type)
-                        self.selection_type = None
-                    else:
-                        modifiers = QtWidgets.QApplication.keyboardModifiers()
-
-                        # If the CTRL key is pressed when the LMB is clicked then if the object is selected it will
-                        # deselect, and if it's not selected then it will be selected
-                        if modifiers == QtCore.Qt.ControlModifier:
-                            # If there is no active command (self.command_active is None) then we check if we clicked
-                            # on a object by checking the bounding limits against mouse click position
-                            if self.command_active is None:
-                                self.select_objects(key='CTRL')
-                                self.delete_hover_shape()
-                        elif modifiers == QtCore.Qt.ShiftModifier:
-                            # if SHIFT was pressed and LMB is clicked then we have a coordinates copy to clipboard
-                            # therefore things should stay as they are
-                            pass
-                        else:
-                            # If there is no active command (self.command_active is None) then we check if we clicked
-                            # on a object by checking the bounding limits against mouse click position
-                            if self.command_active is None:
-                                self.select_objects()
-                                self.delete_hover_shape()
-
-        except Exception as e:
-            log.warning("Error: %s" % str(e))
-            return
-
-    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:
-        """
-        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
-
-        self.delete_selection_shape()
-        for obj in self.collection.get_list():
-            try:
-                # select the object(s) only if it is enabled (plotted)
-                if obj.options['plot']:
-                    poly_obj = Polygon([(obj.options['xmin'], obj.options['ymin']),
-                                        (obj.options['xmax'], obj.options['ymin']),
-                                        (obj.options['xmax'], obj.options['ymax']),
-                                        (obj.options['xmin'], obj.options['ymax'])])
-                    if sel_type is True:
-                        if poly_obj.within(poly_selection):
-                            # create the selection box around the selected object
-                            if self.defaults['global_selection_shape'] is True:
-                                self.draw_selection_shape(obj)
-                            self.collection.set_active(obj.options['name'])
-                    else:
-                        if poly_selection.intersects(poly_obj):
-                            # create the selection box around the selected object
-                            if self.defaults['global_selection_shape'] is True:
-                                self.draw_selection_shape(obj)
-                            self.collection.set_active(obj.options['name'])
-            except Exception as e:
-                # the Exception here will happen if we try to select on screen and we have an newly (and empty)
-                # just created Geometry or Excellon object that do not have the xmin, xmax, ymin, ymax options.
-                # In this case poly_obj creation (see above) will fail
-                log.debug("App.selection_area_handler() --> %s" % str(e))
-
-    def select_objects(self, key=None):
-        # list where we store the overlapped objects under our mouse left click position
-        objects_under_the_click_list = []
-        # Populate the list with the overlapped objects on the click position
-        curr_x, curr_y = self.pos
-        for obj in self.all_objects_list:
-            if (curr_x >= obj.options['xmin']) and (curr_x <= obj.options['xmax']) and \
-                    (curr_y >= obj.options['ymin']) and (curr_y <= obj.options['ymax']):
-                if obj.options['name'] not in objects_under_the_click_list:
-                    if obj.options['plot']:
-                        # add objects to the objects_under_the_click list only if the object is plotted
-                        # (active and not disabled)
-                        objects_under_the_click_list.append(obj.options['name'])
-        try:
-            # If there is no element in the overlapped objects list then make everyone inactive
-            # because we selected "nothing"
-            if not objects_under_the_click_list:
-                self.collection.set_all_inactive()
-                # delete the possible selection box around a possible selected object
-                self.delete_selection_shape()
-
-                # and as a convenience move the focus to the Project tab because Selected tab is now empty but
-                # only when working on App
-                if self.call_source == 'app':
-                    if self.click_noproject is False:
-                        self.ui.notebook.setCurrentWidget(self.ui.project_tab)
-                    else:
-                        # restore auto open the Project Tab
-                        self.click_noproject = False
-
-                    # delete any text in the status bar, implicitly the last object name that was selected
-                    self.inform.emit("")
-                else:
-                    self.call_source = 'app'
-
-            else:
-                # case when there is only an object under the click and we toggle it
-                if len(objects_under_the_click_list) == 1:
-                    if self.collection.get_active() is None :
-                        self.collection.set_active(objects_under_the_click_list[0])
-                        # create the selection box around the selected object
-                        curr_sel_obj = self.collection.get_active()
-                        if self.defaults['global_selection_shape'] is True:
-                            self.draw_selection_shape(curr_sel_obj)
-
-                        # self.inform.emit('[selected] %s: %s selected' %
-                        #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
-                        if curr_sel_obj.kind == 'gerber':
-                            self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                                color='green', name=str(curr_sel_obj.options['name'])))
-                        elif curr_sel_obj.kind == 'excellon':
-                            self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                                color='brown', name=str(curr_sel_obj.options['name'])))
-                        elif curr_sel_obj.kind == 'cncjob':
-                            self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                                color='blue', name=str(curr_sel_obj.options['name'])))
-                        elif curr_sel_obj.kind == 'geometry':
-                            self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                                color='red', name=str(curr_sel_obj.options['name'])))
-
-                    elif self.collection.get_active().options['name'] not in objects_under_the_click_list:
-                        self.collection.set_all_inactive()
-                        self.delete_selection_shape()
-                        self.collection.set_active(objects_under_the_click_list[0])
-                        # create the selection box around the selected object
-                        curr_sel_obj = self.collection.get_active()
-                        if self.defaults['global_selection_shape'] is True:
-                            self.draw_selection_shape(curr_sel_obj)
-
-                        # self.inform.emit('[selected] %s: %s selected' %
-                        #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
-                        if curr_sel_obj.kind == 'gerber':
-                            self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                                color='green', name=str(curr_sel_obj.options['name'])))
-                        elif curr_sel_obj.kind == 'excellon':
-                            self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                                color='brown', name=str(curr_sel_obj.options['name'])))
-                        elif curr_sel_obj.kind == 'cncjob':
-                            self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                                color='blue', name=str(curr_sel_obj.options['name'])))
-                        elif curr_sel_obj.kind == 'geometry':
-                            self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                                color='red', name=str(curr_sel_obj.options['name'])))
-
-                    else:
-                        self.collection.set_all_inactive()
-                        self.delete_selection_shape()
-                        if self.call_source == 'app':
-                            # delete any text in the status bar, implicitly the last object name that was selected
-                            self.inform.emit("")
-                        else:
-                            self.call_source = 'app'
-                else:
-                    # If there is no selected object
-                    # make active the first element of the overlapped objects list
-                    if self.collection.get_active() is None:
-                        self.collection.set_active(objects_under_the_click_list[0])
-
-                    name_sel_obj = self.collection.get_active().options['name']
-                    # In case that there is a selected object but it is not in the overlapped object list
-                    # make that object inactive and activate the first element in the overlapped object list
-                    if name_sel_obj not in objects_under_the_click_list:
-                        self.collection.set_inactive(name_sel_obj)
-                        name_sel_obj = objects_under_the_click_list[0]
-                        self.collection.set_active(name_sel_obj)
-                    else:
-                        name_sel_obj_idx = objects_under_the_click_list.index(name_sel_obj)
-                        self.collection.set_all_inactive()
-                        self.collection.set_active(objects_under_the_click_list[(name_sel_obj_idx + 1) %
-                                                                                len(objects_under_the_click_list)])
-
-                    curr_sel_obj = self.collection.get_active()
-                    # delete the possible selection box around a possible selected object
-                    self.delete_selection_shape()
-                    # create the selection box around the selected object
-                    if self.defaults['global_selection_shape'] is True:
-                        self.draw_selection_shape(curr_sel_obj)
-
-                    # self.inform.emit('[selected] %s: %s selected' %
-                    #                  (str(curr_sel_obj.kind).capitalize(), str(curr_sel_obj.options['name'])))
-                    if curr_sel_obj.kind == 'gerber':
-                        self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                            color='green', name=str(curr_sel_obj.options['name'])))
-                    elif curr_sel_obj.kind == 'excellon':
-                        self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                            color='brown', name=str(curr_sel_obj.options['name'])))
-                    elif curr_sel_obj.kind == 'cncjob':
-                        self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                            color='blue', name=str(curr_sel_obj.options['name'])))
-                    elif curr_sel_obj.kind == 'geometry':
-                        self.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                            color='red', name=str(curr_sel_obj.options['name'])))
-
-                    # for obj in self.collection.get_list():
-                    #     obj.plot()
-                    # curr_sel_obj.plot(color=self.FC_dark_blue, face_color=self.FC_light_blue)
-
-                    # TODO: on selected objects change the object colors and do not draw the selection box
-                    # self.plotcanvas.vispy_canvas.update() # this updates the canvas
-        except Exception as e:
-            log.error("[ERROR] Something went bad. %s" % str(e))
-            return
-
-    def delete_hover_shape(self):
-        self.hover_shapes.clear()
-        self.hover_shapes.redraw()
-
-    def draw_hover_shape(self, sel_obj, color=None):
-        """
-
-        :param sel_obj: the object for which the hover shape must be drawn
-        :return:
-        """
-
-        pt1 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymin']))
-        pt2 = (float(sel_obj.options['xmax']), float(sel_obj.options['ymin']))
-        pt3 = (float(sel_obj.options['xmax']), float(sel_obj.options['ymax']))
-        pt4 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymax']))
-
-        hover_rect = Polygon([pt1, pt2, pt3, pt4])
-        if self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
-            hover_rect = hover_rect.buffer(-0.1)
-            hover_rect = hover_rect.buffer(0.2)
-
-        else:
-            hover_rect = hover_rect.buffer(-0.00393)
-            hover_rect = hover_rect.buffer(0.00787)
-
-        if color:
-            face = Color(color)
-            face.alpha = 0.2
-            outline = Color(color, alpha=0.8)
-        else:
-            face = Color(self.defaults['global_sel_fill'])
-            face.alpha = 0.2
-            outline = self.defaults['global_sel_line']
-
-        self.hover_shapes.add(hover_rect, color=outline, face_color=face, update=True, layer=0, tolerance=None)
-
-    def delete_selection_shape(self):
-        self.move_tool.sel_shapes.clear()
-        self.move_tool.sel_shapes.redraw()
-
-    def draw_selection_shape(self, sel_obj, color=None):
-        """
-
-        :param sel_obj: the object for which the selection shape must be drawn
-        :return:
-        """
-
-        pt1 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymin']))
-        pt2 = (float(sel_obj.options['xmax']), float(sel_obj.options['ymin']))
-        pt3 = (float(sel_obj.options['xmax']), float(sel_obj.options['ymax']))
-        pt4 = (float(sel_obj.options['xmin']), float(sel_obj.options['ymax']))
-
-        sel_rect = Polygon([pt1, pt2, pt3, pt4])
-        if self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper() == 'MM':
-            sel_rect = sel_rect.buffer(-0.1)
-            sel_rect = sel_rect.buffer(0.2)
-        else:
-            sel_rect = sel_rect.buffer(-0.00393)
-            sel_rect = sel_rect.buffer(0.00787)
-
-        if color:
-            face = Color(color, alpha=0.2)
-            outline = Color(color, alpha=0.8)
-        else:
-            face = Color(self.defaults['global_sel_fill'], alpha=0.2)
-            outline = Color(self.defaults['global_sel_line'], alpha=0.8)
-
-        self.sel_objects_list.append(self.move_tool.sel_shapes.add(sel_rect, color=outline,
-                                                               face_color=face, update=True, layer=0, tolerance=None))
-
-    def draw_moving_selection_shape(self, old_coords, coords, **kwargs):
-        """
-
-        :param old_coords: old coordinates
-        :param coords: new coordinates
-        :return:
-        """
-
-        if 'color' in kwargs:
-            color = kwargs['color']
-        else:
-            color = self.defaults['global_sel_line']
-
-        if 'face_color' in kwargs:
-            face_color = kwargs['face_color']
-        else:
-            face_color = self.defaults['global_sel_fill']
-
-        if 'face_alpha' in kwargs:
-            face_alpha = kwargs['face_alpha']
-        else:
-            face_alpha = 0.3
-
-        x0, y0 = old_coords
-        x1, y1 = coords
-        pt1 = (x0, y0)
-        pt2 = (x1, y0)
-        pt3 = (x1, y1)
-        pt4 = (x0, y1)
-        sel_rect = Polygon([pt1, pt2, pt3, pt4])
-
-        color_t = Color(face_color)
-        color_t.alpha = face_alpha
-        self.move_tool.sel_shapes.add(sel_rect, color=color, face_color=color_t, update=True,
-                                      layer=0, tolerance=None)
-
-    def on_file_new_click(self):
-        if self.collection.get_list() and self.should_we_save:
-            msgbox = QtWidgets.QMessageBox()
-            # msgbox.setText("<B>Save changes ...</B>")
-            msgbox.setText(_("There are files/objects opened in FlatCAM.\n"
-                           "Creating a New project will delete them.\n"
-                           "Do you want to Save the project?"))
-            msgbox.setWindowTitle(_("Save changes"))
-            msgbox.setWindowIcon(QtGui.QIcon('share/save_as.png'))
-            bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.YesRole)
-            bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole)
-            bt_cancel = msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.RejectRole)
-
-            msgbox.setDefaultButton(bt_yes)
-            msgbox.exec_()
-            response = msgbox.clickedButton()
-
-            if response == bt_yes:
-                self.on_file_saveprojectas()
-            elif response == bt_cancel:
-                return
-            elif response == bt_no:
-                self.on_file_new()
-        else:
-            self.on_file_new()
-        self.inform.emit(_("[success] New Project created..."))
-
-    def on_file_new(self):
-        """
-        Callback for menu item File->New. Returns the application to its
-        startup state. This method is thread-safe.
-
-        :return: None
-        """
-
-        self.report_usage("on_file_new")
-
-        # Remove everything from memory
-        App.log.debug("on_file_new()")
-
-        if self.call_source != 'app':
-            self.editor2object(cleanup=True)
-            # ## EDITOR section
-            self.geo_editor = FlatCAMGeoEditor(self, disabled=True)
-            self.exc_editor = FlatCAMExcEditor(self)
-            self.grb_editor = FlatCAMGrbEditor(self)
-
-        # Clear pool
-        self.clear_pool()
-
-        for obj in self.collection.get_list():
-            # delete shapes left drawn from mark shape_collections, if any
-            if isinstance(obj, FlatCAMGerber):
-                try:
-                    obj.mark_shapes.enabled = False
-                    obj.mark_shapes.clear(update=True)
-                except AttributeError:
-                    pass
-
-            # also delete annotation shapes, if any
-            elif isinstance(obj, FlatCAMCNCjob):
-                try:
-                    obj.annotation.enabled = False
-                    obj.annotation.clear(update=True)
-                except AttributeError:
-                    pass
-
-        # tcl needs to be reinitialized, otherwise  old shell variables etc  remains
-        self.init_tcl()
-
-        self.delete_selection_shape()
-        self.collection.delete_all()
-
-        self.setup_component_editor()
-
-        # Clear project filename
-        self.project_filename = None
-
-        # Load the application defaults
-        self.load_defaults(filename='current_defaults')
-
-        # Re-fresh project options
-        self.on_options_app2project()
-
-        # Init Tools
-        self.init_tools()
-
-        # Close any Tabs opened in the Plot Tab Area section
-        for index in range(self.ui.plot_tab_area.count()):
-            self.ui.plot_tab_area.closeTab(index)
-            # for whatever reason previous command does not close the last tab so I do it manually
-        self.ui.plot_tab_area.closeTab(0)
-
-        # # And then add again the Plot Area
-        self.ui.plot_tab_area.addTab(self.ui.plot_tab, "Plot Area")
-        self.ui.plot_tab_area.protectTab(0)
-
-        # take the focus of the Notebook on Project Tab.
-        self.ui.notebook.setCurrentWidget(self.ui.project_tab)
-
-        self.set_ui_title(name="New Project")
-
-
-    def obj_properties(self):
-        self.report_usage("obj_properties()")
-
-        self.properties_tool.run(toggle=False)
-
-    def on_project_context_save(self):
-        obj = self.collection.get_active()
-        if type(obj) == FlatCAMGeometry:
-            self.on_file_exportdxf()
-        elif type(obj) == FlatCAMExcellon:
-            self.on_file_saveexcellon()
-        elif type(obj) == FlatCAMCNCjob:
-            obj.on_exportgcode_button_click()
-        elif type(obj) == FlatCAMGerber:
-            self.on_file_savegerber()
-
-    def obj_move(self):
-        self.report_usage("obj_move()")
-        self.move_tool.run(toggle=False)
-
-    def on_fileopengerber(self):
-        """
-        File menu callback for opening a Gerber.
-
-        :return: None
-        """
-
-        self.report_usage("on_fileopengerber")
-        App.log.debug("on_fileopengerber()")
-
-        _filter_ = "Gerber Files (*.gbr *.ger *.gtl *.gbl *.gts *.gbs *.gtp *.gbp *.gto *.gbo *.gm1 *.gml *.gm3 *" \
-                   ".gko *.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim *.mil *.grb" \
-                   "*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb *.pho *.gdo *.art *.gbd *.gb*);;" \
-                   "Protel Files (*.gtl *.gbl *.gts *.gbs *.gto *.gbo *.gtp *.gbp *.gml *.gm1 *.gm3 *.gko);;" \
-                   "Eagle Files (*.cmp *.sol *.stc *.sts *.plc *.pls *.crc *.crs *.tsm *.bsm *.ly2 *.ly15 *.dim " \
-                   "*.mil);;" \
-                   "OrCAD Files (*.top *.bot *.smt *.smb *.sst *.ssb *.spt *.spb);;" \
-                   "Allegro Files (*.art);;" \
-                   "Mentor Files (*.pho *.gdo);;" \
-                   "All Files (*.*)"
-
-        try:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"),
-                                                         directory=self.get_last_folder(), filter=_filter_)
-        except TypeError:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"), filter=_filter_)
-
-        filenames = [str(filename) for filename in filenames]
-
-        if len(filenames) == 0:
-            self.inform.emit(_("[WARNING_NOTCL] Open Gerber cancelled."))
-        else:
-            for filename in filenames:
-                if filename != '':
-                    self.worker_task.emit({'fcn': self.open_gerber,
-                                           'params': [filename]})
-
-    def on_fileopenexcellon(self):
-        """
-        File menu callback for opening an Excellon file.
-
-        :return: None
-        """
-
-        self.report_usage("on_fileopenexcellon")
-        App.log.debug("on_fileopenexcellon()")
-
-        _filter_ = "Excellon Files (*.drl *.txt *.xln *.drd *.tap *.exc *.ncd);;" \
-                   "All Files (*.*)"
-
-        try:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"),
-                                                         directory=self.get_last_folder(), filter=_filter_)
-        except TypeError:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"), filter=_filter_)
-
-        filenames = [str(filename) for filename in filenames]
-
-        if len(filenames) == 0:
-            self.inform.emit(_("[WARNING_NOTCL] Open Excellon cancelled."))
-        else:
-            for filename in filenames:
-                if filename != '':
-                    self.worker_task.emit({'fcn': self.open_excellon,
-                                           'params': [filename]})
-
-    def on_fileopengcode(self):
-        """
-        File menu call back for opening gcode.
-
-        :return: None
-        """
-
-        self.report_usage("on_fileopengcode")
-        App.log.debug("on_fileopengcode()")
-
-        # https://bobcadsupport.com/helpdesk/index.php?/Knowledgebase/Article/View/13/5/known-g-code-file-extensions
-        _filter_ = "G-Code Files (*.txt *.nc *.ncc *.tap *.gcode *.cnc *.ecs *.fnc *.dnc *.ncg *.gc *.fan *.fgc" \
-                   " *.din *.xpi *.hnc *.h *.i *.ncp *.min *.gcd *.rol *.mpr *.ply *.out *.eia *.plt *.sbp *.mpf);;" \
-                   "All Files (*.*)"
-        try:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"),
-                                                         directory=self.get_last_folder(), filter=_filter_)
-        except TypeError:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"), filter=_filter_)
-
-        filenames = [str(filename) for filename in filenames]
-
-        if len(filenames) == 0:
-            self.inform.emit(_("[WARNING_NOTCL] Open G-Code cancelled."))
-        else:
-            for filename in filenames:
-                if filename != '':
-                    self.worker_task.emit({'fcn': self.open_gcode,
-                                           'params': [filename]})
-
-    def on_file_openproject(self):
-        """
-        File menu callback for opening a project.
-
-        :return: None
-        """
-
-        self.report_usage("on_file_openproject")
-        App.log.debug("on_file_openproject()")
-        _filter_ = "FlatCAM Project (*.FlatPrj);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"),
-                                                         directory=self.get_last_folder(), filter=_filter_)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Project"), filter = _filter_)
-
-        # 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 == "":
-            self.inform.emit(_("[WARNING_NOTCL] Open Project cancelled."))
-        else:
-            # self.worker_task.emit({'fcn': self.open_project,
-            #                        'params': [filename]})
-            # The above was failing because open_project() is not
-            # thread safe. The new_project()
-            self.open_project(filename)
-
-    def on_file_openconfig(self):
-        """
-        File menu callback for opening a config file.
-
-        :return: None
-        """
-
-        self.report_usage("on_file_openconfig")
-        App.log.debug("on_file_openconfig()")
-        _filter_ = "FlatCAM Config (*.FlatConfig);;FlatCAM Config (*.json);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
-                                                         directory=self.data_path, filter=_filter_)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open Configuration File"),
-                                                                 filter = _filter_)
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] Open Config cancelled."))
-        else:
-            self.open_config_file(filename)
-
-    def on_file_exportsvg(self):
-        """
-        Callback for menu item File->Export SVG.
-
-        :return: None
-        """
-        self.report_usage("on_file_exportsvg")
-        App.log.debug("on_file_exportsvg()")
-
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected."))
-            msg = _("Please Select a Geometry object to export")
-            msgbox = QtWidgets.QMessageBox()
-            msgbox.setInformativeText(msg)
-            bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole)
-            msgbox.setDefaultButton(bt_ok)
-            msgbox.exec_()
-            return
-
-        # Check for more compatible types and add as required
-        if (not isinstance(obj, FlatCAMGeometry)
-                and not isinstance(obj, FlatCAMGerber)
-                and not isinstance(obj, FlatCAMCNCjob)
-                and not isinstance(obj, FlatCAMExcellon)):
-            msg = _("[ERROR_NOTCL] Only Geometry, Gerber and CNCJob objects can be used.")
-            msgbox = QtWidgets.QMessageBox()
-            msgbox.setInformativeText(msg)
-            bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole)
-            msgbox.setDefaultButton(bt_ok)
-            msgbox.exec_()
-            return
-
-        name = obj.options["name"]
-
-        filter = "SVG File (*.svg);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export SVG"),
-                directory=self.get_last_save_folder() + '/' + str(name),
-                filter=filter)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export SVG"), filter=filter)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] Export SVG cancelled."))
-            return
-        else:
-            self.export_svg(name, filename)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("SVG", filename)
-            self.file_saved.emit("SVG", filename)
-
-    def on_file_exportpng(self):
-        self.report_usage("on_file_exportpng")
-        App.log.debug("on_file_exportpng()")
-
-        self.date = str(datetime.today()).rpartition('.')[0]
-        self.date = ''.join(c for c in self.date if c not in ':-')
-        self.date = self.date.replace(' ', '_')
-
-        image = _screenshot()
-        data = np.asarray(image)
-        if not data.ndim == 3 and data.shape[-1] in (3, 4):
-            self.inform.emit(_('[[WARNING_NOTCL]] Data must be a 3D array with last dimension 3 or 4'))
-            return
-
-        filter_ = "PNG File (*.png);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export PNG Image"),
-                directory=self.get_last_save_folder() + '/png_' + self.date,
-                filter=filter_)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export PNG Image"), filter=filter_)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.inform.emit(_("Export PNG cancelled."))
-            return
-        else:
-            write_png(filename, data)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("png", filename)
-            self.file_saved.emit("png", filename)
-
-    def on_file_savegerber(self):
-        """
-        Callback for menu item File->Export Gerber.
-
-        :return: None
-        """
-        self.report_usage("on_file_savegerber")
-        App.log.debug("on_file_savegerber()")
-
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected. Please select an Gerber object to export."))
-            return
-
-        # Check for more compatible types and add as required
-        if not isinstance(obj, FlatCAMGerber):
-            self.inform.emit(_("[ERROR_NOTCL] Failed. Only Gerber objects can be saved as Gerber files..."))
-            return
-
-        name = self.collection.get_active().options["name"]
-
-        filter = "Gerber File (*.GBR);;Gerber File (*.GRB);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption="Save Gerber source file",
-                directory=self.get_last_save_folder() + '/' + name,
-                filter=filter)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Save Gerber source file"), filter=filter)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] Save Gerber source file cancelled."))
-            return
-        else:
-            self.save_source_file(name, filename)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("Gerber", filename)
-            self.file_saved.emit("Gerber", filename)
-
-    def on_file_saveexcellon(self):
-        """
-        Callback for menu item File->Export Gerber.
-
-        :return: None
-        """
-        self.report_usage("on_file_saveexcellon")
-        App.log.debug("on_file_saveexcellon()")
-
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected. Please select an Excellon object to export."))
-            return
-
-        # Check for more compatible types and add as required
-        if not isinstance(obj, FlatCAMExcellon):
-            self.inform.emit(_("[ERROR_NOTCL] Failed. Only Excellon objects can be saved as Excellon files..."))
-            return
-
-        name = self.collection.get_active().options["name"]
-
-        filter = "Excellon File (*.DRL);;Excellon File (*.TXT);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Save Excellon source file"),
-                directory=self.get_last_save_folder() + '/' + name,
-                filter=filter)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Save Excellon source file"), filter=filter)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] Saving Excellon source file cancelled."))
-            return
-        else:
-            self.save_source_file(name, filename)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("Excellon", filename)
-            self.file_saved.emit("Excellon", filename)
-
-    def on_file_exportexcellon(self):
-        """
-        Callback for menu item File->Export->Excellon.
-
-        :return: None
-        """
-        self.report_usage("on_file_exportexcellon")
-        App.log.debug("on_file_exportexcellon()")
-
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected. Please Select an Excellon object to export."))
-            return
-
-        # Check for more compatible types and add as required
-        if not isinstance(obj, FlatCAMExcellon):
-            self.inform.emit(_("[ERROR_NOTCL] Failed. Only Excellon objects can be saved as Excellon files..."))
-            return
-
-        name = self.collection.get_active().options["name"]
-
-        filter = "Excellon File (*.DRL);;Excellon File (*.TXT);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export Excellon"),
-                directory=self.get_last_save_folder() + '/' + name,
-                filter=filter)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Excellon"), filter=filter)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] Export Excellon cancelled."))
-            return
-        else:
-            self.export_excellon(name, filename)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("Excellon", filename)
-            self.file_saved.emit("Excellon", filename)
-
-    def on_file_exportgerber(self):
-        """
-        Callback for menu item File->Export->Gerber.
-
-        :return: None
-        """
-        self.report_usage("on_file_exportgerber")
-        App.log.debug("on_file_exportgerber()")
-
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected. Please Select an Gerber object to export."))
-            return
-
-        # Check for more compatible types and add as required
-        if not isinstance(obj, FlatCAMGerber):
-            self.inform.emit(_("[ERROR_NOTCL] Failed. Only Gerber objects can be saved as Gerber files..."))
-            return
-
-        name = self.collection.get_active().options["name"]
-
-        _filter_ = "Gerber File (*.GBR);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export Gerber"),
-                directory=self.get_last_save_folder() + '/' + name,
-                filter=_filter_)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Gerber"), filter=_filter_)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] Export Gerber cancelled."))
-            return
-        else:
-            self.export_gerber(name, filename)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("Gerber", filename)
-            self.file_saved.emit("Gerber", filename)
-
-    def on_file_exportdxf(self):
-        """
-                Callback for menu item File->Export DXF.
-
-                :return: None
-                """
-        self.report_usage("on_file_exportdxf")
-        App.log.debug("on_file_exportdxf()")
-
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected."))
-            msg = _("Please Select a Geometry object to export")
-            msgbox = QtWidgets.QMessageBox()
-            msgbox.setInformativeText(msg)
-            bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole)
-            msgbox.setDefaultButton(bt_ok)
-            msgbox.exec_()
-            return
-
-        # Check for more compatible types and add as required
-        if not isinstance(obj, FlatCAMGeometry):
-            msg = _("[ERROR_NOTCL] Only Geometry objects can be used.")
-            msgbox = QtWidgets.QMessageBox()
-            msgbox.setInformativeText(msg)
-            bt_ok = msgbox.addButton(_('Ok'), QtWidgets.QMessageBox.AcceptRole)
-            msgbox.setDefaultButton(bt_ok)
-            msgbox.exec_()
-
-            return
-
-        name = self.collection.get_active().options["name"]
-
-        _filter_ = "DXF File (*.DXF);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export DXF"),
-                directory=self.get_last_save_folder() + '/' + name,
-                filter=_filter_)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export DXF"),
-                                                                 filter=_filter_)
-
-        filename = str(filename)
-
-        if filename == "":
-            self.inform.emit(_("[WARNING_NOTCL] Export DXF cancelled."))
-            return
-        else:
-            self.export_dxf(name, filename)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("DXF", filename)
-            self.file_saved.emit("DXF", filename)
-
-    def on_file_importsvg(self, type_of_obj):
-        """
-        Callback for menu item File->Import SVG.
-        :param type_of_obj: to import the SVG as Geometry or as Gerber
-        :type type_of_obj: str
-        :return: None
-        """
-        self.report_usage("on_file_importsvg")
-        App.log.debug("on_file_importsvg()")
-
-        _filter_ = "SVG File (*.svg);;All Files (*.*)"
-        try:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import SVG"),
-                                                                   directory=self.get_last_folder(), filter=_filter_)
-        except TypeError:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import SVG"),
-                                                                   filter=_filter_)
-
-        if type_of_obj is not "geometry" and type_of_obj is not "gerber":
-            type_of_obj = "geometry"
-
-        filenames = [str(filename) for filename in filenames]
-
-        if len(filenames) == 0:
-            self.inform.emit(_("[WARNING_NOTCL] Open SVG cancelled."))
-        else:
-            for filename in filenames:
-                if filename != '':
-                    self.worker_task.emit({'fcn': self.import_svg,
-                                           'params': [filename, type_of_obj]})
-
-    def on_file_importdxf(self, type_of_obj):
-        """
-        Callback for menu item File->Import DXF.
-        :param type_of_obj: to import the DXF as Geometry or as Gerber
-        :type type_of_obj: str
-        :return: None
-        """
-        self.report_usage("on_file_importdxf")
-        App.log.debug("on_file_importdxf()")
-
-        _filter_ = "DXF File (*.DXF);;All Files (*.*)"
-        try:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import DXF"),
-                                                                   directory=self.get_last_folder(),
-                                                                   filter=_filter_)
-        except TypeError:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Import DXF"),
-                                                                   filter=_filter_)
-
-        if type_of_obj is not "geometry" and type_of_obj is not "gerber":
-            type_of_obj = "geometry"
-
-        filenames = [str(filename) for filename in filenames]
-
-        if len(filenames) == 0:
-            self.inform.emit(_("[WARNING_NOTCL] Open DXF cancelled."))
-        else:
-            for filename in filenames:
-                if filename != '':
-                    self.worker_task.emit({'fcn': self.import_dxf,
-                                           'params': [filename, type_of_obj]})
-
-    # ################################################################################################################ ##
-    # # ## The following section has the functions that are displayed and call the Editor tab CNCJob Tab ############### ##
-    # ################################################################################################################ ##
-
-    def init_code_editor(self, name):
-        # Signals section
-        # Disconnect the old signals
-        self.ui.buttonOpen.clicked.disconnect()
-        self.ui.buttonSave.clicked.disconnect()
-
-        # add the tab if it was closed
-        self.ui.plot_tab_area.addTab(self.ui.cncjob_tab, _('%s') % name)
-        self.ui.cncjob_tab.setObjectName('cncjob_tab')
-
-        # delete the absolute and relative position and messages in the infobar
-        self.ui.position_label.setText("")
-        self.ui.rel_position_label.setText("")
-
-        # first clear previous text in text editor (if any)
-        self.ui.code_editor.clear()
-        self.ui.code_editor.setReadOnly(False)
-        self.toggle_codeeditor = True
-        self.ui.code_editor.completer_enable = False
-
-        # Switch plot_area to CNCJob tab
-        self.ui.plot_tab_area.setCurrentWidget(self.ui.cncjob_tab)
-
-    def on_view_source(self):
-        try:
-            obj = self.collection.get_active()
-        except:
-            self.inform.emit(_("[WARNING_NOTCL] Select an Gerber or Excellon file to view it's source file."))
-            return 'fail'
-
-        # then append the text from GCode to the text editor
-        try:
-            file = StringIO(obj.source_file)
-        except AttributeError:
-            self.inform.emit(_("[WARNING_NOTCL] There is no selected object for which to see it's source file code."))
-            return 'fail'
-
-        if obj.kind == 'gerber':
-            flt = "Gerber Files (*.GBR);;All Files (*.*)"
-        elif obj.kind == 'excellon':
-            flt = "Excellon Files (*.DRL);;All Files (*.*)"
-
-        self.init_code_editor(name=_("Source Editor"))
-        self.ui.buttonOpen.clicked.connect(lambda: self.handleOpen(filt=flt))
-        self.ui.buttonSave.clicked.connect(lambda: self.handleSaveGCode(filt=flt))
-
-        try:
-            for line in file:
-                proc_line = str(line).strip('\n')
-                self.ui.code_editor.append(proc_line)
-        except Exception as e:
-            log.debug('App.on_view_source() -->%s' % str(e))
-            self.inform.emit(_('[ERROR]App.on_view_source() -->%s') % str(e))
-            return
-
-        self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
-
-        self.handleTextChanged()
-        self.ui.show()
-
-    def on_toggle_code_editor(self):
-        self.report_usage("on_toggle_code_editor()")
-
-        if self.toggle_codeeditor is False:
-            self.init_code_editor(name=_("Code Editor"))
-            self.ui.buttonOpen.clicked.connect(lambda: self.handleOpen())
-            self.ui.buttonSave.clicked.connect(lambda: self.handleSaveGCode())
-        else:
-            for idx in range(self.ui.plot_tab_area.count()):
-                if self.ui.plot_tab_area.widget(idx).objectName() == "cncjob_tab":
-                    self.ui.plot_tab_area.closeTab(idx)
-                    break
-            self.toggle_codeeditor = False
-
-    def on_filenewscript(self):
-        flt = "FlatCAM Scripts (*.FlatScript);;All Files (*.*)"
-        self.init_code_editor(name=_("Script Editor"))
-        self.ui.code_editor.completer_enable = True
-        self.ui.code_editor.append(_(
-            "#\n"
-            "# CREATE A NEW FLATCAM TCL SCRIPT\n"
-            "# TCL Tutorial here: https://www.tcl.tk/man/tcl8.5/tutorial/tcltutorial.html\n"
-            "#\n\n"
-            "# FlatCAM commands list:\n"
-            "# AddCircle, AddPolygon, AddPolyline, AddRectangle, AlignDrill, AlignDrillGrid, ClearShell, Cncjob,\n"
-            "# Cutout, Delete, Drillcncjob, ExportGcode, ExportSVG, Exteriors, GeoCutout, GeoUnion, GetNames, GetSys,\n"
-            "# ImportSvg, Interiors, Isolate, Follow, JoinExcellon, JoinGeometry, ListSys, MillHoles, Mirror, New,\n"
-            "# NewGeometry, Offset, OpenExcellon, OpenGCode, OpenGerber, OpenProject, Options, Paint, Panelize,\n"
-            "# Plot, SaveProject, SaveSys, Scale, SetActive, SetSys, Skew, SubtractPoly,SubtractRectangle, Version,\n"
-            "# WriteGCode\n"
-            "#\n\n"
-        ))
-
-        self.ui.buttonOpen.clicked.connect(lambda: self.handleOpen(filt=flt))
-        self.ui.buttonSave.clicked.connect(lambda: self.handleSaveGCode(filt=flt))
-
-        self.handleTextChanged()
-        self.ui.code_editor.show()
-
-    def on_fileopenscript(self):
-        _filter_ = "TCL script (*.FlatScript);;TCL script (*.TCL);;TCL script (*.TXT);;All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open TCL script"),
-                                                                 directory=self.get_last_folder(), filter=_filter_)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Open TCL script"), filter=_filter_)
-
-        # 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 == "":
-            self.inform.emit(_("[WARNING_NOTCL] Open TCL script cancelled."))
-        else:
-            self.on_filenewscript()
-
-            try:
-                with open(filename, "r") as opened_script:
-                    try:
-                        for line in opened_script:
-                            proc_line = str(line).strip('\n')
-                            self.ui.code_editor.append(proc_line)
-                    except Exception as e:
-                        log.debug('App.on_fileopenscript() -->%s' % str(e))
-                        self.inform.emit(_('[ERROR]App.on_fileopenscript() -->%s') % str(e))
-                        return
-
-                    self.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
-
-                    self.handleTextChanged()
-                    self.ui.show()
-
-            except Exception as e:
-                log.debug("App.on_fileopenscript() -> %s" % str(e))
-
-    def on_filerunscript(self, name=None):
-        """
-                File menu callback for loading and running a TCL script.
-
-                :return: None
-                """
-
-        self.report_usage("on_filerunscript")
-        App.log.debug("on_file_runscript()")
-
-        if name:
-            filename = name
-        else:
-            _filter_ = "TCL script (*.FlatScript);;TCL script (*.TCL);;TCL script (*.TXT);;All Files (*.*)"
-            try:
-                filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"),
-                                                             directory=self.get_last_folder(), filter=_filter_)
-            except TypeError:
-                filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Run TCL script"), filter=_filter_)
-
-        # 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 == "":
-            self.inform.emit(_("[WARNING_NOTCL] Run TCL script cancelled."))
-        else:
-            try:
-                with open(filename, "r") as tcl_script:
-                    cmd_line_shellfile_content = tcl_script.read()
-                    self.shell._sysShell.exec_command(cmd_line_shellfile_content)
-            except Exception as e:
-                log.debug("App.on_filerunscript() -> %s" % str(e))
-                sys.exit(2)
-
-    def on_file_saveproject(self):
-        """
-        Callback for menu item File->Save Project. Saves the project to
-        ``self.project_filename`` or calls ``self.on_file_saveprojectas()``
-        if set to None. The project is saved by calling ``self.save_project()``.
-
-        :return: None
-        """
-
-        self.report_usage("on_file_saveproject")
-
-        if self.project_filename is None:
-            self.on_file_saveprojectas()
-        else:
-            self.worker_task.emit({'fcn': self.save_project,
-                                   'params': [self.project_filename]})
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("project", self.project_filename)
-            self.file_saved.emit("project", self.project_filename)
-
-        self.set_ui_title(name=self.project_filename)
-
-        self.should_we_save = False
-
-    def on_file_saveprojectas(self, make_copy=False, thread=True, quit=False):
-        """
-        Callback for menu item File->Save Project As... Opens a file
-        chooser and saves the project to the given file via
-        ``self.save_project()``.
-
-        :return: None
-        """
-
-        self.report_usage("on_file_saveprojectas")
-
-        self.date = str(datetime.today()).rpartition('.')[0]
-        self.date = ''.join(c for c in self.date if c not in ':-')
-        self.date = self.date.replace(' ', '_')
-
-        filter_ = "FlatCAM Project (*.FlatPrj);; All Files (*.*)"
-        try:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Save Project As ..."),
-                directory=_('{l_save}/Project_{date}').format(l_save=str(self.get_last_save_folder()), date=self.date),
-                filter=filter_)
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Save Project As ..."), filter=filter_)
-
-        filename = str(filename)
-
-        if filename == '':
-            self.inform.emit(_("[WARNING_NOTCL] Save Project cancelled."))
-            return
-
-        try:
-            f = open(filename, 'r')
-            f.close()
-        except IOError:
-            pass
-
-        if thread is True:
-            self.worker_task.emit({'fcn': self.save_project,
-                                   'params': [filename, quit]})
-        else:
-            self.save_project(filename, quit)
-
-        # self.save_project(filename)
-        if self.defaults["global_open_style"] is False:
-            self.file_opened.emit("project", filename)
-        self.file_saved.emit("project", filename)
-        if not make_copy:
-            self.project_filename = filename
-
-        self.set_ui_title(name=self.project_filename)
-        self.should_we_save = False
-
-    def export_svg(self, obj_name, filename, scale_factor=0.00):
-        """
-        Exports a Geometry Object to an SVG file.
-
-        :param filename: Path to the SVG file to save to.
-        :return:
-        """
-        self.report_usage("export_svg()")
-
-        if filename is None:
-            filename = self.defaults["global_last_save_folder"]
-
-        self.log.debug("export_svg()")
-
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
-
-        with self.proc_container.new(_("Exporting SVG")) as proc:
-            exported_svg = obj.export_svg(scale_factor=scale_factor)
-
-            # Determine bounding area for svg export
-            bounds = obj.bounds()
-            size = obj.size()
-
-            # Convert everything to strings for use in the xml doc
-            svgwidth = str(size[0])
-            svgheight = str(size[1])
-            minx = str(bounds[0])
-            miny = str(bounds[1] - size[1])
-            uom = obj.units.lower()
-
-            # Add a SVG Header and footer to the svg output from shapely
-            # The transform flips the Y Axis so that everything renders
-            # properly within svg apps such as inkscape
-            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
-                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
-            svg_header += 'width="' + svgwidth + uom + '" '
-            svg_header += 'height="' + svgheight + uom + '" '
-            svg_header += 'viewBox="' + minx + ' ' + miny + ' ' + svgwidth + ' ' + svgheight + '">'
-            svg_header += '<g transform="scale(1,-1)">'
-            svg_footer = '</g> </svg>'
-            svg_elem = svg_header + exported_svg + svg_footer
-
-            # Parse the xml through a xml parser just to add line feeds
-            # and to make it look more pretty for the output
-            svgcode = parse_xml_string(svg_elem)
-            try:
-                with open(filename, 'w') as fp:
-                    fp.write(svgcode.toprettyxml())
-            except PermissionError:
-                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
-                                   "Most likely another app is holding the file open and not accessible."))
-                return 'fail'
-
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("SVG", filename)
-            self.file_saved.emit("SVG", filename)
-            self.inform.emit(_("[success] SVG file exported to %s") % filename)
-
-    def export_svg_negative(self, obj_name, box_name, filename, boundary, scale_factor=0.00, use_thread=True):
-        """
-        Exports a Geometry Object to an SVG file in negative.
-
-        :param filename: Path to the SVG file to save to.
-        :param: use_thread: If True use threads
-        :type: Bool
-        :return:
-        """
-        self.report_usage("export_negative()")
-
-        if filename is None:
-            filename = self.defaults["global_last_save_folder"]
-
-        self.log.debug("export_svg() negative")
-
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
-
-        try:
-            box = self.collection.get_by_name(str(box_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % box_name
-
-        if box is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object Box. Using instead %s") % obj)
-            box = obj
-
-        def make_negative_film():
-            exported_svg = obj.export_svg(scale_factor=scale_factor)
-
-            self.progress.emit(40)
-
-            # Determine bounding area for svg export
-            bounds = box.bounds()
-            size = box.size()
-
-            uom = obj.units.lower()
-
-            # Convert everything to strings for use in the xml doc
-            svgwidth = str(size[0] + (2 * boundary))
-            svgheight = str(size[1] + (2 * boundary))
-            minx = str(bounds[0] - boundary)
-            miny = str(bounds[1] + boundary + size[1])
-            miny_rect = str(bounds[1] - boundary)
-
-            # Add a SVG Header and footer to the svg output from shapely
-            # The transform flips the Y Axis so that everything renders
-            # properly within svg apps such as inkscape
-            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
-                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
-            svg_header += 'width="' + svgwidth + uom + '" '
-            svg_header += 'height="' + svgheight + uom + '" '
-            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
-            svg_header += '>'
-            svg_header += '<g transform="scale(1,-1)">'
-            svg_footer = '</g> </svg>'
-
-            self.progress.emit(60)
-
-            # Change the attributes of the exported SVG
-            # We don't need stroke-width - wrong, we do when we have lines with certain width
-            # We set opacity to maximum
-            # We set the color to WHITE
-            root = ET.fromstring(exported_svg)
-            for child in root:
-                child.set('fill', '#FFFFFF')
-                child.set('opacity', '1.0')
-                child.set('stroke', '#FFFFFF')
-
-            # first_svg_elem = 'rect x="' + minx + '" ' + 'y="' + miny_rect + '" '
-            # first_svg_elem += 'width="' + svgwidth + '" ' + 'height="' + svgheight + '" '
-            # first_svg_elem += 'fill="#000000" opacity="1.0" stroke-width="0.0"'
-
-            first_svg_elem_tag = 'rect'
-            first_svg_elem_attribs = {
-                'x': minx,
-                'y': miny_rect,
-                'width': svgwidth,
-                'height': svgheight,
-                'id': 'neg_rect',
-                'style': 'fill:#000000;opacity:1.0;stroke-width:0.0'
-            }
-
-            root.insert(0, ET.Element(first_svg_elem_tag, first_svg_elem_attribs))
-            exported_svg = ET.tostring(root)
-
-            svg_elem = svg_header + str(exported_svg) + svg_footer
-            self.progress.emit(80)
-
-            # Parse the xml through a xml parser just to add line feeds
-            # and to make it look more pretty for the output
-            doc = parse_xml_string(svg_elem)
-            try:
-                with open(filename, 'w') as fp:
-                    fp.write(doc.toprettyxml())
-            except PermissionError:
-                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
-                                   "Most likely another app is holding the file open and not accessible."))
-                return 'fail'
-
-            self.progress.emit(100)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("SVG", filename)
-            self.file_saved.emit("SVG", filename)
-            self.inform.emit(_("[success] SVG file exported to %s") % filename)
-
-        if use_thread is True:
-            proc = self.proc_container.new(_("Generating Film ... Please wait."))
-
-            def job_thread_film(app_obj):
-                try:
-                    make_negative_film()
-                except Exception as e:
-                    proc.done()
-                    return
-                proc.done()
-
-            self.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
-        else:
-            make_negative_film()
-
-    def export_svg_black(self, obj_name, box_name, filename, scale_factor=0.00, use_thread=True):
-        """
-        Exports a Geometry Object to an SVG file in negative.
-
-        :param filename: Path to the SVG file to save to.
-        :param: use_thread: If True use threads
-        :type: Bool
-        :return:
-        """
-        self.report_usage("export_svg_black()")
-
-        if filename is None:
-            filename = self.defaults["global_last_save_folder"]
-
-        self.log.debug("export_svg() black")
-
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
-
-        try:
-            box = self.collection.get_by_name(str(box_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % box_name
-
-        if box is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object Box. Using instead %s") % obj)
-            box = obj
-
-        def make_black_film():
-            exported_svg = obj.export_svg(scale_factor=scale_factor)
-
-            self.progress.emit(40)
-
-            # Change the attributes of the exported SVG
-            # We don't need stroke-width
-            # We set opacity to maximum
-            # We set the colour to WHITE
-            root = ET.fromstring(exported_svg)
-            for child in root:
-                child.set('fill', '#000000')
-                child.set('opacity', '1.0')
-                child.set('stroke', '#000000')
-
-            exported_svg = ET.tostring(root)
-
-            # Determine bounding area for svg export
-            bounds = box.bounds()
-            size = box.size()
-
-            # This contain the measure units
-            uom = obj.units.lower()
-
-            # Define a boundary around SVG of about 1.0mm (~39mils)
-            if uom in "mm":
-                boundary = 1.0
-            else:
-                boundary = 0.0393701
-
-            self.progress.emit(80)
-
-            # Convert everything to strings for use in the xml doc
-            svgwidth = str(size[0] + (2 * boundary))
-            svgheight = str(size[1] + (2 * boundary))
-            minx = str(bounds[0] - boundary)
-            miny = str(bounds[1] + boundary + size[1])
-
-            self.log.debug(minx)
-            self.log.debug(miny)
-
-            # Add a SVG Header and footer to the svg output from shapely
-            # The transform flips the Y Axis so that everything renders
-            # properly within svg apps such as inkscape
-            svg_header = '<svg xmlns="http://www.w3.org/2000/svg" ' \
-                         'version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" '
-            svg_header += 'width="' + svgwidth + uom + '" '
-            svg_header += 'height="' + svgheight + uom + '" '
-            svg_header += 'viewBox="' + minx + ' -' + miny + ' ' + svgwidth + ' ' + svgheight + '" '
-            svg_header += '>'
-            svg_header += '<g transform="scale(1,-1)">'
-            svg_footer = '</g> </svg>'
-
-            svg_elem = str(svg_header) + str(exported_svg) + str(svg_footer)
-
-            self.progress.emit(90)
-
-            # Parse the xml through a xml parser just to add line feeds
-            # and to make it look more pretty for the output
-            doc = parse_xml_string(svg_elem)
-            try:
-                with open(filename, 'w') as fp:
-                    fp.write(doc.toprettyxml())
-            except PermissionError:
-                self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
-                                   "Most likely another app is holding the file open and not accessible."))
-                return 'fail'
-
-            self.progress.emit(100)
-            if self.defaults["global_open_style"] is False:
-                self.file_opened.emit("SVG", filename)
-            self.file_saved.emit("SVG", filename)
-            self.inform.emit(_("[success] SVG file exported to %s") % filename)
-
-        if use_thread is True:
-            proc = self.proc_container.new(_("Generating Film ... Please wait."))
-
-            def job_thread_film(app_obj):
-                try:
-                    make_black_film()
-                except Exception as e:
-                    proc.done()
-                    return
-                proc.done()
-
-            self.worker_task.emit({'fcn': job_thread_film, 'params': [self]})
-        else:
-            make_black_film()
-
-    def save_source_file(self, obj_name, filename, use_thread=True):
-        """
-        Exports a Gerber Object to an Gerber file.
-
-        :param filename: Path to the Gerber file to save to.
-        :return:
-        """
-        self.report_usage("save source file()")
-
-        if filename is None:
-            filename = self.defaults["global_last_save_folder"]
-
-        self.log.debug("save source file()")
-
-        obj = self.collection.get_by_name(obj_name)
-
-        file_string = StringIO(obj.source_file)
-        time_string = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
-
-        try:
-            with open(filename, 'w') as file:
-                file.writelines('G04*\n')
-                file.writelines('G04 %s (RE)GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' %
-                                (obj.kind.upper(), str(self.version), str(self.version_date)))
-                file.writelines('G04 Filename: %s*\n' % str(obj_name))
-                file.writelines('G04 Created on : %s*\n' % time_string)
-
-                for line in file_string:
-                    file.writelines(line)
-        except PermissionError:
-            self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
-                               "Most likely another app is holding the file open and not accessible."))
-            return 'fail'
-
-    def export_excellon(self, obj_name, filename, use_thread=True):
-        """
-        Exports a Excellon Object to an Excellon file.
-
-        :param filename: Path to the Excellon file to save to.
-        :return:
-        """
-        self.report_usage("export_excellon()")
-
-        if filename is None:
-            filename = self.defaults["global_last_save_folder"]
-
-        self.log.debug("export_excellon()")
-
-        format_exc = ';FILE_FORMAT=%d:%d\n' % (self.defaults["excellon_exp_integer"],
-                                               self.defaults["excellon_exp_decimals"]
-                                               )
-        units = ''
-
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
-
-        # updated units
-        eunits = self.defaults["excellon_exp_units"]
-        ewhole = self.defaults["excellon_exp_integer"]
-        efract = self.defaults["excellon_exp_decimals"]
-        ezeros = self.defaults["excellon_exp_zeros"]
-        eformat = self.defaults[ "excellon_exp_format"]
-
-        fc_units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-        if fc_units == 'MM':
-            factor = 1 if eunits == 'METRIC' else 0.03937
-        else:
-            factor = 25.4 if eunits == 'METRIC' else 1
-
-        def make_excellon():
-            try:
-                time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
-
-                header = 'M48\n'
-                header += ';EXCELLON GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s\n' % \
-                          (str(self.version), str(self.version_date))
-
-                header += ';Filename: %s' % str(obj_name) + '\n'
-                header += ';Created on : %s' % time_str + '\n'
-
-                if eformat == 'dec':
-                    has_slots, excellon_code = obj.export_excellon(ewhole, efract, factor=factor)
-                    header += eunits + '\n'
-
-                    for tool in obj.tools:
-                        if eunits == 'METRIC':
-                            header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['C']) * factor,
-                                                                          tool=str(tool),
-                                                                          dec=2)
-                        else:
-                            header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['C']) * factor,
-                                                                          tool=str(tool),
-                                                                          dec=4)
-                else:
-                    if ezeros == 'LZ':
-                        has_slots, excellon_code = obj.export_excellon(ewhole, efract,
-                                                                       form='ndec', e_zeros='LZ', factor=factor)
-                        header += '%s,%s\n' % (eunits, 'LZ')
-                        header += format_exc
-
-                        for tool in obj.tools:
-                            if eunits == 'METRIC':
-                                header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['C']) * factor,
-                                                                              tool=str(tool),
-                                                                              dec=2)
-                            else:
-                                header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['C']) * factor,
-                                                                              tool=str(tool),
-                                                                              dec=4)
-                    else:
-                        has_slots, excellon_code = obj.export_excellon(ewhole, efract,
-                                                                       form='ndec', e_zeros='TZ', factor=factor)
-                        header += '%s,%s\n' % (eunits, 'TZ')
-                        header += format_exc
-
-                        for tool in obj.tools:
-                            if eunits == 'METRIC':
-                                header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['C']) * factor,
-                                                                              tool=str(tool),
-                                                                              dec=2)
-                            else:
-                                header += "T{tool}F00S00C{:.{dec}f}\n".format(float(obj.tools[tool]['C']) * factor,
-                                                                              tool=str(tool),
-                                                                              dec=4)
-                header += '%\n'
-                footer = 'M30\n'
-
-                exported_excellon = header
-                exported_excellon += excellon_code
-                exported_excellon += footer
-
-                try:
-                    with open(filename, 'w') as fp:
-                        fp.write(exported_excellon)
-                except PermissionError:
-                    self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
-                                       "Most likely another app is holding the file open and not accessible."))
-                    return 'fail'
-
-                if self.defaults["global_open_style"] is False:
-                    self.file_opened.emit("Excellon", filename)
-                self.file_saved.emit("Excellon", filename)
-                self.inform.emit(_("[success] Excellon file exported to %s") % filename)
-            except Exception as e:
-                log.debug("App.export_excellon.make_excellon() --> %s" % str(e))
-                return 'fail'
-
-        if use_thread is True:
-
-            with self.proc_container.new(_("Exporting Excellon")) as proc:
-
-                def job_thread_exc(app_obj):
-                    ret = make_excellon()
-                    if ret == 'fail':
-                        self.inform.emit(_('[ERROR_NOTCL] Could not export Excellon file.'))
-                        return
-
-                self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
-        else:
-            ret = make_excellon()
-            if ret == 'fail':
-                self.inform.emit(_('[ERROR_NOTCL] Could not export Excellon file.'))
-                return
-
-    def export_gerber(self, obj_name, filename, use_thread=True):
-        """
-        Exports a Gerber Object to an Gerber file.
-
-        :param filename: Path to the Gerber file to save to.
-        :return:
-        """
-        self.report_usage("export_gerber()")
-
-        if filename is None:
-            filename = self.defaults["global_last_save_folder"]
-
-        self.log.debug("export_gerber()")
-
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
-
-        # updated units
-        gunits = self.defaults["gerber_exp_units"]
-        gwhole = self.defaults["gerber_exp_integer"]
-        gfract = self.defaults["gerber_exp_decimals"]
-        gzeros = self.defaults["gerber_exp_zeros"]
-
-        fc_units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-        if fc_units == 'MM':
-            factor = 1 if gunits == 'MM' else 0.03937
-        else:
-            factor = 25.4 if gunits == 'MM' else 1
-
-        def make_gerber():
-            try:
-                time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
-
-                header = 'G04*\n'
-                header += 'G04 RS-274X GERBER GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s*\n' % \
-                          (str(self.version), str(self.version_date))
-
-                header += 'G04 Filename: %s*' % str(obj_name) + '\n'
-                header += 'G04 Created on : %s*' % time_str + '\n'
-                header += '%%FS%sAX%s%sY%s%s*%%\n' % (gzeros, gwhole, gfract, gwhole, gfract)
-                header += "%MO{units}*%\n".format(units=gunits)
-
-                for apid in obj.apertures:
-                    if obj.apertures[apid]['type'] == 'C':
-                        header += "%ADD{apid}{type},{size}*%\n".format(
-                            apid=str(apid),
-                            type='C',
-                            size=(factor * obj.apertures[apid]['size'])
-                        )
-                    elif obj.apertures[apid]['type'] == 'R':
-                        header += "%ADD{apid}{type},{width}X{height}*%\n".format(
-                            apid=str(apid),
-                            type='R',
-                            width=(factor * obj.apertures[apid]['width']),
-                            height=(factor * obj.apertures[apid]['height'])
-                        )
-                    elif obj.apertures[apid]['type'] == 'O':
-                        header += "%ADD{apid}{type},{width}X{height}*%\n".format(
-                            apid=str(apid),
-                            type='O',
-                            width=(factor * obj.apertures[apid]['width']),
-                            height=(factor * obj.apertures[apid]['height'])
-                        )
-
-                header += '\n'
-
-                # obsolete units but some software may need it
-                if gunits == 'IN':
-                    header += 'G70*\n'
-                else:
-                    header += 'G71*\n'
-
-                # Absolute Mode
-                header += 'G90*\n'
-
-                header += 'G01*\n'
-                # positive polarity
-                header += '%LPD*%\n'
-
-                footer = 'M02*\n'
-
-                gerber_code = obj.export_gerber(gwhole, gfract, g_zeros=gzeros, factor=factor)
-
-                exported_gerber = header
-                exported_gerber += gerber_code
-                exported_gerber += footer
-
-                try:
-                    with open(filename, 'w') as fp:
-                        fp.write(exported_gerber)
-                except PermissionError:
-                    self.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
-                                       "Most likely another app is holding the file open and not accessible."))
-                    return 'fail'
-
-                if self.defaults["global_open_style"] is False:
-                    self.file_opened.emit("Gerber", filename)
-                self.file_saved.emit("Gerber", filename)
-                self.inform.emit(_("[success] Gerber file exported to %s") % filename)
-            except Exception as e:
-                log.debug("App.export_gerber.make_gerber() --> %s" % str(e))
-                return 'fail'
-
-        if use_thread is True:
-
-            with self.proc_container.new(_("Exporting Gerber")) as proc:
-
-                def job_thread_exc(app_obj):
-                    ret = make_gerber()
-                    if ret == 'fail':
-                        self.inform.emit(_('[ERROR_NOTCL] Could not export Gerber file.'))
-                        return
-
-                self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
-        else:
-            ret = make_gerber()
-            if ret == 'fail':
-                self.inform.emit(_('[ERROR_NOTCL] Could not export Gerber file.'))
-                return
-
-    def export_dxf(self, obj_name, filename, use_thread=True):
-        """
-        Exports a Geometry Object to an DXF file.
-
-        :param filename: Path to the DXF file to save to.
-        :return:
-        """
-        self.report_usage("export_dxf()")
-
-        if filename is None:
-            filename = self.defaults["global_last_save_folder"]
-
-        self.log.debug("export_dxf()")
-
-        format_exc = ''
-        units = ''
-
-        try:
-            obj = self.collection.get_by_name(str(obj_name))
-        except:
-            # TODO: The return behavior has not been established... should raise exception?
-            return "Could not retrieve object: %s" % obj_name
-
-        # updated units
-        units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-        if units == 'IN' or units == 'INCH':
-            units = 'INCH'
-        elif units == 'MM' or units == 'METIRC':
-            units ='METRIC'
-
-        def make_dxf():
-            try:
-                dxf_code = obj.export_dxf()
-                dxf_code.saveas(filename)
-                if self.defaults["global_open_style"] is False:
-                    self.file_opened.emit("DXF", filename)
-                self.file_saved.emit("DXF", filename)
-                self.inform.emit(_("[success] DXF file exported to %s") % filename)
-            except:
-                return 'fail'
-
-        if use_thread is True:
-
-            with self.proc_container.new(_("Exporting DXF")) as proc:
-
-                def job_thread_exc(app_obj):
-                    ret = make_dxf()
-                    if ret == 'fail':
-                        self.inform.emit(_('[[WARNING_NOTCL]] Could not export DXF file.'))
-                        return
-
-                self.worker_task.emit({'fcn': job_thread_exc, 'params': [self]})
-        else:
-            ret = make_dxf()
-            if ret == 'fail':
-                self.inform.emit(_('[[WARNING_NOTCL]] Could not export DXF file.'))
-                return
-
-    def import_svg(self, filename, geo_type='geometry', outname=None):
-        """
-        Adds a new Geometry Object to the projects and populates
-        it with shapes extracted from the SVG file.
-
-        :param filename: Path to the SVG file.
-        :param outname:
-        :return:
-        """
-        self.report_usage("import_svg()")
-
-        obj_type = ""
-        if geo_type is None or geo_type == "geometry":
-            obj_type = "geometry"
-        elif geo_type == "gerber":
-            obj_type = geo_type
-        else:
-            self.inform.emit(_("[ERROR_NOTCL] Not supported type is picked as parameter. "
-                             "Only Geometry and Gerber are supported"))
-            return
-
-        units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        def obj_init(geo_obj, app_obj):
-            geo_obj.import_svg(filename, obj_type, units=units)
-            geo_obj.multigeo = False
-
-        with self.proc_container.new(_("Importing SVG")) as proc:
-
-            # Object name
-            name = outname or filename.split('/')[-1].split('\\')[-1]
-
-            self.new_object(obj_type, name, obj_init, autoselected=False)
-            self.progress.emit(20)
-            # Register recent file
-            self.file_opened.emit("svg", filename)
-
-            # GUI feedback
-            self.inform.emit(_("[success] Opened: %s") % filename)
-            self.progress.emit(100)
-
-    def import_dxf(self, filename, geo_type='geometry', outname=None):
-        """
-        Adds a new Geometry Object to the projects and populates
-        it with shapes extracted from the DXF file.
-
-        :param filename: Path to the DXF file.
-        :param outname:
-        :type putname: str
-        :return:
-        """
-        self.report_usage("import_dxf()")
-
-        obj_type = ""
-        if geo_type is None or geo_type == "geometry":
-            obj_type = "geometry"
-        elif geo_type == "gerber":
-            obj_type = geo_type
-        else:
-            self.inform.emit(_("[ERROR_NOTCL] Not supported type is picked as parameter. "
-                             "Only Geometry and Gerber are supported"))
-            return
-
-        units = self.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        def obj_init(geo_obj, app_obj):
-            geo_obj.import_dxf(filename, obj_type, units=units)
-            geo_obj.multigeo = False
-
-        with self.proc_container.new(_("Importing DXF")) as proc:
-
-            # Object name
-            name = outname or filename.split('/')[-1].split('\\')[-1]
-
-            self.new_object(obj_type, name, obj_init, autoselected=False)
-            self.progress.emit(20)
-            # Register recent file
-            self.file_opened.emit("dxf", filename)
-
-            # GUI feedback
-            self.inform.emit(_("[success] Opened: %s") % filename)
-            self.progress.emit(100)
-
-    def import_image(self, filename, o_type='gerber', dpi=96, mode='black', mask=[250, 250, 250, 250], outname=None):
-        """
-        Adds a new Geometry Object to the projects and populates
-        it with shapes extracted from the SVG file.
-
-        :param filename: Path to the SVG file.
-        :param o_type: type of FlatCAM objeect
-        :param dpi: dot per inch
-        :param mode: black or color
-        :param mask: dictate the level of detail
-        :param outname: name for the resulting file
-        :return:
-        """
-        self.report_usage("import_image()")
-
-        if o_type is None or o_type == "geometry":
-            obj_type = "geometry"
-        elif o_type == "gerber":
-            obj_type = o_type
-        else:
-            self.inform.emit(_("[ERROR_NOTCL] Not supported type is picked as parameter. "
-                               "Only Geometry and Gerber are supported"))
-            return
-
-        def obj_init(geo_obj, app_obj):
-            geo_obj.import_image(filename, units=units, dpi=dpi, mode=mode, mask=mask)
-            geo_obj.multigeo = False
-
-        with self.proc_container.new(_("Importing Image")) as proc:
-
-            # Object name
-            name = outname or filename.split('/')[-1].split('\\')[-1]
-            units = self.ui.general_defaults_form.general_app_group.units_radio.get_value()
-
-            self.new_object(obj_type, name, obj_init)
-            self.progress.emit(20)
-            # Register recent file
-            self.file_opened.emit("image", filename)
-
-            # GUI feedback
-            self.inform.emit(_("[success] Opened: %s") % filename)
-            self.progress.emit(100)
-
-    def open_gerber(self, filename, outname=None):
-        """
-        Opens a Gerber file, parses it and creates a new object for
-        it in the program. Thread-safe.
-
-        :param outname: Name of the resulting object. None causes the
-            name to be that of the file.
-        :param filename: Gerber file filename
-        :type filename: str
-        :param follow: If true, the parser will not create polygons, just lines
-            following the gerber path.
-        :type follow: bool
-        :return: None
-        """
-
-        # How the object should be initialized
-        def obj_init(gerber_obj, app_obj):
-
-            assert isinstance(gerber_obj, FlatCAMGerber), \
-                "Expected to initialize a FlatCAMGerber but got %s" % type(gerber_obj)
-
-            # Opening the file happens here
-            self.progress.emit(30)
-            try:
-                gerber_obj.parse_file(filename)
-            except IOError:
-                app_obj.inform.emit(_("[ERROR_NOTCL] Failed to open file: %s") % filename)
-                app_obj.progress.emit(0)
-                self.inform.emit(_('[ERROR_NOTCL] Failed to open file: %s') % filename)
-                return "fail"
-            except ParseError as err:
-                app_obj.inform.emit(_("[ERROR_NOTCL] Failed to parse file: {name}. {error}").format(name=filename,
-                                                                                                    error=str(err)))
-                app_obj.progress.emit(0)
-                self.log.error(str(err))
-                return "fail"
-            except Exception as e:
-                log.debug("App.open_gerber() --> %s" % str(e))
-                msg = _("[ERROR] An internal error has occurred. See shell.\n")
-                msg += traceback.format_exc()
-                app_obj.inform.emit(msg)
-                return "fail"
-
-            if gerber_obj.is_empty():
-                # app_obj.inform.emit("[ERROR] No geometry found in file: " + filename)
-                # self.collection.set_active(gerber_obj.options["name"])
-                # self.collection.delete_active()
-                self.inform.emit(_("[ERROR_NOTCL] Object is not Gerber file or empty. Aborting object creation."))
-                return "fail"
-
-            # Further parsing
-            self.progress.emit(70)  # TODO: Note the mixture of self and app_obj used here
-
-        App.log.debug("open_gerber()")
-
-        with self.proc_container.new(_("Opening Gerber")) as proc:
-
-            self.progress.emit(10)
-
-            # Object name
-            name = outname or filename.split('/')[-1].split('\\')[-1]
-
-            # # ## Object creation # ##
-            ret = self.new_object("gerber", name, obj_init, autoselected=False)
-            if ret == 'fail':
-                self.inform.emit(_('[ERROR_NOTCL] Open Gerber failed. Probable not a Gerber file.'))
-                return
-
-            # Register recent file
-            self.file_opened.emit("gerber", filename)
-
-            self.progress.emit(100)
-
-            # GUI feedback
-            self.inform.emit(_("[success] Opened: %s") % filename)
-
-    def open_excellon(self, filename, outname=None):
-        """
-        Opens an Excellon file, parses it and creates a new object for
-        it in the program. Thread-safe.
-
-        :param outname: Name of the resulting object. None causes the
-            name to be that of the file.
-        :param filename: Excellon file filename
-        :type filename: str
-        :return: None
-        """
-
-        App.log.debug("open_excellon()")
-
-        # How the object should be initialized
-        def obj_init(excellon_obj, app_obj):
-            # self.progress.emit(20)
-
-            try:
-                ret = excellon_obj.parse_file(filename=filename)
-                if ret == "fail":
-                    log.debug("Excellon parsing failed.")
-                    self.inform.emit(_("[ERROR_NOTCL] This is not Excellon file."))
-                    return "fail"
-            except IOError:
-                app_obj.inform.emit(_("[ERROR_NOTCL] Cannot open file: %s") % filename)
-                log.debug("Could not open Excellon object.")
-                self.progress.emit(0)  # TODO: self and app_bjj mixed
-                return "fail"
-            except:
-                msg = _("[ERROR_NOTCL] An internal error has occurred. See shell.\n")
-                msg += traceback.format_exc()
-                app_obj.inform.emit(msg)
-                return "fail"
-
-            ret = excellon_obj.create_geometry()
-            if ret == 'fail':
-                log.debug("Could not create geometry for Excellon object.")
-                return "fail"
-
-            for tool in excellon_obj.tools:
-                if excellon_obj.tools[tool]['solid_geometry']:
-                    return
-            app_obj.inform.emit(_("[ERROR_NOTCL] No geometry found in file: %s") % filename)
-            return "fail"
-
-        with self.proc_container.new(_("Opening Excellon.")):
-
-            # Object name
-            name = outname or filename.split('/')[-1].split('\\')[-1]
-            ret_val = self.new_object("excellon", name, obj_init, autoselected=False)
-            if ret_val == 'fail':
-                self.inform.emit(_('[ERROR_NOTCL] Open Excellon file failed. Probable not an Excellon file.'))
-                return
-
-            # Register recent file
-            self.file_opened.emit("excellon", filename)
-
-            # GUI feedback
-            self.inform.emit(_("[success] Opened: %s") % filename)
-
-    def open_gcode(self, filename, outname=None):
-        """
-        Opens a G-gcode file, parses it and creates a new object for
-        it in the program. Thread-safe.
-
-        :param outname: Name of the resulting object. None causes the name to be that of the file.
-        :param filename: G-code file filename
-        :type filename: str
-        :return: None
-        """
-        App.log.debug("open_gcode()")
-
-        # How the object should be initialized
-        def obj_init(job_obj, app_obj_):
-            """
-            :param job_obj: the resulting object
-            :type app_obj_: App
-            """
-            assert isinstance(app_obj_, App), \
-                "Initializer expected App, got %s" % type(app_obj_)
-
-            self.progress.emit(10)
-
-            try:
-                f = open(filename)
-                gcode = f.read()
-                f.close()
-            except IOError:
-                app_obj_.inform.emit(_("[ERROR_NOTCL] Failed to open %s") % filename)
-                self.progress.emit(0)
-                return "fail"
-
-            job_obj.gcode = gcode
-
-            self.progress.emit(20)
-
-            ret = job_obj.gcode_parse()
-            if ret == "fail":
-                self.inform.emit(_("[ERROR_NOTCL] This is not GCODE"))
-                return "fail"
-
-            self.progress.emit(60)
-            job_obj.create_geometry()
-
-        with self.proc_container.new(_("Opening G-Code.")):
-
-            # Object name
-            name = outname or filename.split('/')[-1].split('\\')[-1]
-
-            # New object creation and file processing
-            ret = self.new_object("cncjob", name, obj_init, autoselected=False)
-            if ret == 'fail':
-                self.inform.emit(_("[ERROR_NOTCL] Failed to create CNCJob Object. Probable not a GCode file.\n "
-                                   "Attempting to create a FlatCAM CNCJob Object from "
-                                   "G-Code file failed during processing"))
-                return "fail"
-
-            # Register recent file
-            self.file_opened.emit("cncjob", filename)
-
-            # GUI feedback
-            self.inform.emit(_("[success] Opened: %s") % filename)
-            self.progress.emit(100)
-
-    def open_config_file(self, filename, run_from_arg=None):
-        """
-        Loads a config file from the specified file.
-
-        :param filename:  Name of the file from which to load.
-        :type filename: str
-        :return: None
-        """
-        App.log.debug("Opening config file: " + filename)
-
-        # add the tab if it was closed
-        self.ui.plot_tab_area.addTab(self.ui.cncjob_tab, _("Code Editor"))
-        # first clear previous text in text editor (if any)
-        self.ui.code_editor.clear()
-
-        # Switch plot_area to CNCJob tab
-        self.ui.plot_tab_area.setCurrentWidget(self.ui.cncjob_tab)
-
-        try:
-            if filename:
-                f = QtCore.QFile(filename)
-                if f.open(QtCore.QIODevice.ReadOnly):
-                    stream = QtCore.QTextStream(f)
-                    gcode_edited = stream.readAll()
-                    self.ui.code_editor.setPlainText(gcode_edited)
-                    f.close()
-        except IOError:
-            App.log.error("Failed to open config file: %s" % filename)
-            self.inform.emit(_("[ERROR_NOTCL] Failed to open config file: %s") % filename)
-            return
-
-    def open_project(self, filename, run_from_arg=None):
-        """
-        Loads a project from the specified file.
-
-        1) Loads and parses file
-        2) Registers the file as recently opened.
-        3) Calls on_file_new()
-        4) Updates options
-        5) Calls new_object() with the object's from_dict() as init method.
-        6) Calls plot_all()
-
-        :param filename:  Name of the file from which to load.
-        :type filename: str
-        :param run_from_arg: True if run for arguments
-        :return: None
-        """
-        App.log.debug("Opening project: " + filename)
-
-        # Open and parse an uncompressed Project file
-        try:
-            f = open(filename, 'r')
-        except IOError:
-            App.log.error("Failed to open project file: %s" % filename)
-            self.inform.emit(_("[ERROR_NOTCL] Failed to open project file: %s") % filename)
-            return
-
-        try:
-            d = json.load(f, object_hook=dict2obj)
-        except Exception as e:
-            App.log.error("Failed to parse project file, trying to see if it loads as an LZMA archive: %s because %s" %
-                          (filename, str(e)))
-            f.close()
-
-            # Open and parse a compressed Project file
-            try:
-                with lzma.open(filename) as f:
-                    file_content = f.read().decode('utf-8')
-                    d = json.loads(file_content, object_hook=dict2obj)
-            except IOError:
-                App.log.error("Failed to open project file: %s" % filename)
-                self.inform.emit(_("[ERROR_NOTCL] Failed to open project file: %s") % filename)
-                return
-
-        # Clear the current project
-        # # NOT THREAD SAFE # ##
-        if run_from_arg is True:
-            pass
-        else:
-            self.on_file_new()
-
-        # Project options
-        self.options.update(d['options'])
-        self.project_filename = filename
-        self.set_screen_units(self.options["units"])
-
-        # Re create objects
-        App.log.debug("Re-creating objects...")
-        for obj in d['objs']:
-            def obj_init(obj_inst, app_inst):
-                obj_inst.from_dict(obj)
-            App.log.debug(obj['kind'] + ":  " + obj['options']['name'])
-            self.new_object(obj['kind'], obj['options']['name'], obj_init, active=False, fit=False, plot=True)
-        self.plot_all()
-        self.inform.emit(_("[success] Project loaded from: %s") % filename)
-
-        self.should_we_save = False
-        self.file_opened.emit("project", filename)
-        self.set_ui_title(name=self.project_filename)
-
-        App.log.debug("Project loaded")
-
-    def propagate_defaults(self, silent=False):
-        """
-        This method is used to set default values in classes. It's
-        an alternative to project options but allows the use
-        of values invisible to the user.
-
-        :return: None
-        """
-
-        if silent is False:
-            self.log.debug("propagate_defaults()")
-
-        # Which objects to update the given parameters.
-        routes = {
-            "global_zdownrate": CNCjob,
-            "excellon_zeros": Excellon,
-            "excellon_format_upper_in": Excellon,
-            "excellon_format_lower_in": Excellon,
-            "excellon_format_upper_mm": Excellon,
-            "excellon_format_lower_mm": Excellon,
-            "excellon_units": Excellon,
-            "gerber_use_buffer_for_union": Gerber,
-            "geometry_multidepth": Geometry
-        }
-
-        for param in routes:
-            if param in routes[param].defaults:
-                try:
-                    routes[param].defaults[param] = self.defaults[param]
-                    if silent is False:
-                        self.log.debug("  " + param + " OK")
-                except KeyError:
-                    if silent is False:
-                        self.log.debug("  ERROR: " + param + " not in defaults.")
-            else:
-                # Try extracting the name:
-                # classname_param here is param in the object
-                if param.find(routes[param].__name__.lower() + "_") == 0:
-                    p = param[len(routes[param].__name__) + 1:]
-                    if p in routes[param].defaults:
-                        routes[param].defaults[p] = self.defaults[param]
-                        if silent is False:
-                            self.log.debug("  " + param + " OK!")
-
-    def restore_main_win_geom(self):
-        try:
-            self.ui.setGeometry(self.defaults["global_def_win_x"],
-                                self.defaults["global_def_win_y"],
-                                self.defaults["global_def_win_w"],
-                                self.defaults["global_def_win_h"])
-            self.ui.splitter.setSizes([self.defaults["global_def_notebook_width"], 0])
-
-            settings = QSettings("Open Source", "FlatCAM")
-            if settings.contains("maximized_gui"):
-                maximized_ui = settings.value('maximized_gui', type=bool)
-                if maximized_ui is True:
-                    self.ui.showMaximized()
-        except KeyError as e:
-            log.debug("App.restore_main_win_geom() --> %s" % str(e))
-
-    def plot_all(self, zoom=True):
-        """
-        Re-generates all plots from all objects.
-
-        :return: None
-        """
-        self.log.debug("Plot_all()")
-
-        for obj in self.collection.get_list():
-            def worker_task(obj):
-                with self.proc_container.new("Plotting"):
-                    obj.plot(kind=self.defaults["cncjob_plot_kind"])
-                    if zoom:
-                        self.object_plotted.emit(obj)
-
-            # Send to worker
-            self.worker_task.emit({'fcn': worker_task, 'params': [obj]})
-
-    def register_folder(self, filename):
-        self.defaults["global_last_folder"] = os.path.split(str(filename))[0]
-
-    def register_save_folder(self, filename):
-        self.defaults["global_last_save_folder"] = os.path.split(str(filename))[0]
-
-    def set_progress_bar(self, percentage, text=""):
-        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:
-                return _("Available commands:\n") + \
-                       '\n'.join(['  ' + cmd for cmd in sorted(commands)]) + \
-                       _("\n\nType help <command_name> for usage.\n Example: help open_gerber")
-
-            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.")
-            },
-        }
-
-        # 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):
-
-        # TODO: Move this to constructor
-        icons = {
-            "gerber": "share/flatcam_icon16.png",
-            "excellon": "share/drill16.png",
-            'geometry': "share/geometry16.png",
-            "cncjob": "share/cnc16.png",
-            "project": "share/project16.png",
-            "svg": "share/geometry16.png",
-            "dxf": "share/dxf16.png",
-            "pdf": "share/pdf32.png",
-            "image": "share/image16.png"
-
-        }
-
-        openers = {
-            'gerber': lambda fname: self.worker_task.emit({'fcn': self.open_gerber, 'params': [fname]}),
-            'excellon': lambda fname: self.worker_task.emit({'fcn': self.open_excellon, 'params': [fname]}),
-            'geometry': lambda fname: self.worker_task.emit({'fcn': self.import_dxf, 'params': [fname]}),
-            'cncjob': lambda fname: self.worker_task.emit({'fcn': self.open_gcode, 'params': [fname]}),
-            'project': self.open_project,
-            'svg': self.import_svg,
-            'dxf': self.import_dxf,
-            'image': self.import_image,
-            'pdf': lambda fname: self.worker_task.emit({'fcn': self.pdf_tool.open_pdf, 'params': [fname]})
-        }
-
-        # Open recent file for files
-        try:
-            f = open(self.data_path + '/recent.json')
-        except IOError:
-            App.log.error("Failed to load recent item list.")
-            self.inform.emit(_("[ERROR_NOTCL] Failed to load recent item list."))
-            return
-
-        try:
-            self.recent = json.load(f)
-        except json.scanner.JSONDecodeError:
-            App.log.error("Failed to parse recent item list.")
-            self.inform.emit(_("[ERROR_NOTCL] Failed to parse recent item list."))
-            f.close()
-            return
-        f.close()
-
-        # Open recent file for projects
-        try:
-            fp = open(self.data_path + '/recent_projects.json')
-        except IOError:
-            App.log.error("Failed to load recent project item list.")
-            self.inform.emit(_("[ERROR_NOTCL] Failed to load recent projects item list."))
-            return
-
-        try:
-            self.recent_projects = json.load(fp)
-        except json.scanner.JSONDecodeError:
-            App.log.error("Failed to parse recent project item list.")
-            self.inform.emit(_("[ERROR_NOTCL] Failed to parse recent project item list."))
-            fp.close()
-            return
-        fp.close()
-
-        # Closure needed to create callbacks in a loop.
-        # Otherwise late binding occurs.
-        def make_callback(func, fname):
-            def opener():
-                func(fname)
-            return opener
-
-        def reset_recent_files():
-            # Reset menu
-            self.ui.recent.clear()
-            self.recent = []
-            try:
-                f = open(self.data_path + '/recent.json', 'w')
-            except IOError:
-                App.log.error("Failed to open recent items file for writing.")
-                return
-
-            json.dump(self.recent, f)
-
-        def reset_recent_projects():
-            # Reset menu
-            self.ui.recent_projects.clear()
-            self.recent_projects = []
-
-            try:
-                fp = open(self.data_path + '/recent_projects.json', 'w')
-            except IOError:
-                App.log.error("Failed to open recent projects items file for writing.")
-                return
-
-            json.dump(self.recent, fp)
-
-        # Reset menu
-        self.ui.recent.clear()
-        self.ui.recent_projects.clear()
-
-        # Create menu items for projects
-        for recent in self.recent_projects:
-            filename = recent['filename'].split('/')[-1].split('\\')[-1]
-
-            if recent['kind'] == 'project':
-                try:
-                    action = QtWidgets.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self)
-
-                    # Attach callback
-                    o = make_callback(openers[recent["kind"]], recent['filename'])
-                    action.triggered.connect(o)
-
-                    self.ui.recent_projects.addAction(action)
-
-                except KeyError:
-                    App.log.error("Unsupported file type: %s" % recent["kind"])
-
-        # Last action in Recent Files menu is one that Clear the content
-        clear_action_proj = QtWidgets.QAction(QtGui.QIcon('share/trash32.png'), (_("Clear Recent files")), self)
-        clear_action_proj.triggered.connect(reset_recent_projects)
-        self.ui.recent_projects.addSeparator()
-        self.ui.recent_projects.addAction(clear_action_proj)
-
-        # Create menu items for files
-        for recent in self.recent:
-            filename = recent['filename'].split('/')[-1].split('\\')[-1]
-
-            if recent['kind'] != 'project':
-                try:
-                    action = QtWidgets.QAction(QtGui.QIcon(icons[recent["kind"]]), filename, self)
-
-                    # Attach callback
-                    o = make_callback(openers[recent["kind"]], recent['filename'])
-                    action.triggered.connect(o)
-
-                    self.ui.recent.addAction(action)
-
-                except KeyError:
-                    App.log.error("Unsupported file type: %s" % recent["kind"])
-
-        # Last action in Recent Files menu is one that Clear the content
-        clear_action = QtWidgets.QAction(QtGui.QIcon('share/trash32.png'), (_("Clear Recent files")), self)
-        clear_action.triggered.connect(reset_recent_files)
-        self.ui.recent.addSeparator()
-        self.ui.recent.addAction(clear_action)
-
-        # self.builder.get_object('open_recent').set_submenu(recent_menu)
-        # self.ui.menufilerecent.set_submenu(recent_menu)
-        # recent_menu.show_all()
-        # self.ui.recent.show()
-
-        self.log.debug("Recent items list has been populated.")
-
-    def setup_component_editor(self):
-        # label = QtWidgets.QLabel("Choose an item from Project")
-        # label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
-
-        sel_title = QtWidgets.QTextEdit(
-            _('<b>Shortcut Key List</b>'))
-        sel_title.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
-        sel_title.setFrameStyle(QtWidgets.QFrame.NoFrame)
-        # font = self.sel_title.font()
-        # font.setPointSize(12)
-        # self.sel_title.setFont(font)
-
-        selected_text = _('''
-<p><span style="font-size:14px"><strong>Selected Tab - Choose an Item from Project Tab</strong></span></p>
-
-<p><span style="font-size:10px"><strong>Details</strong>:<br />
-The normal flow when working in FlatCAM is the following:</span></p>
-
-<ol>
-	<li><span style="font-size:10px">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 file into the FLATCAM GUI or through the menu/toolbar links offered within the app.</span><br />
-	&nbsp;</li>
-	<li><span style="font-size:10px">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> 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>-&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>
-</ol>
-
-<p><span style="font-size:10px">A list of key shortcuts is available through an menu entry in <strong>Help -&gt; Shortcuts List</strong>&nbsp;or through it&#39;s own key shortcut: <strng>F3</strong>.</span></p>
-
-        ''')
-
-        sel_title.setText(selected_text)
-        sel_title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
-
-        self.ui.selected_scroll_area.setWidget(sel_title)
-
-#         tool_title = QtWidgets.QTextEdit(
-#             '<b>Shortcut Key List</b>')
-#         tool_title.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
-#         tool_title.setFrameStyle(QtWidgets.QFrame.NoFrame)
-#         # font = self.sel_title.font()
-#         # font.setPointSize(12)
-#         # self.sel_title.setFont(font)
-#
-#         tool_text = '''
-# <p><span style="font-size:14px"><strong>Tool Tab - Choose an Item in Tools Menu</strong></span></p>
-#
-# <p><span style="font-size:10px"><strong>Details</strong>:<br />
-# Some of the functionality of FlatCAM have been implemented as tools (a sort of plugins). </span></p>
-#
-# <p><span style="font-size:10px">Most of the tools are accessible through&nbsp;the Tools menu or by using the associated shortcut keys.<br />
-# Each such a tool, if it needs an object to be used as a source it will provide the way to select this object(s) through a series of comboboxes. The result of using a tool is either a Geometry, an information that can be used in the app or it can be a file that can be saved.</span></p>
-#
-# <ol>
-# </ol>
-#
-# <p><span style="font-size:10px">A list of key shortcuts is available through an menu entry in <strong>Help -&gt; Shortcuts List</strong>&nbsp;or through it&#39;s own key shortcut: &#39;`&#39; (key left to 1).</span></p>
-#
-#                 '''
-#
-#         tool_title.setText(tool_text)
-#         tool_title.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
-#
-#         self.ui.tool_scroll_area.setWidget(tool_title)
-
-    def setup_obj_classes(self):
-        """
-        Sets up application specifics on the FlatCAMObj class.
-
-        :return: None
-        """
-        FlatCAMObj.app = self
-        ObjectCollection.app = self
-        Gerber.app = self
-        Excellon.app = self
-        Geometry.app = self
-        CNCjob.app = self
-        FCProcess.app = self
-        FCProcessContainer.app = self
-
-    def version_check(self):
-        """
-        Checks for the latest version of the program. Alerts the
-        user if theirs is outdated. This method is meant to be run
-        in a separate thread.
-
-        :return: None
-        """
-
-        self.log.debug("version_check()")
-
-        if self.ui.general_defaults_form.general_app_group.send_stats_cb.get_value() is True:
-            full_url = App.version_url + \
-                       "?s=" + str(self.defaults['global_serial']) + \
-                       "&v=" + str(self.version) + \
-                       "&os=" + str(self.os) + \
-                       "&" + urllib.parse.urlencode(self.defaults["global_stats"])
-        else:
-            # 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) + \
-                       "&os=" + str(self.os) + \
-                       "&" + urllib.parse.urlencode(no_ststs_dict["global_ststs"])
-
-        App.log.debug("Checking for updates @ %s" % full_url)
-        # ## Get the data
-        try:
-            f = urllib.request.urlopen(full_url)
-        except:
-            # App.log.warning("Failed checking for latest version. Could not connect.")
-            self.log.warning("Failed checking for latest version. Could not connect.")
-            self.inform.emit(_("[WARNING_NOTCL] Failed checking for latest version. Could not connect."))
-            return
-
-        try:
-            data = json.load(f)
-        except Exception as e:
-            App.log.error("Could not parse information about latest version.")
-            self.inform.emit(_("[ERROR_NOTCL] Could not parse information about latest version."))
-            App.log.debug("json.load(): %s" % str(e))
-            f.close()
-            return
-
-        f.close()
-
-        # ## Latest version?
-        if self.version >= data["version"]:
-            App.log.debug("FlatCAM is up to date!")
-            self.inform.emit(_("[success] FlatCAM is up to date!"))
-            return
-
-        App.log.debug("Newer version available.")
-        self.message.emit(
-            _("Newer Version Available"),
-            _("There is a newer version of FlatCAM available for download:\n\n") +
-            "<b>%s</b>" % str(data["name"]) + "\n%s" % str(data["message"]),
-            _("info")
-        )
-
-    def on_zoom_fit(self, event):
-        """
-        Callback for zoom-out request. This can be either from the corresponding
-        toolbar button or the '1' key when the canvas is focused. Calls ``self.adjust_axes()``
-        with axes limits from the geometry bounds of all objects.
-
-        :param event: Ignored.
-        :return: None
-        """
-
-        self.plotcanvas.fit_view()
-
-    def disable_all_plots(self):
-        self.report_usage("disable_all_plots()")
-
-        self.disable_plots(self.collection.get_list())
-        self.inform.emit(_("[success] All plots disabled."))
-
-    def disable_other_plots(self):
-        self.report_usage("disable_other_plots()")
-
-        self.disable_plots(self.collection.get_non_selected())
-        self.inform.emit(_("[success] All non selected plots disabled."))
-
-    def enable_all_plots(self):
-        self.report_usage("enable_all_plots()")
-
-        self.enable_plots(self.collection.get_list())
-        self.inform.emit(_("[success] All plots enabled."))
-
-    def on_enable_sel_plots(self):
-        log.debug("App.on_enable_sel_plot()")
-        object_list = self.collection.get_selected()
-        self.enable_plots(objects=object_list)
-        self.inform.emit(_("[success] Selected plots enabled..."))
-
-    def on_disable_sel_plots(self):
-        log.debug("App.on_disable_sel_plot()")
-
-        # self.inform.emit(_("Disabling plots ..."))
-        object_list = self.collection.get_selected()
-        self.disable_plots(objects=object_list)
-        self.inform.emit(_("[success] Selected plots disabled..."))
-
-    def enable_plots(self, objects):
-        """
-        Disables plots
-        :param objects: list of Objects to be enabled
-        :return:
-        """
-
-        log.debug("Enabling plots ...")
-        self.inform.emit(_("Working ..."))
-        for obj in objects:
-            obj.options['plot'] = True
-        self.plots_updated.emit()
-
-    def disable_plots(self, objects):
-        """
-        Disables plots
-        :param objects: list of Objects to be disabled
-        :return:
-        """
-
-        # if no objects selected then do nothing
-        if not self.collection.get_selected():
-            return
-
-        # if at least one object is visible then do the disable
-        exit_flag = True
-        for obj in objects:
-            if obj.options['plot'] is True:
-                exit_flag = False
-                break
-
-        if exit_flag:
-            return
-
-        log.debug("Disabling plots ...")
-        self.inform.emit(_("Working ..."))
-        for obj in objects:
-            obj.options['plot'] = False
-        self.plots_updated.emit()
-
-    def toggle_plots(self, objects):
-        """
-        Toggle plots visibility
-        :param objects: list of Objects for which to be toggled the visibility
-        :return:
-        """
-
-        # if no objects selected then do nothing
-        if not self.collection.get_selected():
-            return
-
-        log.debug("Toggling plots ...")
-        self.inform.emit(_("Working ..."))
-        for obj in objects:
-            if obj.options['plot'] is False:
-                obj.options['plot'] = True
-            else:
-                obj.options['plot'] = False
-        self.plots_updated.emit()
-
-    def clear_plots(self):
-
-        objects = self.collection.get_list()
-
-        for obj in objects:
-            obj.clear(obj == objects[-1])
-
-        # Clear pool to free memory
-        self.clear_pool()
-
-    def generate_cnc_job(self, objects):
-        self.report_usage("generate_cnc_job()")
-
-        # for obj in objects:
-        #     obj.generatecncjob()
-        for obj in objects:
-            obj.on_generatecnc_button_click()
-
-    def save_project(self, filename, quit=False):
-        """
-        Saves the current project to the specified file.
-
-        :param filename: Name of the file in which to save.
-        :type filename: str
-        :return: None
-        """
-        self.log.debug("save_project()")
-        self.save_in_progress = True
-
-        with self.proc_container.new(_("Saving FlatCAM Project")) as proc:
-            # Capture the latest changes
-            # Current object
-            try:
-                self.collection.get_active().read_form()
-            except:
-                self.log.debug("There was no active object")
-                pass
-            # Project options
-            self.options_read_form()
-
-            # Serialize the whole project
-            d = {"objs": [obj.to_dict() for obj in self.collection.get_list()],
-                 "options": self.options,
-                 "version": self.version}
-
-            if self.defaults["global_save_compressed"] is True:
-                with lzma.open(filename, "w", preset=int(self.defaults['global_compression_level'])) as f:
-                    g = json.dumps(d, default=to_dict, indent=2, sort_keys=True).encode('utf-8')
-                    # # Write
-                    f.write(g)
-                self.inform.emit(_("[success] Project saved to: %s") % filename)
-            else:
-                # Open file
-                try:
-                    f = open(filename, 'w')
-                except IOError:
-                    App.log.error("Failed to open file for saving: %s", filename)
-                    return
-
-                # Write
-                json.dump(d, f, default=to_dict, indent=2, sort_keys=True)
-                f.close()
-
-                # verification of the saved project
-                # Open and parse
-                try:
-                    saved_f = open(filename, 'r')
-                except IOError:
-                    self.inform.emit(_("[ERROR_NOTCL] Failed to verify project file: %s. Retry to save it.") % filename)
-                    return
-
-                try:
-                    saved_d = json.load(saved_f, object_hook=dict2obj)
-                except:
-                    self.inform.emit(
-                        _("[ERROR_NOTCL] Failed to parse saved project file: %s. Retry to save it.") % filename)
-                    f.close()
-                    return
-                saved_f.close()
-
-                if 'version' in saved_d:
-                    self.inform.emit(_("[success] Project saved to: %s") % filename)
-                else:
-                    self.inform.emit(_("[ERROR_NOTCL] Failed to save project file: %s. Retry to save it.") % filename)
-
-            # if quit:
-                # t = threading.Thread(target=lambda: self.check_project_file_size(1, filename=filename))
-                # t.start()
-            self.start_delayed_quit(delay=500, filename=filename, quit=quit)
-
-    def start_delayed_quit(self, delay, filename, quit=None):
-        """
-
-        :param delay:       period of checking if project file size is more than zero; in seconds
-        :param filename:    the name of the project file to be checked periodically for size more than zero
-        :return:
-        """
-        to_quit = quit
-        self.save_timer = QtCore.QTimer()
-        self.save_timer.setInterval(delay)
-        self.save_timer.timeout.connect(lambda : self.check_project_file_size(filename=filename, quit=to_quit))
-        self.save_timer.start()
-
-    def check_project_file_size(self, filename, quit=None):
-        """
-
-        :param filename: the name of the project file to be checked periodically for size more than zero
-        :return:
-        """
-
-        try:
-            if os.stat(filename).st_size > 0:
-                self.save_in_progress = False
-                self.save_timer.stop()
-                if quit:
-                    self.app_quit.emit()
-        except Exception:
-            traceback.print_exc()
-
-    def on_options_app2project(self):
-        """
-        Callback for Options->Transfer Options->App=>Project. Copies options
-        from application defaults to project defaults.
-
-        :return: None
-        """
-
-        self.report_usage("on_options_app2project")
-
-        self.defaults_read_form()
-        self.options.update(self.defaults)
-        self.options_write_form()
-
-    def on_options_project2app(self):
-        """
-        Callback for Options->Transfer Options->Project=>App. Copies options
-        from project defaults to application defaults.
-
-        :return: None
-        """
-
-        self.report_usage("on_options_project2app")
-
-        self.options_read_form()
-        self.defaults.update(self.options)
-        self.defaults_write_form()
-
-    def on_options_project2object(self):
-        """
-        Callback for Options->Transfer Options->Project=>Object. Copies options
-        from project defaults to the currently selected object.
-
-        :return: None
-        """
-
-        self.report_usage("on_options_project2object")
-
-        self.options_read_form()
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected."))
-            return
-        for option in self.options:
-            if option.find(obj.kind + "_") == 0:
-                oname = option[len(obj.kind) + 1:]
-                obj.options[oname] = self.options[option]
-        obj.to_form()  # Update UI
-
-    def on_options_object2project(self):
-        """
-        Callback for Options->Transfer Options->Object=>Project. Copies options
-        from the currently selected object to project defaults.
-
-        :return: None
-        """
-
-        self.report_usage("on_options_object2project")
-
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected."))
-            return
-        obj.read_form()
-        for option in obj.options:
-            if option in ['name']:  # TODO: Handle this better...
-                continue
-            self.options[obj.kind + "_" + option] = obj.options[option]
-        self.options_write_form()
-
-    def on_options_object2app(self):
-        """
-        Callback for Options->Transfer Options->Object=>App. Copies options
-        from the currently selected object to application defaults.
-
-        :return: None
-        """
-
-        self.report_usage("on_options_object2app")
-
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected."))
-            return
-        obj.read_form()
-        for option in obj.options:
-            if option in ['name']:  # TODO: Handle this better...
-                continue
-            self.defaults[obj.kind + "_" + option] = obj.options[option]
-        self.defaults_write_form()
-
-    def on_options_app2object(self):
-        """
-        Callback for Options->Transfer Options->App=>Object. Copies options
-        from application defaults to the currently selected object.
-
-        :return: None
-        """
-
-        self.report_usage("on_options_app2object")
-
-        self.defaults_read_form()
-        obj = self.collection.get_active()
-        if obj is None:
-            self.inform.emit(_("[WARNING_NOTCL] No object selected."))
-            return
-        for option in self.defaults:
-            if option.find(obj.kind + "_") == 0:
-                oname = option[len(obj.kind) + 1:]
-                obj.options[oname] = self.defaults[option]
-        obj.to_form()  # Update UI
-
-# end of file

+ 0 - 48
FlatCAMCommon.py

@@ -1,48 +0,0 @@
-# ########################################################## ##
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
-# MIT Licence                                              #
-# ########################################################## ##
-
-
-class LoudDict(dict):
-    """
-    A Dictionary with a callback for
-    item changes.
-    """
-
-    def __init__(self, *args, **kwargs):
-        dict.__init__(self, *args, **kwargs)
-        self.callback = lambda x: None
-
-    def __setitem__(self, key, value):
-        """
-        Overridden __setitem__ method. Will emit 'changed(QString)'
-        if the item was changed, with key as parameter.
-        """
-        if key in self and self.__getitem__(key) == value:
-            return
-
-        dict.__setitem__(self, key, value)
-        self.callback(key)
-
-    def update(self, *args, **kwargs):
-        if len(args) > 1:
-            raise TypeError("update expected at most 1 arguments, got %d" % len(args))
-        other = dict(*args, **kwargs)
-        for key in other:
-            self[key] = other[key]
-
-    def set_change_callback(self, callback):
-        """
-        Assigns a function as callback on item change. The callback
-        will receive the key of the object that was changed.
-
-        :param callback: Function to call on item change.
-        :type callback: func
-        :return: None
-        """
-
-        self.callback = callback

+ 0 - 6017
FlatCAMObj.py

@@ -1,6017 +0,0 @@
-# ########################################################## ##
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
-# MIT Licence                                              #
-# ########################################################## ##
-
-import copy
-import inspect  # TODO: For debugging only.
-from datetime import datetime
-
-from flatcamGUI.ObjectUI import *
-from FlatCAMCommon import LoudDict
-from camlib import *
-import itertools
-
-import gettext
-import FlatCAMTranslation as fcTranslate
-import builtins
-
-fcTranslate.apply_language('strings')
-if '_' not in builtins.__dict__:
-    _ = gettext.gettext
-
-
-# Interrupts plotting process if FlatCAMObj has been deleted
-class ObjectDeleted(Exception):
-    pass
-
-
-class ValidationError(Exception):
-    def __init__(self, message, errors):
-        super().__init__(message)
-
-        self.errors = errors
-
-# #######################################
-# #            FlatCAMObj              ##
-# #######################################
-
-
-class FlatCAMObj(QtCore.QObject):
-    """
-    Base type of objects handled in FlatCAM. These become interactive
-    in the GUI, can be plotted, and their options can be modified
-    by the user in their respective forms.
-    """
-
-    # Instance of the application to which these are related.
-    # The app should set this value.
-    app = None
-
-    def __init__(self, name):
-        """
-        Constructor.
-
-        :param name: Name of the object given by the user.
-        :return: FlatCAMObj
-        """
-        QtCore.QObject.__init__(self)
-
-        # View
-        self.ui = None
-
-        self.options = LoudDict(name=name)
-        self.options.set_change_callback(self.on_options_change)
-
-        self.form_fields = {}
-
-        # store here the default data for Geometry Data
-        self.default_data = {}
-
-        self.kind = None  # Override with proper name
-
-        # self.shapes = ShapeCollection(parent=self.app.plotcanvas.vispy_canvas.view.scene)
-        self.shapes = self.app.plotcanvas.new_shape_group()
-
-        # self.mark_shapes = self.app.plotcanvas.new_shape_collection(layers=2)
-        self.mark_shapes = {}
-
-        self.item = None  # Link with project view item
-
-        self.muted_ui = False
-        self.deleted = False
-
-        try:
-            self._drawing_tolerance = float(self.app.defaults["global_tolerance"]) if \
-                self.app.defaults["global_tolerance"] else 0.01
-        except ValueError:
-            self._drawing_tolerance = 0.01
-
-        self.isHovering = False
-        self.notHovering = True
-
-        # self.units = 'IN'
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        # assert isinstance(self.ui, ObjectUI)
-        # self.ui.name_entry.returnPressed.connect(self.on_name_activate)
-        # self.ui.offset_button.clicked.connect(self.on_offset_button_click)
-        # self.ui.scale_button.clicked.connect(self.on_scale_button_click)
-
-    def __del__(self):
-        pass
-
-    def __str__(self):
-        return "<FlatCAMObj({:12s}): {:20s}>".format(self.kind, self.options["name"])
-
-    def from_dict(self, d):
-        """
-        This supersedes ``from_dict`` in derived classes. Derived classes
-        must inherit from FlatCAMObj first, then from derivatives of Geometry.
-
-        ``self.options`` is only updated, not overwritten. This ensures that
-        options set by the app do not vanish when reading the objects
-        from a project file.
-
-        :param d: Dictionary with attributes to set.
-        :return: None
-        """
-
-        for attr in self.ser_attrs:
-
-            if attr == 'options':
-                self.options.update(d[attr])
-            else:
-                try:
-                    setattr(self, attr, d[attr])
-                except KeyError:
-                    log.debug("FlatCAMObj.from_dict() --> KeyError: %s. "
-                              "Means that we are loading an old project that don't"
-                              "have all attributes in the latest FlatCAM." % str(attr))
-                    pass
-
-    def on_options_change(self, key):
-        # Update form on programmatically options change
-        self.set_form_item(key)
-
-        # Set object visibility
-        if key == 'plot':
-            self.visible = self.options['plot']
-
-        self.optionChanged.emit(key)
-
-    def set_ui(self, ui):
-        self.ui = ui
-
-        self.form_fields = {"name": self.ui.name_entry}
-
-        assert isinstance(self.ui, ObjectUI)
-        self.ui.name_entry.returnPressed.connect(self.on_name_activate)
-        self.ui.offset_button.clicked.connect(self.on_offset_button_click)
-        self.ui.scale_button.clicked.connect(self.on_scale_button_click)
-
-        self.ui.offsetvector_entry.returnPressed.connect(self.on_offset_button_click)
-        self.ui.scale_entry.returnPressed.connect(self.on_scale_button_click)
-        # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
-
-    def build_ui(self):
-        """
-        Sets up the UI/form for this object. Show the UI
-        in the App.
-
-        :return: None
-        :rtype: None
-        """
-
-        self.muted_ui = True
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.build_ui()")
-
-        # Remove anything else in the box
-        # box_children = self.app.ui.notebook.selected_contents.get_children()
-        # for child in box_children:
-        #     self.app.ui.notebook.selected_contents.remove(child)
-        # while self.app.ui.selected_layout.count():
-        #     self.app.ui.selected_layout.takeAt(0)
-
-        # Put in the UI
-        # box_selected.pack_start(sw, True, True, 0)
-        # self.app.ui.notebook.selected_contents.add(self.ui)
-        # self.app.ui.selected_layout.addWidget(self.ui)
-        try:
-            self.app.ui.selected_scroll_area.takeWidget()
-        except Exception as e:
-            self.app.log.debug("FlatCAMObj.build_ui() --> Nothing to remove: %s" % str(e))
-        self.app.ui.selected_scroll_area.setWidget(self.ui)
-
-        self.muted_ui = False
-
-    def on_name_activate(self, silent=None):
-        old_name = copy(self.options["name"])
-        new_name = self.ui.name_entry.get_value()
-
-        if new_name != old_name:
-            # update the SHELL auto-completer model data
-            try:
-                self.app.myKeywords.remove(old_name)
-                self.app.myKeywords.append(new_name)
-                self.app.shell._edit.set_model_data(self.app.myKeywords)
-                self.app.ui.code_editor.set_model_data(self.app.myKeywords)
-            except Exception as e:
-                log.debug("on_name_activate() --> Could not remove the old object name from auto-completer model list")
-
-            self.options["name"] = self.ui.name_entry.get_value()
-            self.default_data["name"] = self.ui.name_entry.get_value()
-            self.app.collection.update_view()
-            if silent:
-                self.app.inform.emit(_("[success] Name changed from {old} to {new}").format(old=old_name, new=new_name))
-
-    def on_offset_button_click(self):
-        self.app.report_usage("obj_on_offset_button")
-
-        self.read_form()
-        vector_val = self.ui.offsetvector_entry.get_value()
-        self.offset(vector_val)
-        self.plot()
-        self.app.object_changed.emit(self)
-
-    def on_scale_button_click(self):
-        self.app.report_usage("obj_on_scale_button")
-        self.read_form()
-        factor = self.ui.scale_entry.get_value()
-        self.scale(factor)
-        self.plot()
-        self.app.object_changed.emit(self)
-
-    def on_skew_button_click(self):
-        self.app.report_usage("obj_on_skew_button")
-        self.read_form()
-        x_angle = self.ui.xangle_entry.get_value()
-        y_angle = self.ui.yangle_entry.get_value()
-        self.skew(x_angle, y_angle)
-        self.plot()
-        self.app.object_changed.emit(self)
-
-    def to_form(self):
-        """
-        Copies options to the UI form.
-
-        :return: None
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.to_form()")
-        for option in self.options:
-            try:
-                self.set_form_item(option)
-            except Exception as e:
-                self.app.log.warning("Unexpected error:", sys.exc_info())
-
-    def read_form(self):
-        """
-        Reads form into ``self.options``.
-
-        :return: None
-        :rtype: None
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> FlatCAMObj.read_form()")
-        for option in self.options:
-            try:
-                self.read_form_item(option)
-            except Exception as e:
-                self.app.log.warning("Unexpected error:", sys.exc_info())
-
-    def set_form_item(self, option):
-        """
-        Copies the specified option to the UI form.
-
-        :param option: Name of the option (Key in ``self.options``).
-        :type option: str
-        :return: None
-        """
-
-        try:
-            self.form_fields[option].set_value(self.options[option])
-        except KeyError:
-            # self.app.log.warn("Tried to set an option or field that does not exist: %s" % option)
-            pass
-
-    def read_form_item(self, option):
-        """
-        Reads the specified option from the UI form into ``self.options``.
-
-        :param option: Name of the option.
-        :type option: str
-        :return: None
-        """
-
-        try:
-            self.options[option] = self.form_fields[option].get_value()
-        except KeyError:
-            self.app.log.warning("Failed to read option from field: %s" % option)
-
-    def plot(self):
-        """
-        Plot this object (Extend this method to implement the actual plotting).
-        Call this in descendants before doing the plotting.
-
-        :return: Whether to continue plotting or not depending on the "plot" option.
-        :rtype: bool
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMObj.plot()")
-
-        if self.deleted:
-            return False
-
-        self.clear()
-        return True
-
-    def serialize(self):
-        """
-        Returns a representation of the object as a dictionary so
-        it can be later exported as JSON. Override this method.
-
-        :return: Dictionary representing the object
-        :rtype: dict
-        """
-        return
-
-    def deserialize(self, obj_dict):
-        """
-        Re-builds an object from its serialized version.
-
-        :param obj_dict: Dictionary representing a FlatCAMObj
-        :type obj_dict: dict
-        :return: None
-        """
-        return
-
-    def add_shape(self, **kwargs):
-        if self.deleted:
-            raise ObjectDeleted()
-        else:
-            key = self.shapes.add(tolerance=self.drawing_tolerance, **kwargs)
-        return key
-
-    def add_mark_shape(self, apid, **kwargs):
-        if self.deleted:
-            raise ObjectDeleted()
-        else:
-            key = self.mark_shapes[apid].add(tolerance=self.drawing_tolerance, **kwargs)
-        return key
-
-    @property
-    def visible(self):
-        return self.shapes.visible
-
-    @visible.setter
-    def visible(self, value, threaded=False):
-        log.debug("FlatCAMObj.visible()")
-
-        def worker_task(app_obj):
-            app_obj.shapes.visible = value
-
-            # Not all object types has annotations
-            try:
-                app_obj.annotation.visible = value
-            except Exception as e:
-                pass
-
-        if threaded is False:
-            worker_task(self)
-        else:
-            self.app.worker_task.emit({'fcn': worker_task, 'params': [self]})
-
-    @property
-    def drawing_tolerance(self):
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-        tol = self._drawing_tolerance if self.units == 'MM' or not self.units else self._drawing_tolerance / 25.4
-        return tol
-
-    @drawing_tolerance.setter
-    def drawing_tolerance(self, value):
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-        self._drawing_tolerance = value if self.units == 'MM' or not self.units else value / 25.4
-
-    def clear(self, update=False):
-        self.shapes.clear(update)
-
-        # Not all object types has annotations
-        try:
-            self.annotation.clear(update)
-        except AttributeError:
-            pass
-
-    def delete(self):
-        # Free resources
-        del self.ui
-        del self.options
-
-        # Set flag
-        self.deleted = True
-
-
-class FlatCAMGerber(FlatCAMObj, Gerber):
-    """
-    Represents Gerber code.
-    """
-    optionChanged = QtCore.pyqtSignal(str)
-    replotApertures = QtCore.pyqtSignal()
-
-    ui_type = GerberObjectUI
-
-    def merge(self, grb_list, grb_final):
-        """
-        Merges the geometry of objects in geo_list into
-        the geometry of geo_final.
-
-        :param grb_list: List of FlatCAMGerber Objects to join.
-        :param grb_final: Destination FlatCAMGeometry object.
-        :return: None
-        """
-
-        if grb_final.solid_geometry is None:
-            grb_final.solid_geometry = []
-            grb_final.follow_geometry = []
-
-        if not grb_final.apertures:
-            grb_final.apertures = {}
-
-        if type(grb_final.solid_geometry) is not list:
-            grb_final.solid_geometry = [grb_final.solid_geometry]
-            grb_final.follow_geometry = [grb_final.follow_geometry]
-
-        for grb in grb_list:
-
-            # Expand lists
-            if type(grb) is list:
-                FlatCAMGerber.merge(grb, grb_final)
-            else:   # If not list, just append
-                for option in grb.options:
-                    if option is not 'name':
-                        try:
-                            grb_final.options[option] = grb.options[option]
-                        except KeyError:
-                            log.warning("Failed to copy option.", option)
-
-                try:
-                    for geos in grb.solid_geometry:
-                        grb_final.solid_geometry.append(geos)
-                        grb_final.follow_geometry.append(geos)
-                except TypeError:
-                    grb_final.solid_geometry.append(grb.solid_geometry)
-                    grb_final.follow_geometry.append(grb.solid_geometry)
-
-                for ap in grb.apertures:
-                    if ap not in grb_final.apertures:
-                        grb_final.apertures[ap] = grb.apertures[ap]
-                    else:
-                        # create a list of integers out of the grb.apertures keys and find the max of that value
-                        # then, the aperture duplicate is assigned an id value incremented with 1,
-                        # and finally made string because the apertures dict keys are strings
-                        max_ap = str(max([int(k) for k in grb_final.apertures.keys()]) + 1)
-                        grb_final.apertures[max_ap] = {}
-                        grb_final.apertures[max_ap]['geometry'] = []
-
-                        for k, v in grb.apertures[ap].items():
-                            grb_final.apertures[max_ap][k] = deepcopy(v)
-
-        grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry)
-        grb_final.follow_geometry = MultiPolygon(grb_final.follow_geometry)
-
-    def __init__(self, name):
-        Gerber.__init__(self, steps_per_circle=int(self.app.defaults["gerber_circle_steps"]))
-        FlatCAMObj.__init__(self, name)
-
-        self.kind = "gerber"
-
-        # The 'name' is already in self.options from FlatCAMObj
-        # Automatically updates the UI
-        self.options.update({
-            "plot": True,
-            "multicolored": False,
-            "solid": False,
-            "isotooldia": 0.016,
-            "isopasses": 1,
-            "isooverlap": 0.15,
-            "milling_type": "cl",
-            "combine_passes": True,
-            "noncoppermargin": 0.0,
-            "noncopperrounded": False,
-            "bboxmargin": 0.0,
-            "bboxrounded": False,
-            "aperture_display": False,
-            "follow": False
-        })
-
-        # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors)
-        self.iso_type = 2
-
-        self.multigeo = False
-
-        self.follow = False
-
-        self.apertures_row = 0
-
-        # store the source file here
-        self.source_file = ""
-
-        # list of rows with apertures plotted
-        self.marked_rows = []
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from predecessors.
-        self.ser_attrs += ['options', 'kind']
-
-    def set_ui(self, ui):
-        """
-        Maps options with GUI inputs.
-        Connects GUI events to methods.
-
-        :param ui: GUI object.
-        :type ui: GerberObjectUI
-        :return: None
-        """
-        FlatCAMObj.set_ui(self, ui)
-        FlatCAMApp.App.log.debug("FlatCAMGerber.set_ui()")
-
-        self.replotApertures.connect(self.on_mark_cb_click_table)
-
-        self.form_fields.update({
-            "plot": self.ui.plot_cb,
-            "multicolored": self.ui.multicolored_cb,
-            "solid": self.ui.solid_cb,
-            "isotooldia": self.ui.iso_tool_dia_entry,
-            "isopasses": self.ui.iso_width_entry,
-            "isooverlap": self.ui.iso_overlap_entry,
-            "milling_type": self.ui.milling_type_radio,
-            "combine_passes": self.ui.combine_passes_cb,
-            "noncoppermargin": self.ui.noncopper_margin_entry,
-            "noncopperrounded": self.ui.noncopper_rounded_cb,
-            "bboxmargin": self.ui.bbmargin_entry,
-            "bboxrounded": self.ui.bbrounded_cb,
-            "aperture_display": self.ui.aperture_table_visibility_cb,
-            "follow": self.ui.follow_cb
-        })
-
-        # Fill form fields only on object create
-        self.to_form()
-
-        assert isinstance(self.ui, GerberObjectUI)
-        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
-        self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
-        self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
-        self.ui.generate_ext_iso_button.clicked.connect(self.on_ext_iso_button_click)
-        self.ui.generate_int_iso_button.clicked.connect(self.on_int_iso_button_click)
-        self.ui.generate_iso_button.clicked.connect(self.on_iso_button_click)
-        self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run)
-        self.ui.generate_cutout_button.clicked.connect(self.app.cutout_tool.run)
-        self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
-        self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
-        self.ui.aperture_table_visibility_cb.stateChanged.connect(self.on_aperture_table_visibility_change)
-        self.ui.follow_cb.stateChanged.connect(self.on_follow_cb_click)
-
-        # Show/Hide Advanced Options
-        if self.app.defaults["global_app_level"] == 'b':
-            self.ui.level.setText(_(
-                '<span style="color:green;"><b>Basic</b></span>'
-            ))
-            self.ui.apertures_table_label.hide()
-            self.ui.aperture_table_visibility_cb.hide()
-            self.ui.milling_type_label.hide()
-            self.ui.milling_type_radio.hide()
-            self.ui.generate_ext_iso_button.hide()
-            self.ui.generate_int_iso_button.hide()
-            self.ui.follow_cb.hide()
-            self.ui.padding_area_label.show()
-        else:
-            self.ui.level.setText(_(
-                '<span style="color:red;"><b>Advanced</b></span>'
-            ))
-            self.ui.padding_area_label.hide()
-
-        # add the shapes storage for marking apertures
-        for ap_code in self.apertures:
-            self.mark_shapes[ap_code] = self.app.plotcanvas.new_shape_collection(layers=2)
-
-        # set initial state of the aperture table and associated widgets
-        self.on_aperture_table_visibility_change()
-
-        self.build_ui()
-
-    def build_ui(self):
-        FlatCAMObj.build_ui(self)
-
-        try:
-            # if connected, disconnect the signal from the slot on item_changed as it creates issues
-            self.ui.apertures_table.itemChanged.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        self.apertures_row = 0
-        aper_no = self.apertures_row + 1
-        sort = []
-        for k, v in list(self.apertures.items()):
-            sort.append(int(k))
-        sorted_apertures = sorted(sort)
-
-        # sort = []
-        # for k, v in list(self.aperture_macros.items()):
-        #     sort.append(k)
-        # sorted_macros = sorted(sort)
-
-        # n = len(sorted_apertures) + len(sorted_macros)
-        n = len(sorted_apertures)
-        self.ui.apertures_table.setRowCount(n)
-
-        for ap_code in sorted_apertures:
-            ap_code = str(ap_code)
-
-            ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
-            ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.ui.apertures_table.setItem(self.apertures_row, 0, ap_id_item)  # Tool name/id
-
-            ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
-            ap_code_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            ap_type_item = QtWidgets.QTableWidgetItem(str(self.apertures[ap_code]['type']))
-            ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            if str(self.apertures[ap_code]['type']) == 'R' or str(self.apertures[ap_code]['type']) == 'O':
-                ap_dim_item = QtWidgets.QTableWidgetItem(
-                    '%.4f, %.4f' % (self.apertures[ap_code]['width'] * self.file_units_factor,
-                                    self.apertures[ap_code]['height'] * self.file_units_factor
-                                    )
-                )
-                ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
-            elif str(self.apertures[ap_code]['type']) == 'P':
-                ap_dim_item = QtWidgets.QTableWidgetItem(
-                    '%.4f, %.4f' % (self.apertures[ap_code]['diam'] * self.file_units_factor,
-                                    self.apertures[ap_code]['nVertices'] * self.file_units_factor)
-                )
-                ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
-            else:
-                ap_dim_item = QtWidgets.QTableWidgetItem('')
-                ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            try:
-                if self.apertures[ap_code]['size'] is not None:
-                    ap_size_item = QtWidgets.QTableWidgetItem('%.4f' %
-                                                              float(self.apertures[ap_code]['size'] *
-                                                                    self.file_units_factor))
-                else:
-                    ap_size_item = QtWidgets.QTableWidgetItem('')
-            except KeyError:
-                ap_size_item = QtWidgets.QTableWidgetItem('')
-            ap_size_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            mark_item = FCCheckBox()
-            mark_item.setLayoutDirection(QtCore.Qt.RightToLeft)
-            # if self.ui.aperture_table_visibility_cb.isChecked():
-            #     mark_item.setChecked(True)
-
-            self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
-            self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
-            self.ui.apertures_table.setItem(self.apertures_row, 3, ap_size_item)   # Aperture Dimensions
-            self.ui.apertures_table.setItem(self.apertures_row, 4, ap_dim_item)   # Aperture Dimensions
-
-            self.ui.apertures_table.setCellWidget(self.apertures_row, 5, mark_item)
-
-            self.apertures_row += 1
-
-        # for ap_code in sorted_macros:
-        #     ap_code = str(ap_code)
-        #
-        #     ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
-        #     ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-        #     self.ui.apertures_table.setItem(self.apertures_row, 0, ap_id_item)  # Tool name/id
-        #
-        #     ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
-        #
-        #     ap_type_item = QtWidgets.QTableWidgetItem('AM')
-        #     ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
-        #
-        #     mark_item = FCCheckBox()
-        #     mark_item.setLayoutDirection(QtCore.Qt.RightToLeft)
-        #     # if self.ui.aperture_table_visibility_cb.isChecked():
-        #     #     mark_item.setChecked(True)
-        #
-        #     self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
-        #     self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
-        #     self.ui.apertures_table.setCellWidget(self.apertures_row, 5, mark_item)
-        #
-        #     self.apertures_row += 1
-
-        self.ui.apertures_table.selectColumn(0)
-        self.ui.apertures_table.resizeColumnsToContents()
-        self.ui.apertures_table.resizeRowsToContents()
-
-        vertical_header = self.ui.apertures_table.verticalHeader()
-        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
-        vertical_header.hide()
-        self.ui.apertures_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-
-        horizontal_header = self.ui.apertures_table.horizontalHeader()
-        horizontal_header.setMinimumSectionSize(10)
-        horizontal_header.setDefaultSectionSize(70)
-        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(0, 27)
-        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(4,  QtWidgets.QHeaderView.Stretch)
-        horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(5, 17)
-        self.ui.apertures_table.setColumnWidth(5, 17)
-
-        self.ui.apertures_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-        self.ui.apertures_table.setSortingEnabled(False)
-        self.ui.apertures_table.setMinimumHeight(self.ui.apertures_table.getHeight())
-        self.ui.apertures_table.setMaximumHeight(self.ui.apertures_table.getHeight())
-
-        # update the 'mark' checkboxes state according with what is stored in the self.marked_rows list
-        if self.marked_rows:
-            for row in range(self.ui.apertures_table.rowCount()):
-                try:
-                    self.ui.apertures_table.cellWidget(row, 5).set_value(self.marked_rows[row])
-                except IndexError:
-                    pass
-
-        self.ui_connect()
-
-    def ui_connect(self):
-        for row in range(self.ui.apertures_table.rowCount()):
-            self.ui.apertures_table.cellWidget(row, 5).clicked.connect(self.on_mark_cb_click_table)
-
-        self.ui.mark_all_cb.clicked.connect(self.on_mark_all_click)
-
-    def ui_disconnect(self):
-        for row in range(self.ui.apertures_table.rowCount()):
-            try:
-                self.ui.apertures_table.cellWidget(row, 5).clicked.disconnect()
-            except (TypeError, AttributeError):
-                pass
-
-        try:
-            self.ui.mark_all_cb.clicked.disconnect(self.on_mark_all_click)
-        except (TypeError, AttributeError):
-            pass
-
-    def on_generatenoncopper_button_click(self, *args):
-        self.app.report_usage("gerber_on_generatenoncopper_button")
-
-        self.read_form()
-        name = self.options["name"] + "_noncopper"
-
-        def geo_init(geo_obj, app_obj):
-            assert isinstance(geo_obj, FlatCAMGeometry)
-            bounding_box = self.solid_geometry.envelope.buffer(float(self.options["noncoppermargin"]))
-            if not self.options["noncopperrounded"]:
-                bounding_box = bounding_box.envelope
-            non_copper = bounding_box.difference(self.solid_geometry)
-            geo_obj.solid_geometry = non_copper
-
-        # TODO: Check for None
-        self.app.new_object("geometry", name, geo_init)
-
-    def on_generatebb_button_click(self, *args):
-        self.app.report_usage("gerber_on_generatebb_button")
-        self.read_form()
-        name = self.options["name"] + "_bbox"
-
-        def geo_init(geo_obj, app_obj):
-            assert isinstance(geo_obj, FlatCAMGeometry)
-            # Bounding box with rounded corners
-            bounding_box = self.solid_geometry.envelope.buffer(float(self.options["bboxmargin"]))
-            if not self.options["bboxrounded"]:  # Remove rounded corners
-                bounding_box = bounding_box.envelope
-            geo_obj.solid_geometry = bounding_box
-
-        self.app.new_object("geometry", name, geo_init)
-
-    def on_ext_iso_button_click(self, *args):
-
-        if self.ui.follow_cb.get_value() is True:
-            obj = self.app.collection.get_active()
-            obj.follow()
-            # in the end toggle the visibility of the origin object so we can see the generated Geometry
-            obj.ui.plot_cb.toggle()
-        else:
-            self.app.report_usage("gerber_on_iso_button")
-            self.read_form()
-            self.isolate(iso_type=0)
-
-    def on_int_iso_button_click(self, *args):
-
-        if self.ui.follow_cb.get_value() is True:
-            obj = self.app.collection.get_active()
-            obj.follow()
-            # in the end toggle the visibility of the origin object so we can see the generated Geometry
-            obj.ui.plot_cb.toggle()
-        else:
-            self.app.report_usage("gerber_on_iso_button")
-            self.read_form()
-            self.isolate(iso_type=1)
-
-    def on_iso_button_click(self, *args):
-
-        if self.ui.follow_cb.get_value() is True:
-            obj = self.app.collection.get_active()
-            obj.follow_geo()
-            # in the end toggle the visibility of the origin object so we can see the generated Geometry
-            obj.ui.plot_cb.toggle()
-        else:
-            self.app.report_usage("gerber_on_iso_button")
-            self.read_form()
-            self.isolate()
-
-    def follow_geo(self, outname=None):
-        """
-        Creates a geometry object "following" the gerber paths.
-
-        :return: None
-        """
-
-        # default_name = self.options["name"] + "_follow"
-        # follow_name = outname or default_name
-
-        if outname is None:
-            follow_name = self.options["name"] + "_follow"
-        else:
-            follow_name = outname
-
-        def follow_init(follow_obj, app):
-            # Propagate options
-            follow_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-            follow_obj.solid_geometry = self.follow_geometry
-
-        # TODO: Do something if this is None. Offer changing name?
-        try:
-            self.app.new_object("geometry", follow_name, follow_init)
-        except Exception as e:
-            return "Operation failed: %s" % str(e)
-
-    def isolate(self, iso_type=None, dia=None, passes=None, overlap=None,
-                outname=None, combine=None, milling_type=None, follow=None):
-        """
-        Creates an isolation routing geometry object in the project.
-
-        :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both
-        :param dia: Tool diameter
-        :param passes: Number of tool widths to cut
-        :param overlap: Overlap between passes in fraction of tool diameter
-        :param outname: Base name of the output object
-        :return: None
-        """
-        if dia is None:
-            dia = float(self.options["isotooldia"])
-        if passes is None:
-            passes = int(self.options["isopasses"])
-        if overlap is None:
-            overlap = float(self.options["isooverlap"])
-        if combine is None:
-            combine = self.options["combine_passes"]
-        else:
-            combine = bool(combine)
-        if milling_type is None:
-            milling_type = self.options["milling_type"]
-        if iso_type is None:
-            self.iso_type = 2
-        else:
-            self.iso_type = iso_type
-
-        base_name = self.options["name"] + "_iso"
-        base_name = outname or base_name
-
-        def generate_envelope(offset, invert, envelope_iso_type=2, follow=None):
-            # isolation_geometry produces an envelope that is going on the left of the geometry
-            # (the copper features). To leave the least amount of burrs on the features
-            # the tool needs to travel on the right side of the features (this is called conventional milling)
-            # the first pass is the one cutting all of the features, so it needs to be reversed
-            # the other passes overlap preceding ones and cut the left over copper. It is better for them
-            # to cut on the right side of the left over copper i.e on the left side of the features.
-            try:
-                geom = self.isolation_geometry(offset, iso_type=envelope_iso_type, follow=follow)
-            except Exception as e:
-                log.debug('FlatCAMGerber.isolate().generate_envelope() --> %s' % str(e))
-                return 'fail'
-
-            if invert:
-                try:
-                    if type(geom) is MultiPolygon:
-                        pl = []
-                        for p in geom:
-                            if p is not None:
-                                pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
-                        geom = MultiPolygon(pl)
-                    elif type(geom) is Polygon and geom is not None:
-                        geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
-                    else:
-                        log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> Unexpected Geometry")
-                except Exception as e:
-                    log.debug("FlatCAMGerber.isolate().generate_envelope() Error --> %s" % str(e))
-                    return 'fail'
-            return geom
-
-        if float(self.options["isotooldia"]) < 0:
-            self.options["isotooldia"] = -self.options["isotooldia"]
-
-        if combine:
-            if self.iso_type == 0:
-                iso_name = self.options["name"] + "_ext_iso"
-            elif self.iso_type == 1:
-                iso_name = self.options["name"] + "_int_iso"
-            else:
-                iso_name = base_name
-
-            # TODO: This is ugly. Create way to pass data into init function.
-            def iso_init(geo_obj, app_obj):
-                # Propagate options
-                geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-                geo_obj.solid_geometry = []
-                for i in range(passes):
-                    iso_offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
-
-                    # if milling type is climb then the move is counter-clockwise around features
-                    if milling_type == 'cl':
-                        # geom = generate_envelope (offset, i == 0)
-                        geom = generate_envelope(iso_offset, 1, envelope_iso_type=self.iso_type, follow=follow)
-                    else:
-                        geom = generate_envelope(iso_offset, 0, envelope_iso_type=self.iso_type, follow=follow)
-                    if geom == 'fail':
-                        app_obj.inform.emit(_("[ERROR_NOTCL] Isolation geometry could not be generated."))
-                        return 'fail'
-                    geo_obj.solid_geometry.append(geom)
-
-                    # store here the default data for Geometry Data
-                    default_data = {}
-                    default_data.update({
-                        "name": iso_name,
-                        "plot": self.app.defaults['geometry_plot'],
-                        "cutz": self.app.defaults['geometry_cutz'],
-                        "vtipdia": self.app.defaults['geometry_vtipdia'],
-                        "vtipangle": self.app.defaults['geometry_vtipangle'],
-                        "travelz": self.app.defaults['geometry_travelz'],
-                        "feedrate": self.app.defaults['geometry_feedrate'],
-                        "feedrate_z": self.app.defaults['geometry_feedrate_z'],
-                        "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
-                        "dwell": self.app.defaults['geometry_dwell'],
-                        "dwelltime": self.app.defaults['geometry_dwelltime'],
-                        "multidepth": self.app.defaults['geometry_multidepth'],
-                        "ppname_g": self.app.defaults['geometry_ppname_g'],
-                        "depthperpass": self.app.defaults['geometry_depthperpass'],
-                        "extracut": self.app.defaults['geometry_extracut'],
-                        "toolchange": self.app.defaults['geometry_toolchange'],
-                        "toolchangez": self.app.defaults['geometry_toolchangez'],
-                        "endz": self.app.defaults['geometry_endz'],
-                        "spindlespeed": self.app.defaults['geometry_spindlespeed'],
-                        "toolchangexy": self.app.defaults['geometry_toolchangexy'],
-                        "startz": self.app.defaults['geometry_startz']
-                    })
-
-                    geo_obj.tools = dict()
-                    geo_obj.tools['1'] = dict()
-                    geo_obj.tools.update({
-                        '1': {
-                            'tooldia': float(self.options["isotooldia"]),
-                            'offset': 'Path',
-                            'offset_value': 0.0,
-                            'type': _('Rough'),
-                            'tool_type': 'C1',
-                            'data': default_data,
-                            'solid_geometry': geo_obj.solid_geometry
-                        }
-                    })
-
-                # detect if solid_geometry is empty and this require list flattening which is "heavy"
-                # or just looking in the lists (they are one level depth) and if any is not empty
-                # proceed with object creation, if there are empty and the number of them is the length
-                # of the list then we have an empty solid_geometry which should raise a Custom Exception
-                empty_cnt = 0
-                if not isinstance(geo_obj.solid_geometry, list):
-                    geo_obj.solid_geometry = [geo_obj.solid_geometry]
-
-                for g in geo_obj.solid_geometry:
-                    if g:
-                        app_obj.inform.emit(_(
-                            "[success] Isolation geometry created: %s"
-                        ) % geo_obj.options["name"])
-                        break
-                    else:
-                        empty_cnt += 1
-
-                if empty_cnt == len(geo_obj.solid_geometry):
-                    raise ValidationError("Empty Geometry", None)
-                geo_obj.multigeo = True
-
-            # TODO: Do something if this is None. Offer changing name?
-            self.app.new_object("geometry", iso_name, iso_init)
-        else:
-            for i in range(passes):
-
-                offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
-                if passes > 1:
-                    if self.iso_type == 0:
-                        iso_name = self.options["name"] + "_ext_iso" + str(i + 1)
-                    elif self.iso_type == 1:
-                        iso_name = self.options["name"] + "_int_iso" + str(i + 1)
-                    else:
-                        iso_name = base_name + str(i + 1)
-                else:
-                    if self.iso_type == 0:
-                        iso_name = self.options["name"] + "_ext_iso"
-                    elif self.iso_type == 1:
-                        iso_name = self.options["name"] + "_int_iso"
-                    else:
-                        iso_name = base_name
-
-                # TODO: This is ugly. Create way to pass data into init function.
-                def iso_init(geo_obj, app_obj):
-                    # Propagate options
-                    geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-
-                    # if milling type is climb then the move is counter-clockwise around features
-                    if milling_type == 'cl':
-                        # geo_obj.solid_geometry = generate_envelope(offset, i == 0)
-                        geom = generate_envelope(offset, 1, envelope_iso_type=self.iso_type, follow=follow)
-                    else:
-                        geom = generate_envelope(offset, 0, envelope_iso_type=self.iso_type, follow=follow)
-                    if geom == 'fail':
-                        app_obj.inform.emit(_("[ERROR_NOTCL] Isolation geometry could not be generated."))
-                        return 'fail'
-
-                    geo_obj.solid_geometry = geom
-
-                    # detect if solid_geometry is empty and this require list flattening which is "heavy"
-                    # or just looking in the lists (they are one level depth) and if any is not empty
-                    # proceed with object creation, if there are empty and the number of them is the length
-                    # of the list then we have an empty solid_geometry which should raise a Custom Exception
-                    empty_cnt = 0
-                    if not isinstance(geo_obj.solid_geometry, list):
-                        geo_obj.solid_geometry = [geo_obj.solid_geometry]
-
-                    for g in geo_obj.solid_geometry:
-                        if g:
-                            app_obj.inform.emit(_(
-                                "[success] Isolation geometry created: %s"
-                            ) % geo_obj.options["name"])
-                            break
-                        else:
-                            empty_cnt += 1
-                    if empty_cnt == len(geo_obj.solid_geometry):
-                        raise ValidationError("Empty Geometry", None)
-                    geo_obj.multigeo = False
-
-                # TODO: Do something if this is None. Offer changing name?
-                self.app.new_object("geometry", iso_name, iso_init)
-
-    def on_plot_cb_click(self, *args):
-        if self.muted_ui:
-            return
-        self.read_form_item('plot')
-        self.plot()
-
-    def on_solid_cb_click(self, *args):
-        if self.muted_ui:
-            return
-        self.read_form_item('solid')
-        self.plot()
-
-    def on_multicolored_cb_click(self, *args):
-        if self.muted_ui:
-            return
-        self.read_form_item('multicolored')
-        self.plot()
-
-    def on_follow_cb_click(self):
-        if self.muted_ui:
-            return
-        self.plot()
-
-    def on_aperture_table_visibility_change(self):
-        if self.ui.aperture_table_visibility_cb.isChecked():
-            self.ui.apertures_table.setVisible(True)
-            for ap in self.mark_shapes:
-                self.mark_shapes[ap].enabled = True
-
-            self.ui.mark_all_cb.setVisible(True)
-            self.ui.mark_all_cb.setChecked(False)
-        else:
-            self.ui.apertures_table.setVisible(False)
-
-            self.ui.mark_all_cb.setVisible(False)
-
-            # on hide disable all mark plots
-            for row in range(self.ui.apertures_table.rowCount()):
-                self.ui.apertures_table.cellWidget(row, 5).set_value(False)
-            self.clear_plot_apertures()
-
-            for ap in self.mark_shapes:
-                self.mark_shapes[ap].enabled = False
-
-    def convert_units(self, units):
-        """
-        Converts the units of the object by scaling dimensions in all geometry
-        and options.
-
-        :param units: Units to which to convert the object: "IN" or "MM".
-        :type units: str
-        :return: None
-        :rtype: None
-        """
-
-        factor = Gerber.convert_units(self, units)
-
-        self.options['isotooldia'] = float(self.options['isotooldia']) * factor
-        self.options['bboxmargin'] = float(self.options['bboxmargin']) * factor
-
-    def plot(self, **kwargs):
-        """
-
-        :param kwargs: color and face_color
-        :return:
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot()")
-
-        # Does all the required setup and returns False
-        # if the 'ptint' option is set to False.
-        if not FlatCAMObj.plot(self):
-            return
-
-        if 'color' in kwargs:
-            color = kwargs['color']
-        else:
-            color = self.app.defaults['global_plot_line']
-
-        if 'face_color' in kwargs:
-            face_color = kwargs['face_color']
-        else:
-            face_color = self.app.defaults['global_plot_fill']
-
-        # if the Follow Geometry checkbox is checked then plot only the follow geometry
-        if self.ui.follow_cb.get_value():
-            geometry = self.follow_geometry
-        else:
-            geometry = self.solid_geometry
-
-        # Make sure geometry is iterable.
-        try:
-            __ = iter(geometry)
-        except TypeError:
-            geometry = [geometry]
-
-        def random_color():
-            color = np.random.rand(4)
-            color[3] = 1
-            return color
-
-        try:
-            if self.options["solid"]:
-                for g in geometry:
-                    if type(g) == Polygon or type(g) == LineString:
-                        self.add_shape(shape=g, color=color,
-                                       face_color=random_color() if self.options['multicolored']
-                                       else face_color, visible=self.options['plot'])
-                    elif type(g) == Point:
-                        pass
-                    else:
-                        try:
-                            for el in g:
-                                self.add_shape(shape=el, color=color,
-                                               face_color=random_color() if self.options['multicolored']
-                                               else face_color, visible=self.options['plot'])
-                        except TypeError:
-                            self.add_shape(shape=g, color=color,
-                                           face_color=random_color() if self.options['multicolored']
-                                           else face_color, visible=self.options['plot'])
-            else:
-                for g in geometry:
-                    if type(g) == Polygon or type(g) == LineString:
-                        self.add_shape(shape=g, color=random_color() if self.options['multicolored'] else 'black',
-                                       visible=self.options['plot'])
-                    elif type(g) == Point:
-                        pass
-                    else:
-                        for el in g:
-                            self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black',
-                                           visible=self.options['plot'])
-            self.shapes.redraw()
-        except (ObjectDeleted, AttributeError):
-            self.shapes.clear(update=True)
-
-    # experimental plot() when the solid_geometry is stored in the self.apertures
-    def plot_aperture(self, **kwargs):
-        """
-
-        :param kwargs: color and face_color
-        :return:
-        """
-
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> FlatCAMGerber.plot_aperture()")
-
-        # Does all the required setup and returns False
-        # if the 'ptint' option is set to False.
-        if not FlatCAMObj.plot(self):
-            return
-
-        # for marking apertures, line color and fill color are the same
-        if 'color' in kwargs:
-            color = kwargs['color']
-        else:
-            color = self.app.defaults['global_plot_fill']
-
-        if 'marked_aperture' not in kwargs:
-            return
-        else:
-            aperture_to_plot_mark = kwargs['marked_aperture']
-            if aperture_to_plot_mark is None:
-                return
-
-        if 'visible' not in kwargs:
-            visibility = True
-        else:
-            visibility = kwargs['visible']
-
-        with self.app.proc_container.new(_("Plotting Apertures")) as proc:
-            self.app.progress.emit(30)
-
-            def job_thread(app_obj):
-                self.app.progress.emit(30)
-                try:
-                    if aperture_to_plot_mark in self.apertures:
-                        for elem in self.apertures[aperture_to_plot_mark]['geometry']:
-                            if 'solid' in elem:
-                                geo = elem['solid']
-                                if type(geo) == Polygon or type(geo) == LineString:
-                                    self.add_mark_shape(apid=aperture_to_plot_mark, shape=geo, color=color,
-                                                        face_color=color, visible=visibility)
-                                else:
-                                    for el in geo:
-                                        self.add_mark_shape(apid=aperture_to_plot_mark, shape=el, color=color,
-                                                            face_color=color, visible=visibility)
-
-                    self.mark_shapes[aperture_to_plot_mark].redraw()
-                    self.app.progress.emit(100)
-
-                except (ObjectDeleted, AttributeError):
-                    self.clear_plot_apertures()
-
-            self.app.worker_task.emit({'fcn': job_thread, 'params': [self]})
-
-    def clear_plot_apertures(self, aperture='all'):
-        """
-
-        :param aperture: string; aperture for which to clear the mark shapes
-        :return:
-        """
-        if aperture == 'all':
-            for apid in self.apertures:
-                self.mark_shapes[apid].clear(update=True)
-        else:
-            self.mark_shapes[aperture].clear(update=True)
-
-    def clear_mark_all(self):
-        self.ui.mark_all_cb.set_value(False)
-        self.marked_rows[:] = []
-
-    def on_mark_cb_click_table(self):
-        """
-        Will mark aperture geometries on canvas or delete the markings depending on the checkbox state
-        :return:
-        """
-
-        self.ui_disconnect()
-        cw = self.sender()
-        try:
-            cw_index = self.ui.apertures_table.indexAt(cw.pos())
-            cw_row = cw_index.row()
-        except AttributeError:
-            cw_row = 0
-
-        self.marked_rows[:] = []
-
-        try:
-            aperture = self.ui.apertures_table.item(cw_row, 1).text()
-        except AttributeError:
-            return
-
-        if self.ui.apertures_table.cellWidget(cw_row, 5).isChecked():
-            self.marked_rows.append(True)
-            # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True)
-            self.plot_aperture(color=self.app.defaults['global_sel_draw_color'], marked_aperture=aperture, visible=True)
-            self.mark_shapes[aperture].redraw()
-        else:
-            self.marked_rows.append(False)
-            self.clear_plot_apertures(aperture=aperture)
-
-        # make sure that the Mark All is disabled if one of the row mark's are disabled and
-        # if all the row mark's are enabled also enable the Mark All checkbox
-        cb_cnt = 0
-        total_row = self.ui.apertures_table.rowCount()
-        for row in range(total_row):
-            if self.ui.apertures_table.cellWidget(row, 5).isChecked():
-                cb_cnt += 1
-            else:
-                cb_cnt -= 1
-        if cb_cnt < total_row:
-            self.ui.mark_all_cb.setChecked(False)
-        else:
-            self.ui.mark_all_cb.setChecked(True)
-        self.ui_connect()
-
-    def on_mark_all_click(self, signal):
-        self.ui_disconnect()
-        mark_all = self.ui.mark_all_cb.isChecked()
-        for row in range(self.ui.apertures_table.rowCount()):
-            # update the mark_rows list
-            if mark_all:
-                self.marked_rows.append(True)
-            else:
-                self.marked_rows[:] = []
-
-            mark_cb = self.ui.apertures_table.cellWidget(row, 5)
-            mark_cb.setChecked(mark_all)
-
-        if mark_all:
-            for aperture in self.apertures:
-                # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True)
-                self.plot_aperture(color=self.app.defaults['global_sel_draw_color'],
-                                   marked_aperture=aperture, visible=True)
-            # HACK: enable/disable the grid for a better look
-            self.app.ui.grid_snap_btn.trigger()
-            self.app.ui.grid_snap_btn.trigger()
-        else:
-            self.clear_plot_apertures()
-
-        self.ui_connect()
-
-    def export_gerber(self, whole, fract, g_zeros='L', factor=1):
-        """
-
-        :return: Gerber_code
-        """
-
-        def tz_format(x, y, fac):
-            x_c = x * fac
-            y_c = y * fac
-
-            x_form = "{:.{dec}f}".format(x_c, dec=fract)
-            y_form = "{:.{dec}f}".format(y_c, dec=fract)
-
-            # extract whole part and decimal part
-            x_form = x_form.partition('.')
-            y_form = y_form.partition('.')
-
-            # left padd the 'whole' part with zeros
-            x_whole = x_form[0].rjust(whole, '0')
-            y_whole = y_form[0].rjust(whole, '0')
-
-            # restore the coordinate padded in the left with 0 and added the decimal part
-            # without the decinal dot
-            x_form = x_whole + x_form[2]
-            y_form = y_whole + y_form[2]
-            return x_form, y_form
-
-        def lz_format(x, y, fac):
-            x_c = x * fac
-            y_c = y * fac
-
-            x_form = "{:.{dec}f}".format(x_c, dec=fract).replace('.', '')
-            y_form = "{:.{dec}f}".format(y_c, dec=fract).replace('.', '')
-
-            # pad with rear zeros
-            x_form.ljust(length, '0')
-            y_form.ljust(length, '0')
-
-            return x_form, y_form
-
-        # Gerber code is stored here
-        gerber_code = ''
-
-        # apertures processing
-        try:
-            length = whole + fract
-            if '0' in self.apertures:
-                if 'geometry' in self.apertures['0']:
-                    for geo_elem in self.apertures['0']['geometry']:
-                        if 'solid' in geo_elem:
-                            geo = geo_elem['solid']
-                            if not geo.is_empty:
-                                gerber_code += 'G36*\n'
-                                geo_coords = list(geo.exterior.coords)
-                                # first command is a move with pen-up D02 at the beginning of the geo
-                                if g_zeros == 'T':
-                                    x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor)
-                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                   yform=y_formatted)
-                                else:
-                                    x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor)
-                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                   yform=y_formatted)
-                                for coord in geo_coords[1:]:
-                                    if g_zeros == 'T':
-                                        x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
-                                        gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                       yform=y_formatted)
-                                    else:
-                                        x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
-                                        gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                       yform=y_formatted)
-                                gerber_code += 'D02*\n'
-                                gerber_code += 'G37*\n'
-
-                                clear_list = list(geo.interiors)
-                                if clear_list:
-                                    gerber_code += '%LPC*%\n'
-                                    for clear_geo in clear_list:
-                                        gerber_code += 'G36*\n'
-                                        geo_coords = list(clear_geo.coords)
-
-                                        # first command is a move with pen-up D02 at the beginning of the geo
-                                        if g_zeros == 'T':
-                                            x_formatted, y_formatted = tz_format(
-                                                geo_coords[0][0], geo_coords[0][1], factor)
-                                            gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-                                        else:
-                                            x_formatted, y_formatted = lz_format(
-                                                geo_coords[0][0], geo_coords[0][1], factor)
-                                            gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-
-                                        prev_coord = geo_coords[0]
-                                        for coord in geo_coords[1:]:
-                                            if coord != prev_coord:
-                                                if g_zeros == 'T':
-                                                    x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
-                                                    gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                                   yform=y_formatted)
-                                                else:
-                                                    x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
-                                                    gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                                   yform=y_formatted)
-                                            prev_coord = coord
-
-                                        gerber_code += 'D02*\n'
-                                        gerber_code += 'G37*\n'
-                                    gerber_code += '%LPD*%\n'
-                        if 'clear' in geo_elem:
-                            geo = geo_elem['clear']
-                            if not geo.is_empty:
-                                gerber_code += '%LPC*%\n'
-                                gerber_code += 'G36*\n'
-                                geo_coords = list(geo.exterior.coords)
-                                # first command is a move with pen-up D02 at the beginning of the geo
-                                if g_zeros == 'T':
-                                    x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor)
-                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                   yform=y_formatted)
-                                else:
-                                    x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor)
-                                    gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                   yform=y_formatted)
-
-                                prev_coord = geo_coords[0]
-                                for coord in geo_coords[1:]:
-                                    if coord != prev_coord:
-                                        if g_zeros == 'T':
-                                            x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
-                                            gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-                                        else:
-                                            x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
-                                            gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-                                    prev_coord = coord
-
-                                gerber_code += 'D02*\n'
-                                gerber_code += 'G37*\n'
-                                gerber_code += '%LPD*%\n'
-
-            for apid in self.apertures:
-                if apid == '0':
-                    continue
-                else:
-                    gerber_code += 'D%s*\n' % str(apid)
-                    if 'geometry' in self.apertures[apid]:
-                        for geo_elem in self.apertures[apid]['geometry']:
-                            if 'follow' in geo_elem:
-                                geo = geo_elem['follow']
-                                if not geo.is_empty:
-                                    if isinstance(geo, Point):
-                                        if g_zeros == 'T':
-                                            x_formatted, y_formatted = tz_format(geo.x, geo.y, factor)
-                                            gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-                                        else:
-                                            x_formatted, y_formatted = lz_format(geo.x, geo.y, factor)
-                                            gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-                                    else:
-                                        geo_coords = list(geo.coords)
-                                        # first command is a move with pen-up D02 at the beginning of the geo
-                                        if g_zeros == 'T':
-                                            x_formatted, y_formatted = tz_format(
-                                                geo_coords[0][0], geo_coords[0][1], factor)
-                                            gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-                                        else:
-                                            x_formatted, y_formatted = lz_format(
-                                                geo_coords[0][0], geo_coords[0][1], factor)
-                                            gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-
-                                        prev_coord = geo_coords[0]
-                                        for coord in geo_coords[1:]:
-                                            if coord != prev_coord:
-                                                if g_zeros == 'T':
-                                                    x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
-                                                    gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                                   yform=y_formatted)
-                                                else:
-                                                    x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
-                                                    gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                                   yform=y_formatted)
-                                            prev_coord = coord
-
-                                        # gerber_code += "D02*\n"
-
-                            if 'clear' in geo_elem:
-                                gerber_code += '%LPC*%\n'
-
-                                geo = geo_elem['clear']
-                                if not geo.is_empty:
-                                    if isinstance(geo, Point):
-                                        if g_zeros == 'T':
-                                            x_formatted, y_formatted = tz_format(geo.x, geo.y, factor)
-                                            gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-                                        else:
-                                            x_formatted, y_formatted = lz_format(geo.x, geo.y, factor)
-                                            gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-                                    else:
-                                        geo_coords = list(geo.coords)
-                                        # first command is a move with pen-up D02 at the beginning of the geo
-                                        if g_zeros == 'T':
-                                            x_formatted, y_formatted = tz_format(
-                                                geo_coords[0][0], geo_coords[0][1], factor)
-                                            gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-                                        else:
-                                            x_formatted, y_formatted = lz_format(
-                                                geo_coords[0][0], geo_coords[0][1], factor)
-                                            gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
-                                                                                           yform=y_formatted)
-
-                                        prev_coord = geo_coords[0]
-                                        for coord in geo_coords[1:]:
-                                            if coord != prev_coord:
-                                                if g_zeros == 'T':
-                                                    x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
-                                                    gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                                   yform=y_formatted)
-                                                else:
-                                                    x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
-                                                    gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
-                                                                                                   yform=y_formatted)
-
-                                            prev_coord = coord
-                                        # gerber_code += "D02*\n"
-                                    gerber_code += '%LPD*%\n'
-
-        except Exception as e:
-            log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() --> %s" % str(e))
-
-        if not self.apertures:
-            log.debug("FlatCAMObj.FlatCAMGerber.export_gerber() --> Gerber Object is empty: no apertures.")
-            return 'fail'
-
-        return gerber_code
-
-    def mirror(self, axis, point):
-        Gerber.mirror(self, axis=axis, point=point)
-        self.replotApertures.emit()
-
-    def offset(self, vect):
-        Gerber.offset(self, vect=vect)
-        self.replotApertures.emit()
-
-    def rotate(self, angle, point):
-        Gerber.rotate(self, angle=angle, point=point)
-        self.replotApertures.emit()
-
-    def scale(self, xfactor, yfactor=None, point=None):
-        Gerber.scale(self, xfactor=xfactor, yfactor=yfactor, point=point)
-        self.replotApertures.emit()
-
-    def skew(self, angle_x, angle_y, point):
-        Gerber.skew(self, angle_x=angle_x, angle_y=angle_y, point=point)
-        self.replotApertures.emit()
-
-    def serialize(self):
-        return {
-            "options": self.options,
-            "kind": self.kind
-        }
-
-
-class FlatCAMExcellon(FlatCAMObj, Excellon):
-    """
-    Represents Excellon/Drill code.
-    """
-
-    ui_type = ExcellonObjectUI
-    optionChanged = QtCore.pyqtSignal(str)
-
-    def __init__(self, name):
-        Excellon.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"]))
-        FlatCAMObj.__init__(self, name)
-
-        self.kind = "excellon"
-
-        self.options.update({
-            "plot": True,
-            "solid": False,
-            "drillz": -0.1,
-            "travelz": 0.1,
-            "feedrate": 5.0,
-            "feedrate_rapid": 5.0,
-            "tooldia": 0.1,
-            "slot_tooldia": 0.1,
-            "toolchange": False,
-            "toolchangez": 1.0,
-            "toolchangexy": "0.0, 0.0",
-            "endz": 2.0,
-            "startz": None,
-            "spindlespeed": None,
-            "dwell": True,
-            "dwelltime": 1000,
-            "ppname_e": 'defaults',
-            "z_pdepth": -0.02,
-            "feedrate_probe": 3.0,
-            "optimization_type": "R",
-            "gcode_type": "drills"
-        })
-
-        # TODO: Document this.
-        self.tool_cbs = {}
-
-        # dict to hold the tool number as key and tool offset as value
-        self.tool_offset = {}
-
-        # variable to store the total amount of drills per job
-        self.tot_drill_cnt = 0
-        self.tool_row = 0
-
-        # variable to store the total amount of slots per job
-        self.tot_slot_cnt = 0
-        self.tool_row_slots = 0
-
-        # variable to store the distance travelled
-        self.travel_distance = 0.0
-
-        # store the source file here
-        self.source_file = ""
-
-        self.multigeo = False
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from predecessors.
-        self.ser_attrs += ['options', 'kind']
-
-    def merge(self, exc_list, exc_final):
-        """
-        Merge Excellon objects found in exc_list parameter into exc_final object.
-        Options are always copied from source .
-
-        Tools are disregarded, what is taken in consideration is the unique drill diameters found as values in the
-        exc_list tools dict's. In the reconstruction section for each unique tool diameter it will be created a
-        tool_name to be used in the final Excellon object, exc_final.
-
-        If only one object is in exc_list parameter then this function will copy that object in the exc_final
-
-        :param exc_list: List or one object of FlatCAMExcellon Objects to join.
-        :param exc_final: Destination FlatCAMExcellon object.
-        :return: None
-        """
-
-        # flag to signal that we need to reorder the tools dictionary and drills and slots lists
-        flag_order = False
-
-        try:
-            flattened_list = list(itertools.chain(*exc_list))
-        except TypeError:
-            flattened_list = exc_list
-
-        # this dict will hold the unique tool diameters found in the exc_list objects as the dict keys and the dict
-        # values will be list of Shapely Points; for drills
-        custom_dict_drills = {}
-
-        # this dict will hold the unique tool diameters found in the exc_list objects as the dict keys and the dict
-        # values will be list of Shapely Points; for slots
-        custom_dict_slots = {}
-
-        for exc in flattened_list:
-            # copy options of the current excellon obj to the final excellon obj
-            for option in exc.options:
-                if option is not 'name':
-                    try:
-                        exc_final.options[option] = exc.options[option]
-                    except Exception as e:
-                        exc.app.log.warning("Failed to copy option.", option)
-
-            for drill in exc.drills:
-                exc_tool_dia = float('%.4f' % exc.tools[drill['tool']]['C'])
-
-                if exc_tool_dia not in custom_dict_drills:
-                    custom_dict_drills[exc_tool_dia] = [drill['point']]
-                else:
-                    custom_dict_drills[exc_tool_dia].append(drill['point'])
-
-            for slot in exc.slots:
-                exc_tool_dia = float('%.4f' % exc.tools[slot['tool']]['C'])
-
-                if exc_tool_dia not in custom_dict_slots:
-                    custom_dict_slots[exc_tool_dia] = [[slot['start'], slot['stop']]]
-                else:
-                    custom_dict_slots[exc_tool_dia].append([slot['start'], slot['stop']])
-
-            # add the zeros and units to the exc_final object
-            exc_final.zeros = exc.zeros
-            exc_final.units = exc.units
-
-        # ##########################################
-        # Here we add data to the exc_final object #
-        # ##########################################
-
-        # variable to make tool_name for the tools
-        current_tool = 0
-        # The tools diameter are now the keys in the drill_dia dict and the values are the Shapely Points in case of
-        # drills
-        for tool_dia in custom_dict_drills:
-            # we create a tool name for each key in the drill_dia dict (the key is a unique drill diameter)
-            current_tool += 1
-
-            tool_name = str(current_tool)
-            spec = {"C": float(tool_dia)}
-            exc_final.tools[tool_name] = spec
-
-            # rebuild the drills list of dict's that belong to the exc_final object
-            for point in custom_dict_drills[tool_dia]:
-                exc_final.drills.append(
-                    {
-                        "point": point,
-                        "tool": str(current_tool)
-                    }
-                )
-
-        # The tools diameter are now the keys in the drill_dia dict and the values are a list ([start, stop])
-        # of two Shapely Points in case of slots
-        for tool_dia in custom_dict_slots:
-            # we create a tool name for each key in the slot_dia dict (the key is a unique slot diameter)
-            # but only if there are no drills
-            if not exc_final.tools:
-                current_tool += 1
-                tool_name = str(current_tool)
-                spec = {"C": float(tool_dia)}
-                exc_final.tools[tool_name] = spec
-            else:
-                dia_list = []
-                for v in exc_final.tools.values():
-                    dia_list.append(float(v["C"]))
-
-                if tool_dia not in dia_list:
-                    flag_order = True
-
-                    current_tool = len(dia_list) + 1
-                    tool_name = str(current_tool)
-                    spec = {"C": float(tool_dia)}
-                    exc_final.tools[tool_name] = spec
-
-                else:
-                    for k, v in exc_final.tools.items():
-                        if v["C"] == tool_dia:
-                            current_tool = int(k)
-                            break
-
-            # rebuild the slots list of dict's that belong to the exc_final object
-            for point in custom_dict_slots[tool_dia]:
-                exc_final.slots.append(
-                    {
-                        "start": point[0],
-                        "stop": point[1],
-                        "tool": str(current_tool)
-                    }
-                )
-
-        # flag_order == True means that there was an slot diameter not in the tools and we also have drills
-        # and the new tool was added to self.tools therefore we need to reorder the tools and drills and slots
-        current_tool = 0
-        if flag_order is True:
-            dia_list = []
-            temp_drills = []
-            temp_slots = []
-            temp_tools = {}
-            for v in exc_final.tools.values():
-                dia_list.append(float(v["C"]))
-            dia_list.sort()
-            for ordered_dia in dia_list:
-                current_tool += 1
-                tool_name_temp = str(current_tool)
-                spec_temp = {"C": float(ordered_dia)}
-                temp_tools[tool_name_temp] = spec_temp
-
-                for drill in exc_final.drills:
-                    exc_tool_dia = float('%.4f' % exc_final.tools[drill['tool']]['C'])
-                    if exc_tool_dia == ordered_dia:
-                        temp_drills.append(
-                            {
-                                "point": drill["point"],
-                                "tool": str(current_tool)
-                            }
-                        )
-
-                for slot in exc_final.slots:
-                    slot_tool_dia = float('%.4f' % exc_final.tools[slot['tool']]['C'])
-                    if slot_tool_dia == ordered_dia:
-                        temp_slots.append(
-                            {
-                                "start": slot["start"],
-                                "stop": slot["stop"],
-                                "tool": str(current_tool)
-                            }
-                        )
-
-            # delete the exc_final tools, drills and slots
-            exc_final.tools = dict()
-            exc_final.drills[:] = []
-            exc_final.slots[:] = []
-
-            # update the exc_final tools, drills and slots with the ordered values
-            exc_final.tools = temp_tools
-            exc_final.drills[:] = temp_drills
-            exc_final.slots[:] = temp_slots
-
-        # create the geometry for the exc_final object
-        exc_final.create_geometry()
-
-    def build_ui(self):
-        FlatCAMObj.build_ui(self)
-
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        try:
-            # if connected, disconnect the signal from the slot on item_changed as it creates issues
-            self.ui.tools_table.itemChanged.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        n = len(self.tools)
-        # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
-        self.ui.tools_table.setRowCount(n + 2)
-
-        self.tot_drill_cnt = 0
-        self.tot_slot_cnt = 0
-
-        self.tool_row = 0
-
-        sort = []
-        for k, v in list(self.tools.items()):
-            sort.append((k, v.get('C')))
-        sorted_tools = sorted(sort, key=lambda t1: t1[1])
-        tools = [i[0] for i in sorted_tools]
-
-        for tool_no in tools:
-
-            drill_cnt = 0  # variable to store the nr of drills per tool
-            slot_cnt = 0  # variable to store the nr of slots per tool
-
-            # Find no of drills for the current tool
-            for drill in self.drills:
-                if drill['tool'] == tool_no:
-                    drill_cnt += 1
-
-            self.tot_drill_cnt += drill_cnt
-
-            # Find no of slots for the current tool
-            for slot in self.slots:
-                if slot['tool'] == tool_no:
-                    slot_cnt += 1
-
-            self.tot_slot_cnt += slot_cnt
-
-            id = QtWidgets.QTableWidgetItem('%d' % int(tool_no))
-            id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.ui.tools_table.setItem(self.tool_row, 0, id)  # Tool name/id
-
-            # Make sure that the drill diameter when in MM is with no more than 2 decimals
-            # There are no drill bits in MM with more than 3 decimals diameter
-            # For INCH the decimals should be no more than 3. There are no drills under 10mils
-            if self.units == 'MM':
-                dia = QtWidgets.QTableWidgetItem('%.2f' % (self.tools[tool_no]['C']))
-            else:
-                dia = QtWidgets.QTableWidgetItem('%.4f' % (self.tools[tool_no]['C']))
-
-            dia.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            drill_count = QtWidgets.QTableWidgetItem('%d' % drill_cnt)
-            drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            # if the slot number is zero is better to not clutter the GUI with zero's so we print a space
-            if slot_cnt > 0:
-                slot_count = QtWidgets.QTableWidgetItem('%d' % slot_cnt)
-            else:
-                slot_count = QtWidgets.QTableWidgetItem('')
-            slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            try:
-                if self.units == 'MM':
-                    t_offset = self.tool_offset[float('%.2f' % float(self.tools[tool_no]['C']))]
-                else:
-                    t_offset = self.tool_offset[float('%.4f' % float(self.tools[tool_no]['C']))]
-            except KeyError:
-                t_offset = self.app.defaults['excellon_offset']
-
-            tool_offset_item = QtWidgets.QTableWidgetItem('%s' % str(t_offset))
-
-            plot_item = FCCheckBox()
-            plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
-            if self.ui.plot_cb.isChecked():
-                plot_item.setChecked(True)
-
-            self.ui.tools_table.setItem(self.tool_row, 1, dia)  # Diameter
-            self.ui.tools_table.setItem(self.tool_row, 2, drill_count)  # Number of drills per tool
-            self.ui.tools_table.setItem(self.tool_row, 3, slot_count)  # Number of drills per tool
-            self.ui.tools_table.setItem(self.tool_row, 4, tool_offset_item)  # Tool offset
-            self.ui.tools_table.setCellWidget(self.tool_row, 5, plot_item)
-
-            self.tool_row += 1
-
-        # add a last row with the Total number of drills
-        empty = QtWidgets.QTableWidgetItem('')
-        empty.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-        empty_1 = QtWidgets.QTableWidgetItem('')
-        empty_1.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-
-        label_tot_drill_count = QtWidgets.QTableWidgetItem(_('Total Drills'))
-        tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt)
-        label_tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
-        tot_drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
-
-        self.ui.tools_table.setItem(self.tool_row, 0, empty)
-        self.ui.tools_table.setItem(self.tool_row, 1, label_tot_drill_count)
-        self.ui.tools_table.setItem(self.tool_row, 2, tot_drill_count)  # Total number of drills
-        self.ui.tools_table.setItem(self.tool_row, 3, empty_1)  # Total number of drills
-
-        font = QtGui.QFont()
-        font.setBold(True)
-        font.setWeight(75)
-
-        for k in [1, 2]:
-            self.ui.tools_table.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255))
-            self.ui.tools_table.item(self.tool_row, k).setFont(font)
-
-        self.tool_row += 1
-
-        # add a last row with the Total number of slots
-        empty_2 = QtWidgets.QTableWidgetItem('')
-        empty_2.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-        empty_3 = QtWidgets.QTableWidgetItem('')
-        empty_3.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-
-        label_tot_slot_count = QtWidgets.QTableWidgetItem(_('Total Slots'))
-        tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt)
-        label_tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
-        tot_slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
-
-        self.ui.tools_table.setItem(self.tool_row, 0, empty_2)
-        self.ui.tools_table.setItem(self.tool_row, 1, label_tot_slot_count)
-        self.ui.tools_table.setItem(self.tool_row, 2, empty_3)
-        self.ui.tools_table.setItem(self.tool_row, 3, tot_slot_count)  # Total number of slots
-
-        for kl in [1, 2, 3]:
-            self.ui.tools_table.item(self.tool_row, kl).setFont(font)
-            self.ui.tools_table.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255))
-
-        # sort the tool diameter column
-        # self.ui.tools_table.sortItems(1)
-        # all the tools are selected by default
-        self.ui.tools_table.selectColumn(0)
-        #
-        self.ui.tools_table.resizeColumnsToContents()
-        self.ui.tools_table.resizeRowsToContents()
-
-        vertical_header = self.ui.tools_table.verticalHeader()
-        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
-        vertical_header.hide()
-        self.ui.tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-
-        horizontal_header = self.ui.tools_table.horizontalHeader()
-        horizontal_header.setMinimumSectionSize(10)
-        horizontal_header.setDefaultSectionSize(70)
-        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(0, 20)
-        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
-        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(5, 17)
-        self.ui.tools_table.setColumnWidth(5, 17)
-
-        # horizontal_header.setStretchLastSection(True)
-        # horizontal_header.setColumnWidth(2, QtWidgets.QHeaderView.ResizeToContents)
-
-        # horizontal_header.setStretchLastSection(True)
-        self.ui.tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-
-        self.ui.tools_table.setSortingEnabled(False)
-
-        self.ui.tools_table.setMinimumHeight(self.ui.tools_table.getHeight())
-        self.ui.tools_table.setMaximumHeight(self.ui.tools_table.getHeight())
-
-        if not self.drills:
-            self.ui.tdlabel.hide()
-            self.ui.tooldia_entry.hide()
-            self.ui.generate_milling_button.hide()
-        else:
-            self.ui.tdlabel.show()
-            self.ui.tooldia_entry.show()
-            self.ui.generate_milling_button.show()
-
-        if not self.slots:
-            self.ui.stdlabel.hide()
-            self.ui.slot_tooldia_entry.hide()
-            self.ui.generate_milling_slots_button.hide()
-        else:
-            self.ui.stdlabel.show()
-            self.ui.slot_tooldia_entry.show()
-            self.ui.generate_milling_slots_button.show()
-
-        # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated
-        self.ui.tools_table.itemChanged.connect(self.on_tool_offset_edit)
-
-        self.ui_connect()
-
-    def set_ui(self, ui):
-        """
-        Configures the user interface for this object.
-        Connects options to form fields.
-
-        :param ui: User interface object.
-        :type ui: ExcellonObjectUI
-        :return: None
-        """
-        FlatCAMObj.set_ui(self, ui)
-
-        FlatCAMApp.App.log.debug("FlatCAMExcellon.set_ui()")
-
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        self.form_fields.update({
-            "plot": self.ui.plot_cb,
-            "solid": self.ui.solid_cb,
-            "drillz": self.ui.cutz_entry,
-            "travelz": self.ui.travelz_entry,
-            "feedrate": self.ui.feedrate_entry,
-            "feedrate_rapid": self.ui.feedrate_rapid_entry,
-            "tooldia": self.ui.tooldia_entry,
-            "slot_tooldia": self.ui.slot_tooldia_entry,
-            "toolchange": self.ui.toolchange_cb,
-            "toolchangez": self.ui.toolchangez_entry,
-            "spindlespeed": self.ui.spindlespeed_entry,
-            "dwell": self.ui.dwell_cb,
-            "dwelltime": self.ui.dwelltime_entry,
-            "startz": self.ui.estartz_entry,
-            "endz": self.ui.eendz_entry,
-            "ppname_e": self.ui.pp_excellon_name_cb,
-            "z_pdepth": self.ui.pdepth_entry,
-            "feedrate_probe": self.ui.feedrate_probe_entry,
-            "gcode_type": self.ui.excellon_gcode_type_radio
-        })
-
-        for name in list(self.app.postprocessors.keys()):
-            # the HPGL postprocessor is only for Geometry not for Excellon job therefore don't add it
-            if name == 'hpgl':
-                continue
-            self.ui.pp_excellon_name_cb.addItem(name)
-
-        # Fill form fields
-        self.to_form()
-
-        # initialize the dict that holds the tools offset
-        t_default_offset = self.app.defaults["excellon_offset"]
-        if not self.tool_offset:
-            for value in self.tools.values():
-                if self.units == 'MM':
-                    dia = float('%.2f' % float(value['C']))
-                else:
-                    dia = float('%.4f' % float(value['C']))
-                self.tool_offset[dia] = t_default_offset
-
-        # Show/Hide Advanced Options
-        if self.app.defaults["global_app_level"] == 'b':
-            self.ui.level.setText(_(
-                '<span style="color:green;"><b>Basic</b></span>'
-            ))
-
-            self.ui.tools_table.setColumnHidden(4, True)
-            self.ui.estartz_label.hide()
-            self.ui.estartz_entry.hide()
-            self.ui.eendz_label.hide()
-            self.ui.eendz_entry.hide()
-            self.ui.feedrate_rapid_label.hide()
-            self.ui.feedrate_rapid_entry.hide()
-            self.ui.pdepth_label.hide()
-            self.ui.pdepth_entry.hide()
-            self.ui.feedrate_probe_label.hide()
-            self.ui.feedrate_probe_entry.hide()
-        else:
-            self.ui.level.setText(_(
-                '<span style="color:red;"><b>Advanced</b></span>'
-            ))
-
-        assert isinstance(self.ui, ExcellonObjectUI), \
-            "Expected a ExcellonObjectUI, got %s" % type(self.ui)
-        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
-        self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
-        self.ui.generate_cnc_button.clicked.connect(self.on_create_cncjob_button_click)
-        self.ui.generate_milling_button.clicked.connect(self.on_generate_milling_button_click)
-        self.ui.generate_milling_slots_button.clicked.connect(self.on_generate_milling_slots_button_click)
-
-        self.ui.pp_excellon_name_cb.activated.connect(self.on_pp_changed)
-
-    def ui_connect(self):
-
-        for row in range(self.ui.tools_table.rowCount() - 2):
-            self.ui.tools_table.cellWidget(row, 5).clicked.connect(self.on_plot_cb_click_table)
-        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
-
-    def ui_disconnect(self):
-        for row in range(self.ui.tools_table.rowCount()):
-            try:
-                self.ui.tools_table.cellWidget(row, 5).clicked.disconnect()
-            except (TypeError, AttributeError):
-                pass
-
-        try:
-            self.ui.plot_cb.stateChanged.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-    def on_tool_offset_edit(self):
-        # if connected, disconnect the signal from the slot on item_changed as it creates issues
-        self.ui.tools_table.itemChanged.disconnect()
-        # self.tools_table_exc.selectionModel().currentChanged.disconnect()
-
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        self.is_modified = True
-
-        row_of_item_changed = self.ui.tools_table.currentRow()
-        if self.units == 'MM':
-            dia = float('%.2f' % float(self.ui.tools_table.item(row_of_item_changed, 1).text()))
-        else:
-            dia = float('%.4f' % float(self.ui.tools_table.item(row_of_item_changed, 1).text()))
-
-        current_table_offset_edited = None
-        if self.ui.tools_table.currentItem() is not None:
-            try:
-                current_table_offset_edited = float(self.ui.tools_table.currentItem().text())
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    current_table_offset_edited = float(self.ui.tools_table.currentItem().text().replace(',', '.'))
-                    self.ui.tools_table.currentItem().setText(
-                        self.ui.tools_table.currentItem().text().replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit(_(
-                        "[ERROR_NOTCL] Wrong value format entered, use a number."
-                    ))
-                    self.ui.tools_table.currentItem().setText(str(self.tool_offset[dia]))
-                    return
-
-        self.tool_offset[dia] = current_table_offset_edited
-
-        # we reactivate the signals after the after the tool editing
-        self.ui.tools_table.itemChanged.connect(self.on_tool_offset_edit)
-
-    def get_selected_tools_list(self):
-        """
-        Returns the keys to the self.tools dictionary corresponding
-        to the selections on the tool list in the GUI.
-
-        :return: List of tools.
-        :rtype: list
-        """
-
-        return [str(x.text()) for x in self.ui.tools_table.selectedItems()]
-
-    def get_selected_tools_table_items(self):
-        """
-        Returns a list of lists, each list in the list is made out of row elements
-
-        :return: List of table_tools items.
-        :rtype: list
-        """
-        table_tools_items = []
-        for x in self.ui.tools_table.selectedItems():
-            # from the columnCount we subtract a value of 1 which represent the last column (plot column)
-            # which does not have text
-            table_tools_items.append([self.ui.tools_table.item(x.row(), column).text()
-                                      for column in range(0, self.ui.tools_table.columnCount() - 1)])
-        for item in table_tools_items:
-            item[0] = str(item[0])
-        return table_tools_items
-
-    def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1):
-        """
-        Returns two values, first is a boolean , if 1 then the file has slots and second contain the Excellon code
-        :return: has_slots and Excellon_code
-        """
-
-        excellon_code = ''
-
-        # store here if the file has slots, return 1 if any slots, 0 if only drills
-        has_slots = 0
-
-        # drills processing
-        try:
-            if self.drills:
-                length = whole + fract
-                for tool in self.tools:
-                    excellon_code += 'T0%s\n' % str(tool) if int(tool) < 10 else 'T%s\n' % str(tool)
-
-                    for drill in self.drills:
-                        if form == 'dec' and tool == drill['tool']:
-                            drill_x = drill['point'].x * factor
-                            drill_y = drill['point'].y * factor
-                            excellon_code += "X{:.{dec}f}Y{:.{dec}f}\n".format(drill_x, drill_y, dec=fract)
-                        elif e_zeros == 'LZ' and tool == drill['tool']:
-                            drill_x = drill['point'].x * factor
-                            drill_y = drill['point'].y * factor
-
-                            exc_x_formatted = "{:.{dec}f}".format(drill_x, dec=fract)
-                            exc_y_formatted = "{:.{dec}f}".format(drill_y, dec=fract)
-
-                            # extract whole part and decimal part
-                            exc_x_formatted = exc_x_formatted.partition('.')
-                            exc_y_formatted = exc_y_formatted.partition('.')
-
-                            # left padd the 'whole' part with zeros
-                            x_whole = exc_x_formatted[0].rjust(whole, '0')
-                            y_whole = exc_y_formatted[0].rjust(whole, '0')
-
-                            # restore the coordinate padded in the left with 0 and added the decimal part
-                            # without the decinal dot
-                            exc_x_formatted = x_whole + exc_x_formatted[2]
-                            exc_y_formatted = y_whole + exc_y_formatted[2]
-
-                            excellon_code += "X{xform}Y{yform}\n".format(xform=exc_x_formatted,
-                                                                         yform=exc_y_formatted)
-                        elif tool == drill['tool']:
-                            drill_x = drill['point'].x * factor
-                            drill_y = drill['point'].y * factor
-
-                            exc_x_formatted = "{:.{dec}f}".format(drill_x, dec=fract).replace('.', '')
-                            exc_y_formatted = "{:.{dec}f}".format(drill_y, dec=fract).replace('.', '')
-
-                            # pad with rear zeros
-                            exc_x_formatted.ljust(length, '0')
-                            exc_y_formatted.ljust(length, '0')
-
-                            excellon_code += "X{xform}Y{yform}\n".format(xform=exc_x_formatted,
-                                                                         yform=exc_y_formatted)
-        except Exception as e:
-            log.debug(str(e))
-
-        # slots processing
-        try:
-            if self.slots:
-                has_slots = 1
-                for tool in self.tools:
-                    if int(tool) < 10:
-                        excellon_code += 'T0' + str(tool) + '\n'
-                    else:
-                        excellon_code += 'T' + str(tool) + '\n'
-
-                    for slot in self.slots:
-                        if form == 'dec' and tool == slot['tool']:
-                            start_slot_x = slot['start'].x * factor
-                            start_slot_y = slot['start'].y * factor
-                            stop_slot_x = slot['stop'].x * factor
-                            stop_slot_y = slot['stop'].y * factor
-
-                            excellon_code += "G00X{:.{dec}f}Y{:.{dec}f}\nM15\n".format(start_slot_x,
-                                                                                       start_slot_y,
-                                                                                       dec=fract)
-                            excellon_code += "G00X{:.{dec}f}Y{:.{dec}f}\nM16\n".format(stop_slot_x,
-                                                                                       stop_slot_y,
-                                                                                       dec=fract)
-
-                        elif e_zeros == 'LZ' and tool == slot['tool']:
-                            start_slot_x = slot['start'].x * factor
-                            start_slot_y = slot['start'].y * factor
-                            stop_slot_x = slot['stop'].x * factor
-                            stop_slot_y = slot['stop'].y * factor
-
-                            start_slot_x_formatted = "{:.{dec}f}".format(start_slot_x, dec=fract).replace('.', '')
-                            start_slot_y_formatted = "{:.{dec}f}".format(start_slot_y, dec=fract).replace('.', '')
-                            stop_slot_x_formatted = "{:.{dec}f}".format(stop_slot_x, dec=fract).replace('.', '')
-                            stop_slot_y_formatted = "{:.{dec}f}".format(stop_slot_y, dec=fract).replace('.', '')
-
-                            # extract whole part and decimal part
-                            start_slot_x_formatted = start_slot_x_formatted.partition('.')
-                            start_slot_y_formatted = start_slot_y_formatted.partition('.')
-                            stop_slot_x_formatted = stop_slot_x_formatted.partition('.')
-                            stop_slot_y_formatted = stop_slot_y_formatted.partition('.')
-
-                            # left padd the 'whole' part with zeros
-                            start_x_whole = start_slot_x_formatted[0].rjust(whole, '0')
-                            start_y_whole = start_slot_y_formatted[0].rjust(whole, '0')
-                            stop_x_whole = stop_slot_x_formatted[0].rjust(whole, '0')
-                            stop_y_whole = stop_slot_y_formatted[0].rjust(whole, '0')
-
-                            # restore the coordinate padded in the left with 0 and added the decimal part
-                            # without the decinal dot
-                            start_slot_x_formatted = start_x_whole + start_slot_x_formatted[2]
-                            start_slot_y_formatted = start_y_whole + start_slot_y_formatted[2]
-                            stop_slot_x_formatted = stop_x_whole + stop_slot_x_formatted[2]
-                            stop_slot_y_formatted = stop_y_whole + stop_slot_y_formatted[2]
-
-                            excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted,
-                                                                                   ystart=start_slot_y_formatted)
-                            excellon_code += "G00X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted,
-                                                                                 ystop=stop_slot_y_formatted)
-                        elif tool == slot['tool']:
-                            start_slot_x = slot['start'].x * factor
-                            start_slot_y = slot['start'].y * factor
-                            stop_slot_x = slot['stop'].x * factor
-                            stop_slot_y = slot['stop'].y * factor
-                            length = whole + fract
-
-                            start_slot_x_formatted = "{:.{dec}f}".format(start_slot_x, dec=fract).replace('.', '')
-                            start_slot_y_formatted = "{:.{dec}f}".format(start_slot_y, dec=fract).replace('.', '')
-                            stop_slot_x_formatted = "{:.{dec}f}".format(stop_slot_x, dec=fract).replace('.', '')
-                            stop_slot_y_formatted = "{:.{dec}f}".format(stop_slot_y, dec=fract).replace('.', '')
-
-                            # pad with rear zeros
-                            start_slot_x_formatted.ljust(length, '0')
-                            start_slot_y_formatted.ljust(length, '0')
-                            stop_slot_x_formatted.ljust(length, '0')
-                            stop_slot_y_formatted.ljust(length, '0')
-
-                            excellon_code += "G00X{xstart}Y{ystart}\nM15\n".format(xstart=start_slot_x_formatted,
-                                                                                   ystart=start_slot_y_formatted)
-                            excellon_code += "G00X{xstop}Y{ystop}\nM16\n".format(xstop=stop_slot_x_formatted,
-                                                                                 ystop=stop_slot_y_formatted)
-        except Exception as e:
-            log.debug(str(e))
-
-        if not self.drills and not self.slots:
-            log.debug("FlatCAMObj.FlatCAMExcellon.export_excellon() --> Excellon Object is empty: no drills, no slots.")
-            return 'fail'
-
-        return has_slots, excellon_code
-
-    def generate_milling_drills(self, tools=None, outname=None, tooldia=None, use_thread=False):
-        """
-        Note: This method is a good template for generic operations as
-        it takes it's options from parameters or otherwise from the
-        object's options and returns a (success, msg) tuple as feedback
-        for shell operations.
-
-        :return: Success/failure condition tuple (bool, str).
-        :rtype: tuple
-        """
-
-        # Get the tools from the list. These are keys
-        # to self.tools
-        if tools is None:
-            tools = self.get_selected_tools_list()
-
-        if outname is None:
-            outname = self.options["name"] + "_mill"
-
-        if tooldia is None:
-            tooldia = float(self.options["tooldia"])
-
-        # Sort tools by diameter. items() -> [('name', diameter), ...]
-        # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3
-
-        sort = []
-        for k, v in self.tools.items():
-            sort.append((k, v.get('C')))
-        sorted_tools = sorted(sort, key=lambda t1: t1[1])
-
-        if tools == "all":
-            tools = [i[0] for i in sorted_tools]  # List if ordered tool names.
-            log.debug("Tools 'all' and sorted are: %s" % str(tools))
-
-        if len(tools) == 0:
-            self.app.inform.emit(_(
-                "[ERROR_NOTCL] Please select one or more tools from the list and try again."
-            ))
-            return False, "Error: No tools."
-
-        for tool in tools:
-            if tooldia > self.tools[tool]["C"]:
-                self.app.inform.emit(_(
-                    "[ERROR_NOTCL] Milling tool for DRILLS is larger than hole size. Cancelled."
-                ))
-                return False, "Error: Milling tool is larger than hole."
-
-        def geo_init(geo_obj, app_obj):
-            assert isinstance(geo_obj, FlatCAMGeometry), \
-                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
-            app_obj.progress.emit(20)
-
-            # ## Add properties to the object
-
-            # get the tool_table items in a list of row items
-            tool_table_items = self.get_selected_tools_table_items()
-            # insert an information only element in the front
-            tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
-
-            geo_obj.options['Tools_in_use'] = tool_table_items
-            geo_obj.options['type'] = 'Excellon Geometry'
-            geo_obj.options["cnctooldia"] = str(tooldia)
-
-            geo_obj.solid_geometry = []
-
-            # in case that the tool used has the same diameter with the hole, and since the maximum resolution
-            # for FlatCAM is 6 decimals,
-            # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero"
-            for hole in self.drills:
-                if hole['tool'] in tools:
-                    buffer_value = self.tools[hole['tool']]["C"] / 2 - tooldia / 2
-                    if buffer_value == 0:
-                        geo_obj.solid_geometry.append(
-                            Point(hole['point']).buffer(0.0000001).exterior)
-                    else:
-                        geo_obj.solid_geometry.append(
-                            Point(hole['point']).buffer(buffer_value).exterior)
-        if use_thread:
-            def geo_thread(app_obj):
-                app_obj.new_object("geometry", outname, geo_init)
-                app_obj.progress.emit(100)
-
-            # Create a promise with the new name
-            self.app.collection.promise(outname)
-
-            # Send to worker
-            self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
-        else:
-            self.app.new_object("geometry", outname, geo_init)
-
-        return True, ""
-
-    def generate_milling_slots(self, tools=None, outname=None, tooldia=None, use_thread=False):
-        """
-        Note: This method is a good template for generic operations as
-        it takes it's options from parameters or otherwise from the
-        object's options and returns a (success, msg) tuple as feedback
-        for shell operations.
-
-        :return: Success/failure condition tuple (bool, str).
-        :rtype: tuple
-        """
-
-        # Get the tools from the list. These are keys
-        # to self.tools
-        if tools is None:
-            tools = self.get_selected_tools_list()
-
-        if outname is None:
-            outname = self.options["name"] + "_mill"
-
-        if tooldia is None:
-            tooldia = float(self.options["slot_tooldia"])
-
-        # Sort tools by diameter. items() -> [('name', diameter), ...]
-        # sorted_tools = sorted(list(self.tools.items()), key=lambda tl: tl[1]) # no longer works in Python3
-
-        sort = []
-        for k, v in self.tools.items():
-            sort.append((k, v.get('C')))
-        sorted_tools = sorted(sort, key=lambda t1: t1[1])
-
-        if tools == "all":
-            tools = [i[0] for i in sorted_tools]  # List if ordered tool names.
-            log.debug("Tools 'all' and sorted are: %s" % str(tools))
-
-        if len(tools) == 0:
-            self.app.inform.emit(_(
-                "[ERROR_NOTCL] Please select one or more tools from the list and try again."
-            ))
-            return False, "Error: No tools."
-
-        for tool in tools:
-            # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse
-            adj_toolstable_tooldia = float('%.4f' % float(tooldia))
-            adj_file_tooldia = float('%.4f' % float(self.tools[tool]["C"]))
-            if adj_toolstable_tooldia > adj_file_tooldia + 0.0001:
-                self.app.inform.emit(_(
-                    "[ERROR_NOTCL] Milling tool for SLOTS is larger than hole size. Cancelled."
-                ))
-                return False, "Error: Milling tool is larger than hole."
-
-        def geo_init(geo_obj, app_obj):
-            assert isinstance(geo_obj, FlatCAMGeometry), \
-                "Initializer expected a FlatCAMGeometry, got %s" % type(geo_obj)
-            app_obj.progress.emit(20)
-
-            # ## Add properties to the object
-
-            # get the tool_table items in a list of row items
-            tool_table_items = self.get_selected_tools_table_items()
-            # insert an information only element in the front
-            tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
-
-            geo_obj.options['Tools_in_use'] = tool_table_items
-            geo_obj.options['type'] = 'Excellon Geometry'
-            geo_obj.options["cnctooldia"] = str(tooldia)
-
-            geo_obj.solid_geometry = []
-
-            # in case that the tool used has the same diameter with the hole, and since the maximum resolution
-            # for FlatCAM is 6 decimals,
-            # we add a tenth of the minimum value, meaning 0.0000001, which from our point of view is "almost zero"
-            for slot in self.slots:
-                if slot['tool'] in tools:
-                    toolstable_tool = float('%.4f' % float(tooldia))
-                    file_tool = float('%.4f' % float(self.tools[tool]["C"]))
-
-                    # I add the 0.0001 value to account for the rounding error in converting from IN to MM and reverse
-                    # for the file_tool (tooldia actually)
-                    buffer_value = float(file_tool / 2) - float(toolstable_tool / 2) + 0.0001
-                    if buffer_value == 0:
-                        start = slot['start']
-                        stop = slot['stop']
-
-                        lines_string = LineString([start, stop])
-                        poly = lines_string.buffer(0.0000001, int(self.geo_steps_per_circle)).exterior
-                        geo_obj.solid_geometry.append(poly)
-                    else:
-                        start = slot['start']
-                        stop = slot['stop']
-
-                        lines_string = LineString([start, stop])
-                        poly = lines_string.buffer(buffer_value, int(self.geo_steps_per_circle)).exterior
-                        geo_obj.solid_geometry.append(poly)
-
-        if use_thread:
-            def geo_thread(app_obj):
-                app_obj.new_object("geometry", outname + '_slot', geo_init)
-                app_obj.progress.emit(100)
-
-            # Create a promise with the new name
-            self.app.collection.promise(outname)
-
-            # Send to worker
-            self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
-        else:
-            self.app.new_object("geometry", outname + '_slot', geo_init)
-
-        return True, ""
-
-    def on_generate_milling_button_click(self, *args):
-        self.app.report_usage("excellon_on_create_milling_drills button")
-        self.read_form()
-
-        self.generate_milling_drills(use_thread=False)
-
-    def on_generate_milling_slots_button_click(self, *args):
-        self.app.report_usage("excellon_on_create_milling_slots_button")
-        self.read_form()
-
-        self.generate_milling_slots(use_thread=False)
-
-    def on_pp_changed(self):
-        current_pp = self.ui.pp_excellon_name_cb.get_value()
-
-        if "toolchange_probe" in current_pp.lower():
-            self.ui.pdepth_entry.setVisible(True)
-            self.ui.pdepth_label.show()
-
-            self.ui.feedrate_probe_entry.setVisible(True)
-            self.ui.feedrate_probe_label.show()
-        else:
-            self.ui.pdepth_entry.setVisible(False)
-            self.ui.pdepth_label.hide()
-
-            self.ui.feedrate_probe_entry.setVisible(False)
-            self.ui.feedrate_probe_label.hide()
-
-        if 'marlin' in current_pp.lower() or 'custom' in current_pp.lower():
-            self.ui.feedrate_rapid_label.show()
-            self.ui.feedrate_rapid_entry.show()
-        else:
-            self.ui.feedrate_rapid_label.hide()
-            self.ui.feedrate_rapid_entry.hide()
-
-    def on_create_cncjob_button_click(self, *args):
-        self.app.report_usage("excellon_on_create_cncjob_button")
-        self.read_form()
-
-        # Get the tools from the list
-        tools = self.get_selected_tools_list()
-
-        if len(tools) == 0:
-            # if there is a single tool in the table (remember that the last 2 rows are for totals and do not count in
-            # tool number) it means that there are 3 rows (1 tool and 2 totals).
-            # in this case regardless of the selection status of that tool, use it.
-            if self.ui.tools_table.rowCount() == 3:
-                tools.append(self.ui.tools_table.item(0, 0).text())
-            else:
-                self.app.inform.emit(_(
-                    "[ERROR_NOTCL] Please select one or more tools from the list and try again."
-                ))
-                return
-
-        xmin = self.options['xmin']
-        ymin = self.options['ymin']
-        xmax = self.options['xmax']
-        ymax = self.options['ymax']
-
-        job_name = self.options["name"] + "_cnc"
-        pp_excellon_name = self.options["ppname_e"]
-
-        # Object initialization function for app.new_object()
-        def job_init(job_obj, app_obj):
-            assert isinstance(job_obj, FlatCAMCNCjob), \
-                "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
-
-            # get the tool_table items in a list of row items
-            tool_table_items = self.get_selected_tools_table_items()
-            # insert an information only element in the front
-            tool_table_items.insert(0, [_("Tool_nr"), _("Diameter"), _("Drills_Nr"), _("Slots_Nr")])
-
-            # ## Add properties to the object
-
-            job_obj.origin_kind = 'excellon'
-
-            job_obj.options['Tools_in_use'] = tool_table_items
-            job_obj.options['type'] = 'Excellon'
-            job_obj.options['ppname_e'] = pp_excellon_name
-
-            app_obj.progress.emit(20)
-            job_obj.z_cut = float(self.options["drillz"])
-            job_obj.tool_offset = self.tool_offset
-            job_obj.z_move = float(self.options["travelz"])
-            job_obj.feedrate = float(self.options["feedrate"])
-            job_obj.feedrate_rapid = float(self.options["feedrate_rapid"])
-
-            job_obj.spindlespeed = float(self.options["spindlespeed"]) if self.options["spindlespeed"] else None
-            job_obj.spindledir = self.app.defaults['excellon_spindledir']
-            job_obj.dwell = self.options["dwell"]
-            job_obj.dwelltime = float(self.options["dwelltime"])
-
-            job_obj.pp_excellon_name = pp_excellon_name
-
-            job_obj.toolchange_xy_type = "excellon"
-            job_obj.coords_decimals = int(self.app.defaults["cncjob_coords_decimals"])
-            job_obj.fr_decimals = int(self.app.defaults["cncjob_fr_decimals"])
-
-            job_obj.options['xmin'] = xmin
-            job_obj.options['ymin'] = ymin
-            job_obj.options['xmax'] = xmax
-            job_obj.options['ymax'] = ymax
-
-            try:
-                job_obj.z_pdepth = float(self.options["z_pdepth"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit(
-                        _('[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] or self.options["z_pdepth"]'))
-
-            try:
-                job_obj.feedrate_probe = float(self.options["feedrate_probe"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit(
-                        _('[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
-                          'or self.options["feedrate_probe"]'))
-
-            # There could be more than one drill size...
-            # job_obj.tooldia =   # TODO: duplicate variable!
-            # job_obj.options["tooldia"] =
-
-            tools_csv = ','.join(tools)
-            ret_val = job_obj.generate_from_excellon_by_tool(self, tools_csv,
-                                                             drillz=float(self.options['drillz']),
-                                                             toolchange=self.options["toolchange"],
-                                                             toolchangexy=self.app.defaults["excellon_toolchangexy"],
-                                                             toolchangez=float(self.options["toolchangez"]),
-                                                             startz=float(self.options["startz"]) if
-                                                             self.options["startz"] else None,
-                                                             endz=float(self.options["endz"]),
-                                                             excellon_optimization_type=self.app.defaults[
-                                                                 "excellon_optimization_type"])
-            if ret_val == 'fail':
-                return 'fail'
-            app_obj.progress.emit(50)
-            job_obj.gcode_parse()
-
-            app_obj.progress.emit(60)
-            job_obj.create_geometry()
-
-            app_obj.progress.emit(80)
-
-        # To be run in separate thread
-        def job_thread(app_obj):
-            with self.app.proc_container.new(_("Generating CNC Code")):
-                app_obj.new_object("cncjob", job_name, job_init)
-                app_obj.progress.emit(100)
-
-        # Create promise for the new name.
-        self.app.collection.promise(job_name)
-
-        # Send to worker
-        # self.app.worker.add_task(job_thread, [self.app])
-        self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
-
-    def convert_units(self, units):
-        factor = Excellon.convert_units(self, units)
-
-        self.options['drillz'] = float(self.options['drillz']) * factor
-        self.options['travelz'] = float(self.options['travelz']) * factor
-        self.options['feedrate'] = float(self.options['feedrate']) * factor
-        self.options['feedrate_rapid'] = float(self.options['feedrate_rapid']) * factor
-        self.options['toolchangez'] = float(self.options['toolchangez']) * factor
-
-        if self.app.defaults["excellon_toolchangexy"] == '':
-            self.options['toolchangexy'] = "0.0, 0.0"
-        else:
-            coords_xy = [float(eval(coord)) for coord in self.app.defaults["excellon_toolchangexy"].split(",")]
-            if len(coords_xy) < 2:
-                self.app.inform.emit(_("[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
-                                       "in the format (x, y) \nbut now there is only one value, not two. "))
-                return 'fail'
-            coords_xy[0] *= factor
-            coords_xy[1] *= factor
-            self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
-
-        if self.options['startz'] is not None:
-            self.options['startz'] = float(self.options['startz']) * factor
-        self.options['endz'] = float(self.options['endz']) * factor
-
-    def on_solid_cb_click(self, *args):
-        if self.muted_ui:
-            return
-        self.read_form_item('solid')
-        self.plot()
-
-    def on_plot_cb_click(self, *args):
-        if self.muted_ui:
-            return
-        self.plot()
-        self.read_form_item('plot')
-
-        self.ui_disconnect()
-        cb_flag = self.ui.plot_cb.isChecked()
-        for row in range(self.ui.tools_table.rowCount() - 2):
-            table_cb = self.ui.tools_table.cellWidget(row, 5)
-            if cb_flag:
-                table_cb.setChecked(True)
-            else:
-                table_cb.setChecked(False)
-
-        self.ui_connect()
-
-    def on_plot_cb_click_table(self):
-        # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
-        self.ui_disconnect()
-        # cw = self.sender()
-        # cw_index = self.ui.tools_table.indexAt(cw.pos())
-        # cw_row = cw_index.row()
-        check_row = 0
-
-        self.shapes.clear(update=True)
-        for tool_key in self.tools:
-            solid_geometry = self.tools[tool_key]['solid_geometry']
-
-            # find the geo_tool_table row associated with the tool_key
-            for row in range(self.ui.tools_table.rowCount()):
-                tool_item = int(self.ui.tools_table.item(row, 0).text())
-                if tool_item == int(tool_key):
-                    check_row = row
-                    break
-            if self.ui.tools_table.cellWidget(check_row, 5).isChecked():
-                self.options['plot'] = True
-                # self.plot_element(element=solid_geometry, visible=True)
-                # Plot excellon (All polygons?)
-                if self.options["solid"]:
-                    for geo in solid_geometry:
-                        self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF',
-                                       visible=self.options['plot'],
-                                       layer=2)
-                else:
-                    for geo in solid_geometry:
-                        self.add_shape(shape=geo.exterior, color='red', visible=self.options['plot'])
-                        for ints in geo.interiors:
-                            self.add_shape(shape=ints, color='green', visible=self.options['plot'])
-        self.shapes.redraw()
-
-        # make sure that the general plot is disabled if one of the row plot's are disabled and
-        # if all the row plot's are enabled also enable the general plot checkbox
-        cb_cnt = 0
-        total_row = self.ui.tools_table.rowCount()
-        for row in range(total_row - 2):
-            if self.ui.tools_table.cellWidget(row, 5).isChecked():
-                cb_cnt += 1
-            else:
-                cb_cnt -= 1
-        if cb_cnt < total_row - 2:
-            self.ui.plot_cb.setChecked(False)
-        else:
-            self.ui.plot_cb.setChecked(True)
-        self.ui_connect()
-
-    # def plot_element(self, element, color='red', visible=None, layer=None):
-    #
-    #     visible = visible if visible else self.options['plot']
-    #
-    #     try:
-    #         for sub_el in element:
-    #             self.plot_element(sub_el)
-    #
-    #     except TypeError:  # Element is not iterable...
-    #         self.add_shape(shape=element, color=color, visible=visible, layer=0)
-
-    def plot(self, kind=None):
-
-        # Does all the required setup and returns False
-        # if the 'ptint' option is set to False.
-        if not FlatCAMObj.plot(self):
-            return
-
-        # try:
-        #     # Plot Excellon (All polygons?)
-        #     if self.options["solid"]:
-        #         for tool in self.tools:
-        #             for geo in self.tools[tool]['solid_geometry']:
-        #                 self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF',
-        #                                visible=self.options['plot'],
-        #                                layer=2)
-        #     else:
-        #         for tool in self.tools:
-        #             for geo in self.tools[tool]['solid_geometry']:
-        #                 self.add_shape(shape=geo.exterior, color='red', visible=self.options['plot'])
-        #                 for ints in geo.interiors:
-        #                     self.add_shape(shape=ints, color='orange', visible=self.options['plot'])
-        #
-        #     self.shapes.redraw()
-        #     return
-        # except (ObjectDeleted, AttributeError, KeyError):
-        #     self.shapes.clear(update=True)
-
-        # this stays for compatibility reasons, in case we try to open old projects
-        try:
-            __ = iter(self.solid_geometry)
-        except TypeError:
-            self.solid_geometry = [self.solid_geometry]
-
-        try:
-            # Plot Excellon (All polygons?)
-            if self.options["solid"]:
-                for geo in self.solid_geometry:
-                    self.add_shape(shape=geo, color='#750000BF', face_color='#C40000BF',
-                                   visible=self.options['plot'],
-                                   layer=2)
-            else:
-                for geo in self.solid_geometry:
-                    self.add_shape(shape=geo.exterior, color='red', visible=self.options['plot'])
-                    for ints in geo.interiors:
-                        self.add_shape(shape=ints, color='orange', visible=self.options['plot'])
-
-            self.shapes.redraw()
-        except (ObjectDeleted, AttributeError):
-            self.shapes.clear(update=True)
-
-
-class FlatCAMGeometry(FlatCAMObj, Geometry):
-    """
-    Geometric object not associated with a specific
-    format.
-    """
-    optionChanged = QtCore.pyqtSignal(str)
-    ui_type = GeometryObjectUI
-
-    def merge(self, geo_list, geo_final, multigeo=None):
-        """
-        Merges the geometry of objects in grb_list into
-        the geometry of geo_final.
-
-        :param geo_list: List of FlatCAMGerber Objects to join.
-        :param geo_final: Destination FlatCAMGerber object.
-        :return: None
-        """
-
-        if geo_final.solid_geometry is None:
-            geo_final.solid_geometry = []
-
-        if type(geo_final.solid_geometry) is not list:
-            geo_final.solid_geometry = [geo_final.solid_geometry]
-
-        for geo in geo_list:
-            for option in geo.options:
-                if option is not 'name':
-                    try:
-                        geo_final.options[option] = geo.options[option]
-                    except Exception as e:
-                        log.warning("Failed to copy option %s. Error: %s" % (str(option), str(e)))
-
-            # Expand lists
-            if type(geo) is list:
-                FlatCAMGeometry.merge(self, geo_list=geo, geo_final=geo_final)
-            # If not list, just append
-            else:
-                # merge solid_geometry, useful for singletool geometry, for multitool each is empty
-                if multigeo is None or multigeo is False:
-                    geo_final.multigeo = False
-                    try:
-                        geo_final.solid_geometry.append(geo.solid_geometry)
-                    except Exception as e:
-                        log.debug("FlatCAMGeometry.merge() --> %s" % str(e))
-                else:
-                    geo_final.multigeo = True
-                    # if multigeo the solid_geometry is empty in the object attributes because it now lives in the
-                    # tools object attribute, as a key value
-                    geo_final.solid_geometry = []
-
-                # find the tool_uid maximum value in the geo_final
-                geo_final_uid_list = []
-                for key in geo_final.tools:
-                    geo_final_uid_list.append(int(key))
-
-                try:
-                    max_uid = max(geo_final_uid_list, key=int)
-                except ValueError:
-                    max_uid = 0
-
-                # add and merge tools. If what we try to merge as Geometry is Excellon's and/or Gerber's then don't try
-                # to merge the obj.tools as it is likely there is none to merge.
-                if not isinstance(geo, FlatCAMGerber) and not isinstance(geo, FlatCAMExcellon):
-                    for tool_uid in geo.tools:
-                        max_uid += 1
-                        geo_final.tools[max_uid] = deepcopy(geo.tools[tool_uid])
-
-    @staticmethod
-    def get_pts(o):
-        """
-        Returns a list of all points in the object, where
-        the object can be a MultiPolygon, Polygon, Not a polygon, or a list
-        of such. Search is done recursively.
-
-        :param: geometric object
-        :return: List of points
-        :rtype: list
-        """
-        pts = []
-
-        # Iterable: descend into each item.
-        try:
-            for subo in o:
-                pts += FlatCAMGeometry.get_pts(subo)
-
-        # Non-iterable
-        except TypeError:
-            if o is not None:
-                if type(o) == MultiPolygon:
-                    for poly in o:
-                        pts += FlatCAMGeometry.get_pts(poly)
-                # ## Descend into .exerior and .interiors
-                elif type(o) == Polygon:
-                    pts += FlatCAMGeometry.get_pts(o.exterior)
-                    for i in o.interiors:
-                        pts += FlatCAMGeometry.get_pts(i)
-                elif type(o) == MultiLineString:
-                    for line in o:
-                        pts += FlatCAMGeometry.get_pts(line)
-                # ## Has .coords: list them.
-                else:
-                    pts += list(o.coords)
-            else:
-                return
-        return pts
-
-    def __init__(self, name):
-        FlatCAMObj.__init__(self, name)
-        Geometry.__init__(self, geo_steps_per_circle=int(self.app.defaults["geometry_circle_steps"]))
-
-        self.kind = "geometry"
-
-        self.options.update({
-            "plot": True,
-            "cutz": -0.002,
-            "vtipdia": 0.1,
-            "vtipangle": 30,
-            "travelz": 0.1,
-            "feedrate": 5.0,
-            "feedrate_z": 5.0,
-            "feedrate_rapid": 5.0,
-            "spindlespeed": None,
-            "dwell": True,
-            "dwelltime": 1000,
-            "multidepth": False,
-            "depthperpass": 0.002,
-            "extracut": False,
-            "endz": 2.0,
-            "toolchange": False,
-            "toolchangez": 1.0,
-            "toolchangexy": "0.0, 0.0",
-            "startz": None,
-            "ppname_g": 'default',
-            "z_pdepth": -0.02,
-            "feedrate_probe": 3.0,
-        })
-
-        if "cnctooldia" not in self.options:
-            self.options["cnctooldia"] = self.app.defaults["geometry_cnctooldia"]
-
-        self.options["startz"] = self.app.defaults["geometry_startz"]
-
-        # this will hold the tool unique ID that is useful when having multiple tools with same diameter
-        self.tooluid = 0
-
-        '''
-            self.tools = {}
-            This is a dictionary. Each dict key is associated with a tool used in geo_tools_table. The key is the 
-            tool_id of the tools and the value is another dict that will hold the data under the following form:
-                {tooluid:   {
-                            'tooldia': 1,
-                            'offset': 'Path',
-                            'offset_value': 0.0
-                            'type': 'Rough',
-                            'tool_type': 'C1',
-                            'data': self.default_tool_data
-                            'solid_geometry': []
-                            }
-                }
-        '''
-        self.tools = {}
-
-        # this dict is to store those elements (tools) of self.tools that are selected in the self.geo_tools_table
-        # those elements are the ones used for generating GCode
-        self.sel_tools = {}
-
-        self.offset_item_options = ["Path", "In", "Out", "Custom"]
-        self.type_item_options = [_("Iso"), _("Rough"), _("Finish")]
-        self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
-
-        # flag to store if the V-Shape tool is selected in self.ui.geo_tools_table
-        self.v_tool_type = None
-
-        # flag to store if the Geometry is type 'multi-geometry' meaning that each tool has it's own geometry
-        # the default value is False
-        self.multigeo = False
-
-        # flag to store if the geometry is part of a special group of geometries that can't be processed by the default
-        # engine of FlatCAM. Most likely are generated by some of tools and are special cases of geometries.
-        self. special_group = None
-
-        self.old_pp_state = ''
-        self.old_toolchangeg_state = ''
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from predecessors.
-        self.ser_attrs += ['options', 'kind', 'tools', 'multigeo']
-
-    def build_ui(self):
-        self.ui_disconnect()
-        FlatCAMObj.build_ui(self)
-
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        offset = 0
-        tool_idx = 0
-
-        n = len(self.tools)
-        self.ui.geo_tools_table.setRowCount(n)
-
-        for tooluid_key, tooluid_value in self.tools.items():
-            tool_idx += 1
-            row_no = tool_idx - 1
-
-            id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
-            id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.ui.geo_tools_table.setItem(row_no, 0, id)  # Tool name/id
-
-            # Make sure that the tool diameter when in MM is with no more than 2 decimals.
-            # There are no tool bits in MM with more than 3 decimals diameter.
-            # For INCH the decimals should be no more than 3. There are no tools under 10mils.
-            if self.units == 'MM':
-                dia_item = QtWidgets.QTableWidgetItem('%.2f' % float(tooluid_value['tooldia']))
-            else:
-                dia_item = QtWidgets.QTableWidgetItem('%.4f' % float(tooluid_value['tooldia']))
-
-            dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            offset_item = QtWidgets.QComboBox()
-            for item in self.offset_item_options:
-                offset_item.addItem(item)
-            offset_item.setStyleSheet('background-color: rgb(255,255,255)')
-            idx = offset_item.findText(tooluid_value['offset'])
-            offset_item.setCurrentIndex(idx)
-
-            type_item = QtWidgets.QComboBox()
-            for item in self.type_item_options:
-                type_item.addItem(item)
-            type_item.setStyleSheet('background-color: rgb(255,255,255)')
-            idx = type_item.findText(tooluid_value['type'])
-            type_item.setCurrentIndex(idx)
-
-            tool_type_item = QtWidgets.QComboBox()
-            for item in self.tool_type_item_options:
-                tool_type_item.addItem(item)
-                tool_type_item.setStyleSheet('background-color: rgb(255,255,255)')
-            idx = tool_type_item.findText(tooluid_value['tool_type'])
-            tool_type_item.setCurrentIndex(idx)
-
-            tool_uid_item = QtWidgets.QTableWidgetItem(str(tooluid_key))
-
-            plot_item = FCCheckBox()
-            plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
-            if self.ui.plot_cb.isChecked():
-                plot_item.setChecked(True)
-
-            self.ui.geo_tools_table.setItem(row_no, 1, dia_item)  # Diameter
-            self.ui.geo_tools_table.setCellWidget(row_no, 2, offset_item)
-            self.ui.geo_tools_table.setCellWidget(row_no, 3, type_item)
-            self.ui.geo_tools_table.setCellWidget(row_no, 4, tool_type_item)
-
-            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY ###
-            self.ui.geo_tools_table.setItem(row_no, 5, tool_uid_item)  # Tool unique ID
-            self.ui.geo_tools_table.setCellWidget(row_no, 6, plot_item)
-
-            try:
-                self.ui.tool_offset_entry.set_value(tooluid_value['offset_value'])
-            except Exception as e:
-                log.debug("build_ui() --> Could not set the 'offset_value' key in self.tools. Error: %s" % str(e))
-
-        # make the diameter column editable
-        for row in range(tool_idx):
-            self.ui.geo_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
-                                                          QtCore.Qt.ItemIsEditable |
-                                                          QtCore.Qt.ItemIsEnabled)
-
-        # sort the tool diameter column
-        # self.ui.geo_tools_table.sortItems(1)
-        # all the tools are selected by default
-        # self.ui.geo_tools_table.selectColumn(0)
-
-        self.ui.geo_tools_table.resizeColumnsToContents()
-        self.ui.geo_tools_table.resizeRowsToContents()
-
-        vertical_header = self.ui.geo_tools_table.verticalHeader()
-        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
-        vertical_header.hide()
-        self.ui.geo_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-
-        horizontal_header = self.ui.geo_tools_table.horizontalHeader()
-        horizontal_header.setMinimumSectionSize(10)
-        horizontal_header.setDefaultSectionSize(70)
-        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(0, 20)
-        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
-        # horizontal_header.setColumnWidth(2, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(4, 40)
-        horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(4, 17)
-        # horizontal_header.setStretchLastSection(True)
-        self.ui.geo_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-
-        self.ui.geo_tools_table.setColumnWidth(0, 20)
-        self.ui.geo_tools_table.setColumnWidth(4, 40)
-        self.ui.geo_tools_table.setColumnWidth(6, 17)
-
-        # self.ui.geo_tools_table.setSortingEnabled(True)
-
-        self.ui.geo_tools_table.setMinimumHeight(self.ui.geo_tools_table.getHeight())
-        self.ui.geo_tools_table.setMaximumHeight(self.ui.geo_tools_table.getHeight())
-
-        # update UI for all rows - useful after units conversion but only if there is at least one row
-        row_cnt = self.ui.geo_tools_table.rowCount()
-        if row_cnt > 0:
-            for r in range(row_cnt):
-                self.update_ui(r)
-
-        # select only the first tool / row
-        selected_row = 0
-        try:
-            self.select_tools_table_row(selected_row, clearsel=True)
-            # update the Geometry UI
-            self.update_ui()
-        except Exception as e:
-            # when the tools table is empty there will be this error but once the table is populated it will go away
-            log.debug(str(e))
-
-        # disable the Plot column in Tool Table if the geometry is SingleGeo as it is not needed
-        # and can create some problems
-        if self.multigeo is False:
-            self.ui.geo_tools_table.setColumnHidden(6, True)
-        else:
-            self.ui.geo_tools_table.setColumnHidden(6, False)
-
-        self.set_tool_offset_visibility(selected_row)
-        self.ui_connect()
-
-        # HACK: for whatever reasons the name in Selected tab is reverted to the original one after a successful rename
-        # done in the collection view but only for Geometry objects. Perhaps some references remains. Should be fixed.
-        self.ui.name_entry.set_value(self.options['name'])
-
-    def set_ui(self, ui):
-        FlatCAMObj.set_ui(self, ui)
-
-        log.debug("FlatCAMGeometry.set_ui()")
-
-        assert isinstance(self.ui, GeometryObjectUI), \
-            "Expected a GeometryObjectUI, got %s" % type(self.ui)
-
-        # populate postprocessor names in the combobox
-        for name in list(self.app.postprocessors.keys()):
-            self.ui.pp_geometry_name_cb.addItem(name)
-
-        self.form_fields.update({
-            "plot": self.ui.plot_cb,
-            "cutz": self.ui.cutz_entry,
-            "vtipdia": self.ui.tipdia_entry,
-            "vtipangle": self.ui.tipangle_entry,
-            "travelz": self.ui.travelz_entry,
-            "feedrate": self.ui.cncfeedrate_entry,
-            "feedrate_z": self.ui.cncplunge_entry,
-            "feedrate_rapid": self.ui.cncfeedrate_rapid_entry,
-            "spindlespeed": self.ui.cncspindlespeed_entry,
-            "dwell": self.ui.dwell_cb,
-            "dwelltime": self.ui.dwelltime_entry,
-            "multidepth": self.ui.mpass_cb,
-            "ppname_g": self.ui.pp_geometry_name_cb,
-            "z_pdepth": self.ui.pdepth_entry,
-            "feedrate_probe": self.ui.feedrate_probe_entry,
-            "depthperpass": self.ui.maxdepth_entry,
-            "extracut": self.ui.extracut_cb,
-            "toolchange": self.ui.toolchangeg_cb,
-            "toolchangez": self.ui.toolchangez_entry,
-            "endz": self.ui.gendz_entry,
-        })
-
-        # Fill form fields only on object create
-        self.to_form()
-
-        self.ui.tipdialabel.hide()
-        self.ui.tipdia_entry.hide()
-        self.ui.tipanglelabel.hide()
-        self.ui.tipangle_entry.hide()
-        self.ui.cutz_entry.setDisabled(False)
-
-        # store here the default data for Geometry Data
-        self.default_data = {}
-        self.default_data.update({
-            "name": None,
-            "plot": None,
-            "cutz": None,
-            "vtipdia": None,
-            "vtipangle": None,
-            "travelz": None,
-            "feedrate": None,
-            "feedrate_z": None,
-            "feedrate_rapid": None,
-            "dwell": None,
-            "dwelltime": None,
-            "multidepth": None,
-            "ppname_g": None,
-            "depthperpass": None,
-            "extracut": None,
-            "toolchange": None,
-            "toolchangez": None,
-            "endz": None,
-            "spindlespeed": None,
-            "toolchangexy": None,
-            "startz": None
-        })
-
-        # fill in self.default_data values from self.options
-        for def_key in self.default_data:
-            for opt_key, opt_val in self.options.items():
-                if def_key == opt_key:
-                    self.default_data[def_key] = deepcopy(opt_val)
-
-        try:
-            temp_tools = self.options["cnctooldia"].split(",")
-            tools_list = [
-                float(eval(dia)) for dia in temp_tools if dia != ''
-            ]
-        except Exception as e:
-            log.error("At least one tool diameter needed. Verify in Edit -> Preferences -> Geometry General -> "
-                      "Tool dia. %s" % str(e))
-            return
-
-        self.tooluid += 1
-
-        if not self.tools:
-            for toold in tools_list:
-                self.tools.update({
-                    self.tooluid: {
-                        'tooldia': float(toold),
-                        'offset': 'Path',
-                        'offset_value': 0.0,
-                        'type': _('Rough'),
-                        'tool_type': 'C1',
-                        'data': self.default_data,
-                        'solid_geometry': self.solid_geometry
-                    }
-                })
-                self.tooluid += 1
-        else:
-            # if self.tools is not empty then it can safely be assumed that it comes from an opened project.
-            # Because of the serialization the self.tools list on project save, the dict keys (members of self.tools
-            # are each a dict) are turned into strings so we rebuild the self.tools elements so the keys are
-            # again float type; dict's don't like having keys changed when iterated through therefore the need for the
-            # following convoluted way of changing the keys from string to float type
-            temp_tools = {}
-            new_key = 0.0
-            for tooluid_key in self.tools:
-                val = deepcopy(self.tools[tooluid_key])
-                new_key = deepcopy(int(tooluid_key))
-                temp_tools[new_key] = val
-
-            self.tools.clear()
-            self.tools = deepcopy(temp_tools)
-
-        self.ui.tool_offset_entry.hide()
-        self.ui.tool_offset_lbl.hide()
-
-        # used to store the state of the mpass_cb if the selected postproc for geometry is hpgl
-        self.old_pp_state = self.default_data['multidepth']
-        self.old_toolchangeg_state = self.default_data['toolchange']
-
-        if not isinstance(self.ui, GeometryObjectUI):
-            log.debug("Expected a GeometryObjectUI, got %s" % type(self.ui))
-            return
-
-        self.ui.geo_tools_table.setupContextMenu()
-        self.ui.geo_tools_table.addContextMenu(
-            _("Copy"), self.on_tool_copy, icon=QtGui.QIcon("share/copy16.png"))
-        self.ui.geo_tools_table.addContextMenu(
-            _("Delete"), lambda: self.on_tool_delete(all=None), icon=QtGui.QIcon("share/delete32.png"))
-
-        # Show/Hide Advanced Options
-        if self.app.defaults["global_app_level"] == 'b':
-            self.ui.level.setText(_(
-                '<span style="color:green;"><b>Basic</b></span>'
-            ))
-
-            self.ui.geo_tools_table.setColumnHidden(2, True)
-            self.ui.geo_tools_table.setColumnHidden(3, True)
-            # self.ui.geo_tools_table.setColumnHidden(4, True)
-            self.ui.addtool_entry_lbl.hide()
-            self.ui.addtool_entry.hide()
-            self.ui.addtool_btn.hide()
-            self.ui.copytool_btn.hide()
-            self.ui.deltool_btn.hide()
-            self.ui.endzlabel.hide()
-            self.ui.gendz_entry.hide()
-            self.ui.fr_rapidlabel.hide()
-            self.ui.cncfeedrate_rapid_entry.hide()
-            self.ui.extracut_cb.hide()
-            self.ui.pdepth_label.hide()
-            self.ui.pdepth_entry.hide()
-            self.ui.feedrate_probe_label.hide()
-            self.ui.feedrate_probe_entry.hide()
-        else:
-            self.ui.level.setText(_(
-                '<span style="color:red;"><b>Advanced</b></span>'
-            ))
-
-        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
-        self.ui.generate_cnc_button.clicked.connect(self.on_generatecnc_button_click)
-        self.ui.paint_tool_button.clicked.connect(lambda: self.app.paint_tool.run(toggle=False))
-        self.ui.pp_geometry_name_cb.activated.connect(self.on_pp_changed)
-        self.ui.addtool_entry.returnPressed.connect(lambda: self.on_tool_add())
-
-    def set_tool_offset_visibility(self, current_row):
-        if current_row is None:
-            return
-        try:
-            tool_offset = self.ui.geo_tools_table.cellWidget(current_row, 2)
-            if tool_offset is not None:
-                tool_offset_txt = tool_offset.currentText()
-                if tool_offset_txt == 'Custom':
-                    self.ui.tool_offset_entry.show()
-                    self.ui.tool_offset_lbl.show()
-                else:
-                    self.ui.tool_offset_entry.hide()
-                    self.ui.tool_offset_lbl.hide()
-        except Exception as e:
-            log.debug("set_tool_offset_visibility() --> " + str(e))
-            return
-
-    def on_offset_value_edited(self):
-        """
-        This will save the offset_value into self.tools storage whenever the offset value is edited
-        :return:
-        """
-
-        for current_row in self.ui.geo_tools_table.selectedItems():
-            # sometime the header get selected and it has row number -1
-            # we don't want to do anything with the header :)
-            if current_row.row() < 0:
-                continue
-            tool_uid = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
-            self.set_tool_offset_visibility(current_row.row())
-
-            for tooluid_key, tooluid_value in self.tools.items():
-                if int(tooluid_key) == tool_uid:
-                    try:
-                        tooluid_value['offset_value'] = float(self.ui.tool_offset_entry.get_value())
-                    except ValueError:
-                        # try to convert comma to decimal point. if it's still not working error message and return
-                        try:
-                            tooluid_value['offset_value'] = float(
-                                self.ui.tool_offset_entry.get_value().replace(',', '.')
-                            )
-                        except ValueError:
-                            self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
-                                                   "use a number."))
-                            return
-
-    def ui_connect(self):
-        # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
-        # changes in geometry UI
-        for i in range(self.ui.grid3.count()):
-            current_widget = self.ui.grid3.itemAt(i).widget()
-            if isinstance(current_widget, FCCheckBox):
-                current_widget.stateChanged.connect(self.gui_form_to_storage)
-            elif isinstance(current_widget, FCComboBox):
-                current_widget.currentIndexChanged.connect(self.gui_form_to_storage)
-            elif isinstance(current_widget, FloatEntry) or isinstance(current_widget, LengthEntry) or \
-                    isinstance(current_widget, FCEntry) or isinstance(current_widget, IntEntry):
-                current_widget.editingFinished.connect(self.gui_form_to_storage)
-
-        for row in range(self.ui.geo_tools_table.rowCount()):
-            for col in [2, 3, 4]:
-                self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.connect(
-                    self.on_tooltable_cellwidget_change)
-
-        # I use lambda's because the connected functions have parameters that could be used in certain scenarios
-        self.ui.addtool_btn.clicked.connect(lambda: self.on_tool_add())
-
-        self.ui.copytool_btn.clicked.connect(lambda: self.on_tool_copy())
-        self.ui.deltool_btn.clicked.connect(lambda: self.on_tool_delete())
-
-        self.ui.geo_tools_table.currentItemChanged.connect(self.on_row_selection_change)
-        self.ui.geo_tools_table.itemChanged.connect(self.on_tool_edit)
-        self.ui.tool_offset_entry.editingFinished.connect(self.on_offset_value_edited)
-
-        for row in range(self.ui.geo_tools_table.rowCount()):
-            self.ui.geo_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
-        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
-
-    def ui_disconnect(self):
-
-        # on any change to the widgets that matter it will be called self.gui_form_to_storage which will save the
-        # changes in geometry UI
-        for i in range(self.ui.grid3.count()):
-            current_widget = self.ui.grid3.itemAt(i).widget()
-            if isinstance(current_widget, FCCheckBox):
-                try:
-                    self.ui.grid3.itemAt(i).widget().stateChanged.disconnect(self.gui_form_to_storage)
-                except (TypeError, AttributeError):
-                    pass
-            elif isinstance(current_widget, FCComboBox):
-                try:
-                    self.ui.grid3.itemAt(i).widget().currentIndexChanged.disconnect(self.gui_form_to_storage)
-                except (TypeError, AttributeError):
-                    pass
-            elif isinstance(current_widget, LengthEntry) or isinstance(current_widget, IntEntry) or \
-                    isinstance(current_widget, FCEntry) or isinstance(current_widget, FloatEntry):
-                try:
-                    self.ui.grid3.itemAt(i).widget().editingFinished.disconnect(self.gui_form_to_storage)
-                except (TypeError, AttributeError):
-                    pass
-
-        for row in range(self.ui.geo_tools_table.rowCount()):
-            for col in [2, 3, 4]:
-                try:
-                    self.ui.geo_tools_table.cellWidget(row, col).currentIndexChanged.disconnect()
-                except (TypeError, AttributeError):
-                    pass
-
-        try:
-            self.ui.addtool_btn.clicked.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        try:
-            self.ui.copytool_btn.clicked.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        try:
-            self.ui.deltool_btn.clicked.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        try:
-            self.ui.geo_tools_table.currentItemChanged.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        try:
-            self.ui.geo_tools_table.itemChanged.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        try:
-            self.ui.tool_offset_entry.editingFinished.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-        for row in range(self.ui.geo_tools_table.rowCount()):
-            try:
-                self.ui.geo_tools_table.cellWidget(row, 6).clicked.disconnect()
-            except (TypeError, AttributeError):
-                pass
-
-        try:
-            self.ui.plot_cb.stateChanged.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
-    def on_tool_add(self, dia=None):
-        self.ui_disconnect()
-
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        # if a Tool diameter entered is a char instead a number the final message of Tool adding is changed
-        # because the Default value for Tool is used.
-        change_message = False
-
-        if dia is not None:
-            tooldia = dia
-        else:
-            try:
-                tooldia = float(self.ui.addtool_entry.get_value())
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    tooldia = float(self.ui.addtool_entry.get_value().replace(',', '.'))
-                except ValueError:
-                    change_message = True
-                    tooldia = float(self.options["cnctooldia"][0])
-
-            if tooldia is None:
-                self.build_ui()
-                self.app.inform.emit(_(
-                    "[ERROR_NOTCL] Please enter the desired tool diameter in Float format."
-                ))
-                return
-
-        # construct a list of all 'tooluid' in the self.tools
-        tool_uid_list = []
-        for tooluid_key in self.tools:
-            tool_uid_item = int(tooluid_key)
-            tool_uid_list.append(tool_uid_item)
-
-        # find maximum from the temp_uid, add 1 and this is the new 'tooluid'
-        if not tool_uid_list:
-            max_uid = 0
-        else:
-            max_uid = max(tool_uid_list)
-        self.tooluid = max_uid + 1
-
-        if self.units == 'IN':
-            tooldia = float('%.4f' % tooldia)
-        else:
-            tooldia = float('%.2f' % tooldia)
-
-        # here we actually add the new tool; if there is no tool in the tool table we add a tool with default data
-        # otherwise we add a tool with data copied from last tool
-        if not self.tools:
-            self.tools.update({
-                self.tooluid: {
-                    'tooldia': tooldia,
-                    'offset': 'Path',
-                    'offset_value': 0.0,
-                    'type': _('Rough'),
-                    'tool_type': 'C1',
-                    'data': deepcopy(self.default_data),
-                    'solid_geometry': self.solid_geometry
-                }
-            })
-        else:
-            last_data = self.tools[max_uid]['data']
-            last_offset = self.tools[max_uid]['offset']
-            last_offset_value = self.tools[max_uid]['offset_value']
-            last_type = self.tools[max_uid]['type']
-            last_tool_type = self.tools[max_uid]['tool_type']
-            last_solid_geometry = self.tools[max_uid]['solid_geometry']
-
-            # if previous geometry was empty (it may happen for the first tool added)
-            # then copy the object.solid_geometry
-            if not last_solid_geometry:
-                last_solid_geometry = self.solid_geometry
-
-            self.tools.update({
-                self.tooluid: {
-                    'tooldia': tooldia,
-                    'offset': last_offset,
-                    'offset_value': last_offset_value,
-                    'type': last_type,
-                    'tool_type': last_tool_type,
-                    'data': deepcopy(last_data),
-                    'solid_geometry': deepcopy(last_solid_geometry)
-                }
-            })
-
-        self.tools[self.tooluid]['data']['name'] = self.options['name']
-
-        self.ui.tool_offset_entry.hide()
-        self.ui.tool_offset_lbl.hide()
-
-        # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
-        try:
-            self.ser_attrs.remove('tools')
-        except TypeError:
-            pass
-        self.ser_attrs.append('tools')
-
-        if change_message is False:
-            self.app.inform.emit(_("[success] Tool added in Tool Table."))
-        else:
-            change_message = False
-            self.app.inform.emit(_("[WARNING_NOTCL] Default Tool added. Wrong value format entered."))
-        self.build_ui()
-
-        # if there is no tool left in the Tools Table, enable the parameters GUI
-        if self.ui.geo_tools_table.rowCount() != 0:
-            self.ui.geo_param_frame.setDisabled(False)
-
-    def on_tool_copy(self, all=None):
-        self.ui_disconnect()
-
-        # find the tool_uid maximum value in the self.tools
-        uid_list = []
-        for key in self.tools:
-            uid_list.append(int(key))
-        try:
-            max_uid = max(uid_list, key=int)
-        except ValueError:
-            max_uid = 0
-
-        if all is None:
-            if self.ui.geo_tools_table.selectedItems():
-                for current_row in self.ui.geo_tools_table.selectedItems():
-                    # sometime the header get selected and it has row number -1
-                    # we don't want to do anything with the header :)
-                    if current_row.row() < 0:
-                        continue
-                    try:
-                        tooluid_copy = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
-                        self.set_tool_offset_visibility(current_row.row())
-                        max_uid += 1
-                        self.tools[int(max_uid)] = deepcopy(self.tools[tooluid_copy])
-                    except AttributeError:
-                        self.app.inform.emit(_("[WARNING_NOTCL] Failed. Select a tool to copy."))
-                        self.build_ui()
-                        return
-                    except Exception as e:
-                        log.debug("on_tool_copy() --> " + str(e))
-                # deselect the table
-                # self.ui.geo_tools_table.clearSelection()
-            else:
-                self.app.inform.emit(_("[WARNING_NOTCL] Failed. Select a tool to copy."))
-                self.build_ui()
-                return
-        else:
-            # we copy all tools in geo_tools_table
-            try:
-                temp_tools = deepcopy(self.tools)
-                max_uid += 1
-                for tooluid in temp_tools:
-                    self.tools[int(max_uid)] = deepcopy(temp_tools[tooluid])
-                temp_tools.clear()
-            except Exception as e:
-                log.debug("on_tool_copy() --> " + str(e))
-
-        # if there are no more tools in geo tools table then hide the tool offset
-        if not self.tools:
-            self.ui.tool_offset_entry.hide()
-            self.ui.tool_offset_lbl.hide()
-
-        # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
-        try:
-            self.ser_attrs.remove('tools')
-        except ValueError:
-            pass
-        self.ser_attrs.append('tools')
-
-        self.build_ui()
-        self.app.inform.emit(_("[success] Tool was copied in Tool Table."))
-
-    def on_tool_edit(self, current_item):
-
-        self.ui_disconnect()
-
-        current_row = current_item.row()
-        try:
-            d = float(self.ui.geo_tools_table.item(current_row, 1).text())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                d = float(self.ui.geo_tools_table.item(current_row, 1).text().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
-                                       "use a number."))
-                return
-
-        tool_dia = float('%.4f' % d)
-        tooluid = int(self.ui.geo_tools_table.item(current_row, 5).text())
-
-        self.tools[tooluid]['tooldia'] = tool_dia
-
-        try:
-            self.ser_attrs.remove('tools')
-            self.ser_attrs.append('tools')
-        except (TypeError, ValueError):
-            pass
-
-        self.app.inform.emit(_("[success] Tool was edited in Tool Table."))
-        self.build_ui()
-
-    def on_tool_delete(self, all=None):
-        self.ui_disconnect()
-
-        if all is None:
-            if self.ui.geo_tools_table.selectedItems():
-                for current_row in self.ui.geo_tools_table.selectedItems():
-                    # sometime the header get selected and it has row number -1
-                    # we don't want to do anything with the header :)
-                    if current_row.row() < 0:
-                        continue
-                    try:
-                        tooluid_del = int(self.ui.geo_tools_table.item(current_row.row(), 5).text())
-                        self.set_tool_offset_visibility(current_row.row())
-
-                        temp_tools = deepcopy(self.tools)
-                        for tooluid_key in self.tools:
-                            if int(tooluid_key) == tooluid_del:
-                                # if the self.tools has only one tool and we delete it then we move the solid_geometry
-                                # as a property of the object otherwise there will be nothing to hold it
-                                if len(self.tools) == 1:
-                                    self.solid_geometry = deepcopy(self.tools[tooluid_key]['solid_geometry'])
-                                temp_tools.pop(tooluid_del, None)
-                        self.tools = deepcopy(temp_tools)
-                        temp_tools.clear()
-                    except AttributeError:
-                        self.app.inform.emit(_("[WARNING_NOTCL] Failed. Select a tool to delete."))
-                        self.build_ui()
-                        return
-                    except Exception as e:
-                        log.debug("on_tool_delete() --> " + str(e))
-                # deselect the table
-                # self.ui.geo_tools_table.clearSelection()
-            else:
-                self.app.inform.emit(_("[WARNING_NOTCL] Failed. Select a tool to delete."))
-                self.build_ui()
-                return
-        else:
-            # we delete all tools in geo_tools_table
-            self.tools.clear()
-
-        self.app.plot_all()
-
-        # if there are no more tools in geo tools table then hide the tool offset
-        if not self.tools:
-            self.ui.tool_offset_entry.hide()
-            self.ui.tool_offset_lbl.hide()
-
-        # we do this HACK to make sure the tools attribute to be serialized is updated in the self.ser_attrs list
-        try:
-            self.ser_attrs.remove('tools')
-        except TypeError:
-            pass
-        self.ser_attrs.append('tools')
-
-        self.build_ui()
-        self.app.inform.emit(_("[success] Tool was deleted in Tool Table."))
-
-        obj_active = self.app.collection.get_active()
-        # if the object was MultiGeo and now it has no tool at all (therefore no geometry)
-        # we make it back SingleGeo
-        if self.ui.geo_tools_table.rowCount() <= 0:
-            obj_active.multigeo = False
-            obj_active.options['xmin'] = 0
-            obj_active.options['ymin'] = 0
-            obj_active.options['xmax'] = 0
-            obj_active.options['ymax'] = 0
-
-        if obj_active.multigeo is True:
-            try:
-                xmin, ymin, xmax, ymax = obj_active.bounds()
-                obj_active.options['xmin'] = xmin
-                obj_active.options['ymin'] = ymin
-                obj_active.options['xmax'] = xmax
-                obj_active.options['ymax'] = ymax
-            except Exception as e:
-                obj_active.options['xmin'] = 0
-                obj_active.options['ymin'] = 0
-                obj_active.options['xmax'] = 0
-                obj_active.options['ymax'] = 0
-
-        # if there is no tool left in the Tools Table, disable the parameters GUI
-        if self.ui.geo_tools_table.rowCount() == 0:
-            self.ui.geo_param_frame.setDisabled(True)
-
-    def on_row_selection_change(self):
-        self.update_ui()
-
-    def update_ui(self, row=None):
-        self.ui_disconnect()
-
-        if row is None:
-            try:
-                current_row = self.ui.geo_tools_table.currentRow()
-            except Exception as e:
-                current_row = 0
-        else:
-            current_row = row
-
-        if current_row < 0:
-            current_row = 0
-
-        self.set_tool_offset_visibility(current_row)
-
-        # populate the form with the data from the tool associated with the row parameter
-        try:
-            item = self.ui.geo_tools_table.item(current_row, 5)
-            if type(item) is not None:
-                tooluid = int(item.text())
-            else:
-                return
-        except Exception as e:
-            log.debug("Tool missing. Add a tool in Geo Tool Table. %s" % str(e))
-            return
-
-        # update the form with the V-Shape fields if V-Shape selected in the geo_tool_table
-        # also modify the Cut Z form entry to reflect the calculated Cut Z from values got from V-Shape Fields
-        try:
-            item = self.ui.geo_tools_table.cellWidget(current_row, 4)
-            if item is not None:
-                tool_type_txt = item.currentText()
-                self.ui_update_v_shape(tool_type_txt=tool_type_txt)
-            else:
-                return
-        except Exception as e:
-            log.debug("Tool missing in ui_update_v_shape(). Add a tool in Geo Tool Table. %s" % str(e))
-            return
-
-        try:
-            # set the form with data from the newly selected tool
-            for tooluid_key, tooluid_value in self.tools.items():
-                if int(tooluid_key) == tooluid:
-                    for key, value in tooluid_value.items():
-                        if key == 'data':
-                            form_value_storage = tooluid_value[key]
-                            self.update_form(form_value_storage)
-                        if key == 'offset_value':
-                            # update the offset value in the entry even if the entry is hidden
-                            self.ui.tool_offset_entry.set_value(tooluid_value[key])
-
-                        if key == 'tool_type' and value == 'V':
-                            self.update_cutz()
-        except Exception as e:
-            log.debug("FlatCAMObj ---> update_ui() " + str(e))
-        self.ui_connect()
-
-    def ui_update_v_shape(self, tool_type_txt):
-        if tool_type_txt == 'V':
-            self.ui.tipdialabel.show()
-            self.ui.tipdia_entry.show()
-            self.ui.tipanglelabel.show()
-            self.ui.tipangle_entry.show()
-            self.ui.cutz_entry.setDisabled(True)
-
-            self.update_cutz()
-        else:
-            self.ui.tipdialabel.hide()
-            self.ui.tipdia_entry.hide()
-            self.ui.tipanglelabel.hide()
-            self.ui.tipangle_entry.hide()
-            self.ui.cutz_entry.setDisabled(False)
-
-    def update_cutz(self):
-        try:
-            vdia = float(self.ui.tipdia_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                vdia = float(self.ui.tipdia_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
-                                       "use a number."))
-                return
-
-        try:
-            half_vangle = float(self.ui.tipangle_entry.get_value()) / 2
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                half_vangle = float(self.ui.tipangle_entry.get_value().replace(',', '.')) / 2
-            except ValueError:
-                self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
-                                       "use a number."))
-                return
-
-        row = self.ui.geo_tools_table.currentRow()
-        tool_uid = int(self.ui.geo_tools_table.item(row, 5).text())
-
-        tooldia = float(self.ui.geo_tools_table.item(row, 1).text())
-        new_cutz = (tooldia - vdia) / (2 * math.tan(math.radians(half_vangle)))
-        new_cutz = float('%.4f' % -new_cutz)
-        self.ui.cutz_entry.set_value(new_cutz)
-
-        # store the new CutZ value into storage (self.tools)
-        for tooluid_key, tooluid_value in self.tools.items():
-            if int(tooluid_key) == tool_uid:
-                tooluid_value['data']['cutz'] = new_cutz
-
-    def on_tooltable_cellwidget_change(self):
-        cw = self.sender()
-        cw_index = self.ui.geo_tools_table.indexAt(cw.pos())
-        cw_row = cw_index.row()
-        cw_col = cw_index.column()
-        current_uid = int(self.ui.geo_tools_table.item(cw_row, 5).text())
-
-        # store the text of the cellWidget that changed it's index in the self.tools
-        for tooluid_key, tooluid_value in self.tools.items():
-            if int(tooluid_key) == current_uid:
-                cb_txt = cw.currentText()
-                if cw_col == 2:
-                    tooluid_value['offset'] = cb_txt
-                    if cb_txt == 'Custom':
-                        self.ui.tool_offset_entry.show()
-                        self.ui.tool_offset_lbl.show()
-                    else:
-                        self.ui.tool_offset_entry.hide()
-                        self.ui.tool_offset_lbl.hide()
-                        # reset the offset_value in storage self.tools
-                        tooluid_value['offset_value'] = 0.0
-                elif cw_col == 3:
-                    # force toolpath type as 'Iso' if the tool type is V-Shape
-                    if self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText() == 'V':
-                        tooluid_value['type'] = _('Iso')
-                        idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso'))
-                        self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
-                    else:
-                        tooluid_value['type'] = cb_txt
-                elif cw_col == 4:
-                    tooluid_value['tool_type'] = cb_txt
-
-                    # if the tool_type selected is V-Shape then autoselect the toolpath type as Iso
-                    if cb_txt == 'V':
-                        idx = self.ui.geo_tools_table.cellWidget(cw_row, 3).findText(_('Iso'))
-                        self.ui.geo_tools_table.cellWidget(cw_row, 3).setCurrentIndex(idx)
-                self.ui_update_v_shape(tool_type_txt=self.ui.geo_tools_table.cellWidget(cw_row, 4).currentText())
-
-    def update_form(self, dict_storage):
-        for form_key in self.form_fields:
-            for storage_key in dict_storage:
-                if form_key == storage_key:
-                    try:
-                        self.form_fields[form_key].set_value(dict_storage[form_key])
-                    except Exception as e:
-                        log.debug(str(e))
-
-        # this is done here because those buttons control through OptionalInputSelection if some entry's are Enabled
-        # or not. But due of using the ui_disconnect() status is no longer updated and I had to do it here
-        self.ui.ois_dwell_geo.on_cb_change()
-        self.ui.ois_mpass_geo.on_cb_change()
-        self.ui.ois_tcz_geo.on_cb_change()
-
-    def gui_form_to_storage(self):
-
-        if self.ui.geo_tools_table.rowCount() == 0:
-            # there is no tool in tool table so we can't save the GUI elements values to storage
-            log.debug("FlatCAMGeometry.gui_form_to_storage() --> no tool in Tools Table, aborting.")
-            return
-
-        self.ui_disconnect()
-        widget_changed = self.sender()
-        try:
-            widget_idx = self.ui.grid3.indexOf(widget_changed)
-        except Exception as e:
-            return
-
-        # those are the indexes for the V-Tip Dia and V-Tip Angle, if edited calculate the new Cut Z
-        if widget_idx == 1 or widget_idx == 3:
-            self.update_cutz()
-
-        # the original connect() function of the OptionalInpuSelection is no longer working because of the
-        # ui_diconnect() so I use this 'hack'
-        if isinstance(widget_changed, FCCheckBox):
-            if widget_changed.text() == 'Multi-Depth:':
-                self.ui.ois_mpass_geo.on_cb_change()
-
-            if widget_changed.text() == 'Tool change':
-                self.ui.ois_tcz_geo.on_cb_change()
-
-            if widget_changed.text() == 'Dwell:':
-                self.ui.ois_dwell_geo.on_cb_change()
-
-        row = self.ui.geo_tools_table.currentRow()
-        if row < 0:
-            row = 0
-
-        # store all the data associated with the row parameter to the self.tools storage
-        tooldia_item = float(self.ui.geo_tools_table.item(row, 1).text())
-        offset_item = self.ui.geo_tools_table.cellWidget(row, 2).currentText()
-        type_item = self.ui.geo_tools_table.cellWidget(row, 3).currentText()
-        tool_type_item = self.ui.geo_tools_table.cellWidget(row, 4).currentText()
-        tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text())
-
-        try:
-            offset_value_item = float(self.ui.tool_offset_entry.get_value())
-        except ValueError:
-            # try to convert comma to decimal point. if it's still not working error message and return
-            try:
-                offset_value_item = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
-            except ValueError:
-                self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
-                                       "use a number."))
-                return
-
-        # this new dict will hold the actual useful data, another dict that is the value of key 'data'
-        temp_tools = {}
-        temp_dia = {}
-        temp_data = {}
-
-        for tooluid_key, tooluid_value in self.tools.items():
-            if int(tooluid_key) == tooluid_item:
-                for key, value in tooluid_value.items():
-                    if key == 'tooldia':
-                        temp_dia[key] = tooldia_item
-                    # update the 'offset', 'type' and 'tool_type' sections
-                    if key == 'offset':
-                        temp_dia[key] = offset_item
-                    if key == 'type':
-                        temp_dia[key] = type_item
-                    if key == 'tool_type':
-                        temp_dia[key] = tool_type_item
-                    if key == 'offset_value':
-                        temp_dia[key] = offset_value_item
-
-                    if key == 'data':
-                        # update the 'data' section
-                        for data_key in tooluid_value[key].keys():
-                            for form_key, form_value in self.form_fields.items():
-                                if form_key == data_key:
-                                    temp_data[data_key] = form_value.get_value()
-                            # make sure we make a copy of the keys not in the form (we may use 'data' keys that are
-                            # updated from self.app.defaults
-                            if data_key not in self.form_fields:
-                                temp_data[data_key] = value[data_key]
-                        temp_dia[key] = deepcopy(temp_data)
-                        temp_data.clear()
-
-                    if key == 'solid_geometry':
-                        temp_dia[key] = deepcopy(self.tools[tooluid_key]['solid_geometry'])
-
-                    temp_tools[tooluid_key] = deepcopy(temp_dia)
-
-            else:
-                temp_tools[tooluid_key] = deepcopy(tooluid_value)
-
-        self.tools.clear()
-        self.tools = deepcopy(temp_tools)
-        temp_tools.clear()
-        self.ui_connect()
-
-    def select_tools_table_row(self, row, clearsel=None):
-        if clearsel:
-            self.ui.geo_tools_table.clearSelection()
-
-        if self.ui.geo_tools_table.rowCount() > 0:
-            # self.ui.geo_tools_table.item(row, 0).setSelected(True)
-            self.ui.geo_tools_table.setCurrentItem(self.ui.geo_tools_table.item(row, 0))
-
-    def export_dxf(self):
-        units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-        dwg = None
-        try:
-            dwg = ezdxf.new('R2010')
-            msp = dwg.modelspace()
-
-            def g2dxf(dxf_space, geo):
-                if isinstance(geo, MultiPolygon):
-                    for poly in geo:
-                        ext_points = list(poly.exterior.coords)
-                        dxf_space.add_lwpolyline(ext_points)
-                        for interior in poly.interiors:
-                            dxf_space.add_lwpolyline(list(interior.coords))
-                if isinstance(geo, Polygon):
-                    ext_points = list(geo.exterior.coords)
-                    dxf_space.add_lwpolyline(ext_points)
-                    for interior in geo.interiors:
-                        dxf_space.add_lwpolyline(list(interior.coords))
-                if isinstance(geo, MultiLineString):
-                    for line in geo:
-                        dxf_space.add_lwpolyline(list(line.coords))
-                if isinstance(geo, LineString) or isinstance(geo, LinearRing):
-                    dxf_space.add_lwpolyline(list(geo.coords))
-
-            multigeo_solid_geometry = []
-            if self.multigeo:
-                for tool in self.tools:
-                    multigeo_solid_geometry += self.tools[tool]['solid_geometry']
-            else:
-                multigeo_solid_geometry = self.solid_geometry
-
-            for geo in multigeo_solid_geometry:
-                if type(geo) == list:
-                    for g in geo:
-                        g2dxf(msp, g)
-                else:
-                    g2dxf(msp, geo)
-
-                # points = FlatCAMGeometry.get_pts(geo)
-                # msp.add_lwpolyline(points)
-        except Exception as e:
-            log.debug(str(e))
-
-        return dwg
-
-    def get_selected_tools_table_items(self):
-        """
-        Returns a list of lists, each list in the list is made out of row elements
-
-        :return: List of table_tools items.
-        :rtype: list
-        """
-        table_tools_items = []
-        if self.multigeo:
-            for x in self.ui.geo_tools_table.selectedItems():
-                table_tools_items.append([self.ui.geo_tools_table.item(x.row(), column).text()
-                                          for column in range(0, self.ui.geo_tools_table.columnCount())])
-        else:
-            for x in self.ui.geo_tools_table.selectedItems():
-                r = []
-                txt = ''
-
-                # the last 2 columns for single-geo geometry are irrelevant and create problems reading
-                # so we don't read them
-                for column in range(0, self.ui.geo_tools_table.columnCount() - 2):
-                    # the columns have items that have text but also have items that are widgets
-                    # for which the text they hold has to be read differently
-                    try:
-                        txt = self.ui.geo_tools_table.item(x.row(), column).text()
-                    except AttributeError:
-                        txt = self.ui.geo_tools_table.cellWidget(x.row(), column).currentText()
-                    except Exception as e:
-                        pass
-                    r.append(txt)
-                table_tools_items.append(r)
-
-        for item in table_tools_items:
-            item[0] = str(item[0])
-        return table_tools_items
-
-    def on_pp_changed(self):
-        current_pp = self.ui.pp_geometry_name_cb.get_value()
-        if current_pp == 'hpgl':
-            self.old_pp_state = self.ui.mpass_cb.get_value()
-            self.old_toolchangeg_state = self.ui.toolchangeg_cb.get_value()
-
-            self.ui.mpass_cb.set_value(False)
-            self.ui.mpass_cb.setDisabled(True)
-
-            self.ui.toolchangeg_cb.set_value(True)
-            self.ui.toolchangeg_cb.setDisabled(True)
-        else:
-            self.ui.mpass_cb.set_value(self.old_pp_state)
-            self.ui.mpass_cb.setDisabled(False)
-
-            self.ui.toolchangeg_cb.set_value(self.old_toolchangeg_state)
-            self.ui.toolchangeg_cb.setDisabled(False)
-
-        if "toolchange_probe" in current_pp.lower():
-            self.ui.pdepth_entry.setVisible(True)
-            self.ui.pdepth_label.show()
-
-            self.ui.feedrate_probe_entry.setVisible(True)
-            self.ui.feedrate_probe_label.show()
-        else:
-            self.ui.pdepth_entry.setVisible(False)
-            self.ui.pdepth_label.hide()
-
-            self.ui.feedrate_probe_entry.setVisible(False)
-            self.ui.feedrate_probe_label.hide()
-
-        if 'marlin' in current_pp.lower() or 'custom' in current_pp.lower():
-            self.ui.fr_rapidlabel.show()
-            self.ui.cncfeedrate_rapid_entry.show()
-        else:
-            self.ui.fr_rapidlabel.hide()
-            self.ui.cncfeedrate_rapid_entry.hide()
-
-    def on_generatecnc_button_click(self, *args):
-        log.debug("Generating CNCJob from Geometry ...")
-        self.app.report_usage("geometry_on_generatecnc_button")
-        self.read_form()
-
-        self.sel_tools = {}
-
-        try:
-            if self.special_group:
-                self.app.inform.emit(_("[WARNING_NOTCL] This Geometry can't be processed because it is %s geometry."
-                ) % str(self.special_group))
-                return
-        except AttributeError:
-            pass
-
-        # test to see if we have tools available in the tool table
-        if self.ui.geo_tools_table.selectedItems():
-            for x in self.ui.geo_tools_table.selectedItems():
-                try:
-                    tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text())
-                except ValueError:
-                    # try to convert comma to decimal point. if it's still not working error message and return
-                    try:
-                        tooldia = float(self.ui.geo_tools_table.item(x.row(), 1).text().replace(',', '.'))
-                    except ValueError:
-                        self.app.inform.emit(_("[ERROR_NOTCL] Wrong Tool Dia value format entered, "
-                                               "use a number."))
-                        return
-                tooluid = int(self.ui.geo_tools_table.item(x.row(), 5).text())
-
-                for tooluid_key, tooluid_value in self.tools.items():
-                    if int(tooluid_key) == tooluid:
-                        self.sel_tools.update({
-                            tooluid: deepcopy(tooluid_value)
-                        })
-            self.mtool_gen_cncjob()
-            self.ui.geo_tools_table.clearSelection()
-
-        elif self.ui.geo_tools_table.rowCount() == 1:
-            tooluid = int(self.ui.geo_tools_table.item(0, 5).text())
-
-            for tooluid_key, tooluid_value in self.tools.items():
-                if int(tooluid_key) == tooluid:
-                    self.sel_tools.update({
-                        tooluid: deepcopy(tooluid_value)
-                    })
-            self.mtool_gen_cncjob()
-            self.ui.geo_tools_table.clearSelection()
-
-        else:
-            self.app.inform.emit(_("[ERROR_NOTCL] Failed. No tool selected in the tool table ..."))
-
-    def mtool_gen_cncjob(self, segx=None, segy=None, use_thread=True):
-        """
-        Creates a multi-tool CNCJob out of this Geometry object.
-        The actual work is done by the target FlatCAMCNCjob object's
-        `generate_from_geometry_2()` method.
-
-        :param z_cut: Cut depth (negative)
-        :param z_move: Hight of the tool when travelling (not cutting)
-        :param feedrate: Feed rate while cutting on X - Y plane
-        :param feedrate_z: Feed rate while cutting on Z plane
-        :param feedrate_rapid: Feed rate while moving with rapids
-        :param tooldia: Tool diameter
-        :param outname: Name of the new object
-        :param spindlespeed: Spindle speed (RPM)
-        :param ppname_g Name of the postprocessor
-        :return: None
-        """
-
-        offset_str = ''
-        multitool_gcode = ''
-
-        # use the name of the first tool selected in self.geo_tools_table which has the diameter passed as tool_dia
-        outname = "%s_%s" % (self.options["name"], 'cnc')
-
-        segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
-        segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
-
-        try:
-            xmin = self.options['xmin']
-            ymin = self.options['ymin']
-            xmax = self.options['xmax']
-            ymax = self.options['ymax']
-        except Exception as e:
-            log.debug("FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s\n" % str(e))
-            msg = _("[ERROR] An internal error has occurred. See shell.\n")
-            msg += _('FlatCAMObj.FlatCAMGeometry.mtool_gen_cncjob() --> %s') % str(e)
-            msg += traceback.format_exc()
-            self.app.inform.emit(msg)
-            return
-
-        # Object initialization function for app.new_object()
-        # RUNNING ON SEPARATE THREAD!
-        def job_init_single_geometry(job_obj, app_obj):
-            log.debug("Creating a CNCJob out of a single-geometry")
-            assert isinstance(job_obj, FlatCAMCNCjob), \
-                "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
-
-            # count the tools
-            tool_cnt = 0
-
-            dia_cnc_dict = {}
-
-            # this turn on the FlatCAMCNCJob plot for multiple tools
-            job_obj.multitool = True
-            job_obj.multigeo = False
-            job_obj.cnc_tools.clear()
-            # job_obj.create_geometry()
-
-            job_obj.options['Tools_in_use'] = self.get_selected_tools_table_items()
-            job_obj.segx = segx
-            job_obj.segy = segy
-
-            try:
-                job_obj.z_pdepth = float(self.options["z_pdepth"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] '
-                                           'or self.options["z_pdepth"]'))
-
-            try:
-                job_obj.feedrate_probe = float(self.options["feedrate_probe"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
-                                           'or self.options["feedrate_probe"]'))
-
-            for tooluid_key in self.sel_tools:
-                tool_cnt += 1
-                app_obj.progress.emit(20)
-
-                for diadict_key, diadict_value in self.sel_tools[tooluid_key].items():
-                    if diadict_key == 'tooldia':
-                        tooldia_val = float('%.4f' % float(diadict_value))
-                        dia_cnc_dict.update({
-                            diadict_key: tooldia_val
-                        })
-                    if diadict_key == 'offset':
-                        o_val = diadict_value.lower()
-                        dia_cnc_dict.update({
-                            diadict_key: o_val
-                        })
-
-                    if diadict_key == 'type':
-                        t_val = diadict_value
-                        dia_cnc_dict.update({
-                            diadict_key: t_val
-                        })
-
-                    if diadict_key == 'tool_type':
-                        tt_val = diadict_value
-                        dia_cnc_dict.update({
-                            diadict_key: tt_val
-                        })
-
-                    if diadict_key == 'data':
-                        for data_key, data_value in diadict_value.items():
-                            if data_key == "multidepth":
-                                multidepth = data_value
-                            if data_key == "depthperpass":
-                                depthpercut = data_value
-
-                            if data_key == "extracut":
-                                extracut = data_value
-                            if data_key == "startz":
-                                startz = data_value
-                            if data_key == "endz":
-                                endz = data_value
-
-                            if data_key == "toolchangez":
-                                toolchangez = data_value
-                            if data_key == "toolchangexy":
-                                toolchangexy = data_value
-                            if data_key == "toolchange":
-                                toolchange = data_value
-
-                            if data_key == "cutz":
-                                z_cut = data_value
-                            if data_key == "travelz":
-                                z_move = data_value
-
-                            if data_key == "feedrate":
-                                feedrate = data_value
-                            if data_key == "feedrate_z":
-                                feedrate_z = data_value
-                            if data_key == "feedrate_rapid":
-                                feedrate_rapid = data_value
-
-                            if data_key == "ppname_g":
-                                pp_geometry_name = data_value
-
-                            if data_key == "spindlespeed":
-                                spindlespeed = data_value
-                            if data_key == "dwell":
-                                dwell = data_value
-                            if data_key == "dwelltime":
-                                dwelltime = data_value
-
-                        datadict = deepcopy(diadict_value)
-                        dia_cnc_dict.update({
-                            diadict_key: datadict
-                        })
-
-                if dia_cnc_dict['offset'] == 'in':
-                    tool_offset = -dia_cnc_dict['tooldia'] / 2
-                    offset_str = 'inside'
-                elif dia_cnc_dict['offset'].lower() == 'out':
-                    tool_offset = dia_cnc_dict['tooldia'] / 2
-                    offset_str = 'outside'
-                elif dia_cnc_dict['offset'].lower() == 'path':
-                    offset_str = 'onpath'
-                    tool_offset = 0.0
-                else:
-                    offset_str = 'custom'
-                    try:
-                        offset_value = float(self.ui.tool_offset_entry.get_value())
-                    except ValueError:
-                        # try to convert comma to decimal point. if it's still not working error message and return
-                        try:
-                            offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
-                        except ValueError:
-                            self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
-                                                   "use a number."))
-                            return
-                    if offset_value:
-                        tool_offset = float(offset_value)
-                    else:
-                        self.app.inform.emit(_("[WARNING] Tool Offset is selected in Tool Table but "
-                                               "no value is provided.\n"
-                                               "Add a Tool Offset or change the Offset Type."))
-                        return
-                dia_cnc_dict.update({
-                    'offset_value': tool_offset
-                })
-
-                spindledir = self.app.defaults['geometry_spindledir']
-
-                job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
-                job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
-
-                # Propagate options
-                job_obj.options["tooldia"] = tooldia_val
-                job_obj.options['type'] = 'Geometry'
-                job_obj.options['tool_dia'] = tooldia_val
-
-                job_obj.options['xmin'] = xmin
-                job_obj.options['ymin'] = ymin
-                job_obj.options['xmax'] = xmax
-                job_obj.options['ymax'] = ymax
-
-                app_obj.progress.emit(40)
-
-                # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
-                # to a value of 0.0005 which is 20 times less than 0.01
-                tol = float(self.app.defaults['global_tolerance']) / 20
-                res = job_obj.generate_from_geometry_2(
-                    self, tooldia=tooldia_val, offset=tool_offset, tolerance=tol,
-                    z_cut=z_cut, z_move=z_move,
-                    feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
-                    spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime,
-                    multidepth=multidepth, depthpercut=depthpercut,
-                    extracut=extracut, startz=startz, endz=endz,
-                    toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
-                    pp_geometry_name=pp_geometry_name,
-                    tool_no=tool_cnt)
-
-                if res == 'fail':
-                    log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed")
-                    return 'fail'
-                else:
-                    dia_cnc_dict['gcode'] = res
-
-                app_obj.progress.emit(50)
-                # tell gcode_parse from which point to start drawing the lines depending on what kind of
-                # object is the source of gcode
-                job_obj.toolchange_xy_type = "geometry"
-
-                dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
-
-                # TODO this serve for bounding box creation only; should be optimized
-                dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
-
-                app_obj.progress.emit(80)
-
-                job_obj.cnc_tools.update({
-                    tooluid_key: deepcopy(dia_cnc_dict)
-                })
-                dia_cnc_dict.clear()
-
-        # Object initialization function for app.new_object()
-        # RUNNING ON SEPARATE THREAD!
-        def job_init_multi_geometry(job_obj, app_obj):
-            log.debug("Creating a CNCJob out of a multi-geometry")
-            assert isinstance(job_obj, FlatCAMCNCjob), \
-                "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
-
-            # count the tools
-            tool_cnt = 0
-
-            dia_cnc_dict = {}
-
-            current_uid = int(1)
-
-            # this turn on the FlatCAMCNCJob plot for multiple tools
-            job_obj.multitool = True
-            job_obj.multigeo = True
-            job_obj.cnc_tools.clear()
-
-            job_obj.options['xmin'] = xmin
-            job_obj.options['ymin'] = ymin
-            job_obj.options['xmax'] = xmax
-            job_obj.options['ymax'] = ymax
-
-            try:
-                job_obj.z_pdepth = float(self.options["z_pdepth"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] '
-                                           'or self.options["z_pdepth"]'))
-
-            try:
-                job_obj.feedrate_probe = float(self.options["feedrate_probe"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
-                                           'or self.options["feedrate_probe"]'))
-
-            # make sure that trying to make a CNCJob from an empty file is not creating an app crash
-            if not self.solid_geometry:
-                a = 0
-                for tooluid_key in self.tools:
-                    if self.tools[tooluid_key]['solid_geometry'] is None:
-                        a += 1
-                if a == len(self.tools):
-                    self.app.inform.emit(_('[ERROR_NOTCL] Cancelled. Empty file, it has no geometry...'))
-                    return 'fail'
-
-            for tooluid_key in self.sel_tools:
-                tool_cnt += 1
-                app_obj.progress.emit(20)
-
-                # find the tool_dia associated with the tooluid_key
-                sel_tool_dia = self.sel_tools[tooluid_key]['tooldia']
-
-                # search in the self.tools for the sel_tool_dia and when found see what tooluid has
-                # on the found tooluid in self.tools we also have the solid_geometry that interest us
-                for k, v in self.tools.items():
-                    if float('%.4f' % float(v['tooldia'])) == float('%.4f' % float(sel_tool_dia)):
-                        current_uid = int(k)
-                        break
-
-                for diadict_key, diadict_value in self.sel_tools[tooluid_key].items():
-                    if diadict_key == 'tooldia':
-                        tooldia_val = float('%.4f' % float(diadict_value))
-                        dia_cnc_dict.update({
-                            diadict_key: tooldia_val
-                        })
-                    if diadict_key == 'offset':
-                        o_val = diadict_value.lower()
-                        dia_cnc_dict.update({
-                            diadict_key: o_val
-                        })
-
-                    if diadict_key == 'type':
-                        t_val = diadict_value
-                        dia_cnc_dict.update({
-                            diadict_key: t_val
-                        })
-
-                    if diadict_key == 'tool_type':
-                        tt_val = diadict_value
-                        dia_cnc_dict.update({
-                            diadict_key: tt_val
-                        })
-
-                    if diadict_key == 'data':
-                        for data_key, data_value in diadict_value.items():
-                            if data_key == "multidepth":
-                                multidepth = data_value
-                            if data_key == "depthperpass":
-                                depthpercut = data_value
-
-                            if data_key == "extracut":
-                                extracut = data_value
-                            if data_key == "startz":
-                                startz = data_value
-                            if data_key == "endz":
-                                endz = data_value
-
-                            if data_key == "toolchangez":
-                                toolchangez = data_value
-                            if data_key == "toolchangexy":
-                                toolchangexy = data_value
-                            if data_key == "toolchange":
-                                toolchange = data_value
-
-                            if data_key == "cutz":
-                                z_cut = data_value
-                            if data_key == "travelz":
-                                z_move = data_value
-
-                            if data_key == "feedrate":
-                                feedrate = data_value
-                            if data_key == "feedrate_z":
-                                feedrate_z = data_value
-                            if data_key == "feedrate_rapid":
-                                feedrate_rapid = data_value
-
-                            if data_key == "ppname_g":
-                                pp_geometry_name = data_value
-
-                            if data_key == "spindlespeed":
-                                spindlespeed = data_value
-                            if data_key == "dwell":
-                                dwell = data_value
-                            if data_key == "dwelltime":
-                                dwelltime = data_value
-
-                        datadict = deepcopy(diadict_value)
-                        dia_cnc_dict.update({
-                            diadict_key: datadict
-                        })
-
-                if dia_cnc_dict['offset'] == 'in':
-                    tool_offset = -dia_cnc_dict['tooldia'] / 2
-                    offset_str = 'inside'
-                elif dia_cnc_dict['offset'].lower() == 'out':
-                    tool_offset = dia_cnc_dict['tooldia'] / 2
-                    offset_str = 'outside'
-                elif dia_cnc_dict['offset'].lower() == 'path':
-                    offset_str = 'onpath'
-                    tool_offset = 0.0
-                else:
-                    offset_str = 'custom'
-                    try:
-                        offset_value = float(self.ui.tool_offset_entry.get_value())
-                    except ValueError:
-                        # try to convert comma to decimal point. if it's still not working error message and return
-                        try:
-                            offset_value = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
-                        except ValueError:
-                            self.app.inform.emit(_("[ERROR_NOTCL] Wrong value format entered, "
-                                                   "use a number."))
-                            return
-                    if offset_value:
-                        tool_offset = float(offset_value)
-                    else:
-                        self.app.inform.emit(_("[WARNING] Tool Offset is selected in Tool Table but "
-                                               "no value is provided.\n"
-                                               "Add a Tool Offset or change the Offset Type."))
-                        return
-                dia_cnc_dict.update({
-                    'offset_value': tool_offset
-                })
-
-                job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
-                job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
-
-                # Propagate options
-                job_obj.options["tooldia"] = tooldia_val
-                job_obj.options['type'] = 'Geometry'
-                job_obj.options['tool_dia'] = tooldia_val
-
-                app_obj.progress.emit(40)
-
-                spindledir = self.app.defaults['geometry_spindledir']
-                tool_solid_geometry = self.tools[current_uid]['solid_geometry']
-
-                # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
-                # to a value of 0.0005 which is 20 times less than 0.01
-                tol = float(self.app.defaults['global_tolerance']) / 20
-                res = job_obj.generate_from_multitool_geometry(
-                    tool_solid_geometry, tooldia=tooldia_val, offset=tool_offset,
-                    tolerance=tol, z_cut=z_cut, z_move=z_move,
-                    feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
-                    spindlespeed=spindlespeed, spindledir=spindledir, dwell=dwell, dwelltime=dwelltime,
-                    multidepth=multidepth, depthpercut=depthpercut,
-                    extracut=extracut, startz=startz, endz=endz,
-                    toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
-                    pp_geometry_name=pp_geometry_name,
-                    tool_no=tool_cnt)
-
-                if res == 'fail':
-                    log.debug("FlatCAMGeometry.mtool_gen_cncjob() --> generate_from_geometry2() failed")
-                    return 'fail'
-                else:
-                    dia_cnc_dict['gcode'] = res
-
-                dia_cnc_dict['gcode_parsed'] = job_obj.gcode_parse()
-
-                # TODO this serve for bounding box creation only; should be optimized
-                dia_cnc_dict['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_cnc_dict['gcode_parsed']])
-
-                # tell gcode_parse from which point to start drawing the lines depending on what kind of
-                # object is the source of gcode
-                job_obj.toolchange_xy_type = "geometry"
-
-                app_obj.progress.emit(80)
-
-                job_obj.cnc_tools.update({
-                    tooluid_key: deepcopy(dia_cnc_dict)
-                })
-                dia_cnc_dict.clear()
-
-        if use_thread:
-            # To be run in separate thread
-            # The idea is that if there is a solid_geometry in the file "root" then most likely thare are no
-            # separate solid_geometry in the self.tools dictionary
-            def job_thread(app_obj):
-                if self.solid_geometry:
-                    with self.app.proc_container.new(_("Generating CNC Code")):
-                        if app_obj.new_object("cncjob", outname, job_init_single_geometry) != 'fail':
-                            app_obj.inform.emit("[success] CNCjob created: %s" % outname)
-                            app_obj.progress.emit(100)
-                else:
-                    with self.app.proc_container.new(_("Generating CNC Code")):
-                        if app_obj.new_object("cncjob", outname, job_init_multi_geometry) != 'fail':
-                            app_obj.inform.emit("[success] CNCjob created: %s" % outname)
-                            app_obj.progress.emit(100)
-
-            # Create a promise with the name
-            self.app.collection.promise(outname)
-            # Send to worker
-            self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
-        else:
-            if self.solid_geometry:
-                self.app.new_object("cncjob", outname, job_init_single_geometry)
-            else:
-                self.app.new_object("cncjob", outname, job_init_multi_geometry)
-
-    def generatecncjob(self, outname=None,
-                       tooldia=None, offset=None,
-                       z_cut=None, z_move=None,
-                       feedrate=None, feedrate_z=None, feedrate_rapid=None,
-                       spindlespeed=None, dwell=None, dwelltime=None,
-                       multidepth=None, depthperpass=None,
-                       toolchange=None, toolchangez=None, toolchangexy=None,
-                       extracut=None, startz=None, endz=None,
-                       ppname_g=None,
-                       segx=None,
-                       segy=None,
-                       use_thread=True):
-        """
-        Only used for TCL Command.
-        Creates a CNCJob out of this Geometry object. The actual
-        work is done by the target FlatCAMCNCjob object's
-        `generate_from_geometry_2()` method.
-
-        :param z_cut: Cut depth (negative)
-        :param z_move: Hight of the tool when travelling (not cutting)
-        :param feedrate: Feed rate while cutting on X - Y plane
-        :param feedrate_z: Feed rate while cutting on Z plane
-        :param feedrate_rapid: Feed rate while moving with rapids
-        :param tooldia: Tool diameter
-        :param outname: Name of the new object
-        :param spindlespeed: Spindle speed (RPM)
-        :param ppname_g Name of the postprocessor
-        :return: None
-        """
-
-        tooldia = tooldia if tooldia else float(self.options["cnctooldia"])
-        outname = outname if outname is not None else self.options["name"]
-
-        z_cut = z_cut if z_cut is not None else float(self.options["cutz"])
-        z_move = z_move if z_move is not None else float(self.options["travelz"])
-
-        feedrate = feedrate if feedrate is not None else float(self.options["feedrate"])
-        feedrate_z = feedrate_z if feedrate_z is not None else float(self.options["feedrate_z"])
-        feedrate_rapid = feedrate_rapid if feedrate_rapid is not None else float(self.options["feedrate_rapid"])
-
-        multidepth = multidepth if multidepth is not None else self.options["multidepth"]
-        depthperpass = depthperpass if depthperpass is not None else float(self.options["depthperpass"])
-
-        segx = segx if segx is not None else float(self.app.defaults['geometry_segx'])
-        segy = segy if segy is not None else float(self.app.defaults['geometry_segy'])
-
-        extracut = extracut if extracut is not None else float(self.options["extracut"])
-        startz = startz if startz is not None else self.options["startz"]
-        endz = endz if endz is not None else float(self.options["endz"])
-
-        toolchangez = toolchangez if toolchangez else float(self.options["toolchangez"])
-        toolchangexy = toolchangexy if toolchangexy else self.options["toolchangexy"]
-        toolchange = toolchange if toolchange else self.options["toolchange"]
-
-        offset = offset if offset else 0.0
-
-        # int or None.
-        spindlespeed = spindlespeed if spindlespeed else self.options['spindlespeed']
-        dwell = dwell if dwell else self.options["dwell"]
-        dwelltime = dwelltime if dwelltime else float(self.options["dwelltime"])
-
-        ppname_g = ppname_g if ppname_g else self.options["ppname_g"]
-
-        # Object initialization function for app.new_object()
-        # RUNNING ON SEPARATE THREAD!
-        def job_init(job_obj, app_obj):
-            assert isinstance(job_obj, FlatCAMCNCjob), "Initializer expected a FlatCAMCNCjob, got %s" % type(job_obj)
-
-            # Propagate options
-            job_obj.options["tooldia"] = tooldia
-
-            app_obj.progress.emit(20)
-
-            job_obj.coords_decimals = self.app.defaults["cncjob_coords_decimals"]
-            job_obj.fr_decimals = self.app.defaults["cncjob_fr_decimals"]
-            app_obj.progress.emit(40)
-
-            job_obj.options['type'] = 'Geometry'
-            job_obj.options['tool_dia'] = tooldia
-
-            job_obj.segx = segx
-            job_obj.segy = segy
-
-            try:
-                job_obj.z_pdepth = float(self.options["z_pdepth"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.z_pdepth = float(self.options["z_pdepth"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["z_pdepth"] '
-                                           'or self.options["z_pdepth"]'))
-
-            try:
-                job_obj.feedrate_probe = float(self.options["feedrate_probe"])
-            except ValueError:
-                # try to convert comma to decimal point. if it's still not working error message and return
-                try:
-                    job_obj.feedrate_rapid = float(self.options["feedrate_probe"].replace(',', '.'))
-                except ValueError:
-                    self.app.inform.emit(_('[ERROR_NOTCL] Wrong value format for self.defaults["feedrate_probe"] '
-                                           'or self.options["feedrate_probe"]'))
-
-            job_obj.options['xmin'] = self.options['xmin']
-            job_obj.options['ymin'] = self.options['ymin']
-            job_obj.options['xmax'] = self.options['xmax']
-            job_obj.options['ymax'] = self.options['ymax']
-
-            # it seems that the tolerance needs to be a lot lower value than 0.01 and it was hardcoded initially
-            # to a value of 0.0005 which is 20 times less than 0.01
-            tol = float(self.app.defaults['global_tolerance']) / 20
-            job_obj.generate_from_geometry_2(self, tooldia=tooldia, offset=offset, tolerance=tol,
-                                             z_cut=z_cut, z_move=z_move,
-                                             feedrate=feedrate, feedrate_z=feedrate_z, feedrate_rapid=feedrate_rapid,
-                                             spindlespeed=spindlespeed, dwell=dwell, dwelltime=dwelltime,
-                                             multidepth=multidepth, depthpercut=depthperpass,
-                                             toolchange=toolchange, toolchangez=toolchangez, toolchangexy=toolchangexy,
-                                             extracut=extracut, startz=startz, endz=endz,
-                                             pp_geometry_name=ppname_g
-                                             )
-
-            app_obj.progress.emit(50)
-            # tell gcode_parse from which point to start drawing the lines depending on what kind of object is the
-            # source of gcode
-            job_obj.toolchange_xy_type = "geometry"
-            job_obj.gcode_parse()
-
-            app_obj.progress.emit(80)
-
-        if use_thread:
-            # To be run in separate thread
-            def job_thread(app_obj):
-                with self.app.proc_container.new(_("Generating CNC Code")):
-                    app_obj.new_object("cncjob", outname, job_init)
-                    app_obj.inform.emit("[success] CNCjob created: %s" % outname)
-                    app_obj.progress.emit(100)
-
-            # Create a promise with the name
-            self.app.collection.promise(outname)
-            # Send to worker
-            self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
-        else:
-            self.app.new_object("cncjob", outname, job_init)
-
-    # def on_plot_cb_click(self, *args):  # TODO: args not needed
-    #     if self.muted_ui:
-    #         return
-    #     self.read_form_item('plot')
-
-    def scale(self, xfactor, yfactor=None, point=None):
-        """
-        Scales all geometry by a given factor.
-
-        :param xfactor: Factor by which to scale the object's geometry/
-        :type xfactor: float
-        :param yfactor: Factor by which to scale the object's geometry/
-        :type yfactor: float
-        :return: None
-        :rtype: None
-        """
-
-        try:
-            xfactor = float(xfactor)
-        except Exception as e:
-            self.app.inform.emit(_(
-                "[ERROR_NOTCL] Scale factor has to be a number: integer or float."))
-            return
-
-        if yfactor is None:
-            yfactor = xfactor
-        else:
-            try:
-                yfactor = float(yfactor)
-            except Exception as e:
-                self.app.inform.emit(_("[ERROR_NOTCL] Scale factor has to be a number: integer or float."))
-                return
-
-        if point is None:
-            px = 0
-            py = 0
-        else:
-            px, py = point
-
-        # if type(self.solid_geometry) == list:
-        #     geo_list =  self.flatten(self.solid_geometry)
-        #     self.solid_geometry = []
-        #     # for g in geo_list:
-        #     #     self.solid_geometry.append(affinity.scale(g, xfactor, yfactor, origin=(px, py)))
-        #     self.solid_geometry = [affinity.scale(g, xfactor, yfactor, origin=(px, py))
-        #                            for g in geo_list]
-        # else:
-        #     self.solid_geometry = affinity.scale(self.solid_geometry, xfactor, yfactor,
-        #                                          origin=(px, py))
-        # self.app.inform.emit("[success] Geometry Scale done.")
-
-        def scale_recursion(geom):
-            if type(geom) is list:
-                geoms = list()
-                for local_geom in geom:
-                    geoms.append(scale_recursion(local_geom))
-                return geoms
-            else:
-                return affinity.scale(geom, xfactor, yfactor, origin=(px, py))
-
-        if self.multigeo is True:
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = scale_recursion(self.tools[tool]['solid_geometry'])
-        else:
-            self.solid_geometry = scale_recursion(self.solid_geometry)
-
-        self.app.inform.emit(_(
-            "[success] Geometry Scale done."
-        ))
-
-    def offset(self, vect):
-        """
-        Offsets all geometry by a given vector/
-
-        :param vect: (x, y) vector by which to offset the object's geometry.
-        :type vect: tuple
-        :return: None
-        :rtype: None
-        """
-
-        try:
-            dx, dy = vect
-        except TypeError:
-            self.app.inform.emit(_(
-                "[ERROR_NOTCL] An (x,y) pair of values are needed. "
-                "Probable you entered only one value in the Offset field."
-            ))
-            return
-
-        def translate_recursion(geom):
-            if type(geom) is list:
-                geoms = list()
-                for local_geom in geom:
-                    geoms.append(translate_recursion(local_geom))
-                return geoms
-            else:
-                return affinity.translate(geom, xoff=dx, yoff=dy)
-
-        if self.multigeo is True:
-            for tool in self.tools:
-                self.tools[tool]['solid_geometry'] = translate_recursion(self.tools[tool]['solid_geometry'])
-        else:
-            self.solid_geometry = translate_recursion(self.solid_geometry)
-        self.app.inform.emit(_("[success] Geometry Offset done."))
-
-    def convert_units(self, units):
-        self.ui_disconnect()
-
-        factor = Geometry.convert_units(self, units)
-
-        self.options['cutz'] = float(self.options['cutz']) * factor
-        self.options['depthperpass'] = float(self.options['depthperpass']) * factor
-        self.options['travelz'] = float(self.options['travelz']) * factor
-        self.options['feedrate'] = float(self.options['feedrate']) * factor
-        self.options['feedrate_z'] = float(self.options['feedrate_z']) * factor
-        self.options['feedrate_rapid'] = float(self.options['feedrate_rapid']) * factor
-        self.options['endz'] = float(self.options['endz']) * factor
-        # self.options['cnctooldia'] *= factor
-        # self.options['painttooldia'] *= factor
-        # self.options['paintmargin'] *= factor
-        # self.options['paintoverlap'] *= factor
-
-        self.options["toolchangez"] = float(self.options["toolchangez"]) * factor
-
-        if self.app.defaults["geometry_toolchangexy"] == '':
-            self.options['toolchangexy'] = "0.0, 0.0"
-        else:
-            coords_xy = [float(eval(coord)) for coord in self.app.defaults["geometry_toolchangexy"].split(",")]
-            if len(coords_xy) < 2:
-                self.app.inform.emit(_(
-                    "[ERROR]The Toolchange X,Y field in Edit -> Preferences has to be "
-                    "in the format (x, y) \nbut now there is only one value, not two. "
-                ))
-                return 'fail'
-            coords_xy[0] *= factor
-            coords_xy[1] *= factor
-            self.options['toolchangexy'] = "%f, %f" % (coords_xy[0], coords_xy[1])
-
-        if self.options['startz'] is not None:
-            self.options['startz'] = float(self.options['startz']) * factor
-
-        param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
-                      'endz', 'toolchangez']
-
-        if isinstance(self, FlatCAMGeometry):
-            temp_tools_dict = {}
-            tool_dia_copy = {}
-            data_copy = {}
-            for tooluid_key, tooluid_value in self.tools.items():
-                for dia_key, dia_value in tooluid_value.items():
-                    if dia_key == 'tooldia':
-                        dia_value *= factor
-                        dia_value = float('%.4f' % dia_value)
-                        tool_dia_copy[dia_key] = dia_value
-                    if dia_key == 'offset':
-                        tool_dia_copy[dia_key] = dia_value
-                    if dia_key == 'offset_value':
-                        dia_value *= factor
-                        tool_dia_copy[dia_key] = dia_value
-
-                        # convert the value in the Custom Tool Offset entry in UI
-                        custom_offset = None
-                        try:
-                            custom_offset = float(self.ui.tool_offset_entry.get_value())
-                        except ValueError:
-                            # try to convert comma to decimal point. if it's still not working error message and return
-                            try:
-                                custom_offset = float(self.ui.tool_offset_entry.get_value().replace(',', '.'))
-                            except ValueError:
-                                self.app.inform.emit(_(
-                                    "[ERROR_NOTCL] Wrong value format entered, "
-                                    "use a number."
-                                ))
-                                return
-                        except TypeError:
-                            pass
-
-                        if custom_offset:
-                            custom_offset *= factor
-                            self.ui.tool_offset_entry.set_value(custom_offset)
-
-                    if dia_key == 'type':
-                        tool_dia_copy[dia_key] = dia_value
-                    if dia_key == 'tool_type':
-                        tool_dia_copy[dia_key] = dia_value
-                    if dia_key == 'data':
-                        for data_key, data_value in dia_value.items():
-                            # convert the form fields that are convertible
-                            for param in param_list:
-                                if data_key == param and data_value is not None:
-                                    data_copy[data_key] = data_value * factor
-                            # copy the other dict entries that are not convertible
-                            if data_key not in param_list:
-                                data_copy[data_key] = data_value
-                        tool_dia_copy[dia_key] = deepcopy(data_copy)
-                        data_copy.clear()
-
-                temp_tools_dict.update({
-                    tooluid_key: deepcopy(tool_dia_copy)
-                })
-                tool_dia_copy.clear()
-
-            self.tools.clear()
-            self.tools = deepcopy(temp_tools_dict)
-
-        # if there is a value in the new tool field then convert that one too
-        tooldia = self.ui.addtool_entry.get_value()
-        if tooldia:
-            tooldia *= factor
-            # limit the decimals to 2 for METRIC and 3 for INCH
-            if units.lower() == 'in':
-                tooldia = float('%.4f' % tooldia)
-            else:
-                tooldia = float('%.2f' % tooldia)
-
-            self.ui.addtool_entry.set_value(tooldia)
-
-        return factor
-
-    def plot_element(self, element, color='red', visible=None):
-
-        visible = visible if visible else self.options['plot']
-
-        try:
-            for sub_el in element:
-                self.plot_element(sub_el)
-
-        except TypeError:  # Element is not iterable...
-            self.add_shape(shape=element, color=color, visible=visible, layer=0)
-
-    def plot(self, visible=None, kind=None):
-        """
-        Plot the object.
-
-        :param visible: Controls if the added shape is visible of not
-        :param kind: added so there is no error when a project is loaded and it has both geometry and CNCJob, because
-        CNCJob require the 'kind' parameter. Perhaps the FlatCAMObj.plot() has to be rewrited
-        :return:
-        """
-
-        # Does all the required setup and returns False
-        # if the 'ptint' option is set to False.
-        if not FlatCAMObj.plot(self):
-            return
-
-        try:
-            # plot solid geometries found as members of self.tools attribute dict
-            # for MultiGeo
-            if self.multigeo is True:  # geo multi tool usage
-                for tooluid_key in self.tools:
-                    solid_geometry = self.tools[tooluid_key]['solid_geometry']
-                    self.plot_element(solid_geometry, visible=visible)
-
-            # plot solid geometry that may be an direct attribute of the geometry object
-            # for SingleGeo
-            if self.solid_geometry:
-                self.plot_element(self.solid_geometry, visible=visible)
-
-            # self.plot_element(self.solid_geometry, visible=self.options['plot'])
-            self.shapes.redraw()
-        except (ObjectDeleted, AttributeError):
-            self.shapes.clear(update=True)
-
-    def on_plot_cb_click(self, *args):
-        if self.muted_ui:
-            return
-        self.plot()
-        self.read_form_item('plot')
-
-        self.ui_disconnect()
-        cb_flag = self.ui.plot_cb.isChecked()
-        for row in range(self.ui.geo_tools_table.rowCount()):
-            table_cb = self.ui.geo_tools_table.cellWidget(row, 6)
-            if cb_flag:
-                table_cb.setChecked(True)
-            else:
-                table_cb.setChecked(False)
-        self.ui_connect()
-
-    def on_plot_cb_click_table(self):
-        # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
-        self.ui_disconnect()
-        # cw = self.sender()
-        # cw_index = self.ui.geo_tools_table.indexAt(cw.pos())
-        # cw_row = cw_index.row()
-        check_row = 0
-
-        self.shapes.clear(update=True)
-        for tooluid_key in self.tools:
-            solid_geometry = self.tools[tooluid_key]['solid_geometry']
-
-            # find the geo_tool_table row associated with the tooluid_key
-            for row in range(self.ui.geo_tools_table.rowCount()):
-                tooluid_item = int(self.ui.geo_tools_table.item(row, 5).text())
-                if tooluid_item == int(tooluid_key):
-                    check_row = row
-                    break
-            if self.ui.geo_tools_table.cellWidget(check_row, 6).isChecked():
-                self.plot_element(element=solid_geometry, visible=True)
-        self.shapes.redraw()
-
-        # make sure that the general plot is disabled if one of the row plot's are disabled and
-        # if all the row plot's are enabled also enable the general plot checkbox
-        cb_cnt = 0
-        total_row = self.ui.geo_tools_table.rowCount()
-        for row in range(total_row):
-            if self.ui.geo_tools_table.cellWidget(row, 6).isChecked():
-                cb_cnt += 1
-            else:
-                cb_cnt -= 1
-        if cb_cnt < total_row:
-            self.ui.plot_cb.setChecked(False)
-        else:
-            self.ui.plot_cb.setChecked(True)
-        self.ui_connect()
-
-
-class FlatCAMCNCjob(FlatCAMObj, CNCjob):
-    """
-    Represents G-Code.
-    """
-    optionChanged = QtCore.pyqtSignal(str)
-    ui_type = CNCObjectUI
-
-    def __init__(self, name, units="in", kind="generic", z_move=0.1,
-                 feedrate=3.0, feedrate_rapid=3.0, z_cut=-0.002, tooldia=0.0,
-                 spindlespeed=None):
-
-        FlatCAMApp.App.log.debug("Creating CNCJob object...")
-
-        CNCjob.__init__(self, units=units, kind=kind, z_move=z_move,
-                        feedrate=feedrate, feedrate_rapid=feedrate_rapid, z_cut=z_cut, tooldia=tooldia,
-                        spindlespeed=spindlespeed, steps_per_circle=int(self.app.defaults["cncjob_steps_per_circle"]))
-
-        FlatCAMObj.__init__(self, name)
-
-        self.kind = "cncjob"
-
-        self.options.update({
-            "plot": True,
-            "tooldia": 0.03937,  # 0.4mm in inches
-            "append": "",
-            "prepend": "",
-            "dwell": False,
-            "dwelltime": 1,
-            "type": 'Geometry',
-            "toolchange_macro": '',
-            "toolchange_macro_enable": False
-        })
-
-        '''
-            This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the 
-            diameter of the tools and the value is another dict that will hold the data under the following form:
-               {tooldia:   {
-                           'tooluid': 1,
-                           'offset': 'Path',
-                           'type_item': 'Rough',
-                           'tool_type': 'C1',
-                           'data': {} # a dict to hold the parameters
-                           'gcode': "" # a string with the actual GCODE
-                           'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry 
-                           (cut or move)
-                           'solid_geometry': []
-                           },
-                           ...
-               }
-            It is populated in the FlatCAMGeometry.mtool_gen_cncjob()
-            BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
-        '''
-        self.cnc_tools = {}
-
-        '''
-           This is a dict of dictionaries. Each dict is associated with a tool present in the file. The key is the 
-           diameter of the tools and the value is another dict that will hold the data under the following form:
-              {tooldia:   {
-                          'tool': int,
-                          'nr_drills': int,
-                          'nr_slots': int,
-                          'offset': float,
-                          'data': {} # a dict to hold the parameters
-                          'gcode': "" # a string with the actual GCODE
-                          'gcode_parsed': {} # dictionary holding the CNCJob geometry and type of geometry (cut or move)
-                          'solid_geometry': []
-                          },
-                          ...
-              }
-           It is populated in the FlatCAMExcellon.on_create_cncjob_click() but actually 
-           it's done in camlib.Excellon.generate_from_excellon_by_tool()
-           BEWARE: I rely on the ordered nature of the Python 3.7 dictionary. Things might change ...
-       '''
-        self.exc_cnc_tools = {}
-
-        # flag to store if the CNCJob is part of a special group of CNCJob objects that can't be processed by the
-        # default engine of FlatCAM. They generated by some of tools and are special cases of CNCJob objects.
-        self.special_group = None
-
-        # for now it show if the plot will be done for multi-tool CNCJob (True) or for single tool
-        # (like the one in the TCL Command), False
-        self.multitool = False
-
-        # used for parsing the GCode lines to adjust the GCode when the GCode is offseted or scaled
-        gcodex_re_string = r'(?=.*(X[-\+]?\d*\.\d*))'
-        self.g_x_re = re.compile(gcodex_re_string)
-        gcodey_re_string = r'(?=.*(Y[-\+]?\d*\.\d*))'
-        self.g_y_re = re.compile(gcodey_re_string)
-        gcodez_re_string = r'(?=.*(Z[-\+]?\d*\.\d*))'
-        self.g_z_re = re.compile(gcodez_re_string)
-
-        gcodef_re_string = r'(?=.*(F[-\+]?\d*\.\d*))'
-        self.g_f_re = re.compile(gcodef_re_string)
-        gcodet_re_string = r'(?=.*(\=\s*[-\+]?\d*\.\d*))'
-        self.g_t_re = re.compile(gcodet_re_string)
-
-        gcodenr_re_string = r'([+-]?\d*\.\d+)'
-        self.g_nr_re = re.compile(gcodenr_re_string)
-
-        # Attributes to be included in serialization
-        # Always append to it because it carries contents
-        # from predecessors.
-        self.ser_attrs += ['options', 'kind', 'cnc_tools', 'multitool']
-
-        self.annotation = self.app.plotcanvas.new_text_group()
-
-    def build_ui(self):
-        self.ui_disconnect()
-
-        FlatCAMObj.build_ui(self)
-
-        # if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it
-        if self.cnc_tools:
-            self.ui.cnc_tools_table.show()
-        else:
-            self.ui.cnc_tools_table.hide()
-
-        self.units = self.app.ui.general_defaults_form.general_app_group.units_radio.get_value().upper()
-
-        offset = 0
-        tool_idx = 0
-
-        n = len(self.cnc_tools)
-        self.ui.cnc_tools_table.setRowCount(n)
-
-        for dia_key, dia_value in self.cnc_tools.items():
-
-            tool_idx += 1
-            row_no = tool_idx - 1
-
-            id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
-            # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
-            self.ui.cnc_tools_table.setItem(row_no, 0, id)  # Tool name/id
-
-            # Make sure that the tool diameter when in MM is with no more than 2 decimals.
-            # There are no tool bits in MM with more than 2 decimals diameter.
-            # For INCH the decimals should be no more than 4. There are no tools under 10mils.
-            if self.units == 'MM':
-                dia_item = QtWidgets.QTableWidgetItem('%.2f' % float(dia_value['tooldia']))
-            else:
-                dia_item = QtWidgets.QTableWidgetItem('%.4f' % float(dia_value['tooldia']))
-
-            offset_txt = list(str(dia_value['offset']))
-            offset_txt[0] = offset_txt[0].upper()
-            offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt))
-            type_item = QtWidgets.QTableWidgetItem(str(dia_value['type']))
-            tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type']))
-
-            id.setFlags(QtCore.Qt.ItemIsEnabled)
-            dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
-            offset_item.setFlags(QtCore.Qt.ItemIsEnabled)
-            type_item.setFlags(QtCore.Qt.ItemIsEnabled)
-            tool_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
-
-            # hack so the checkbox stay centered in the table cell
-            # used this:
-            # https://stackoverflow.com/questions/32458111/pyqt-allign-checkbox-and-put-it-in-every-row
-            # plot_item = QtWidgets.QWidget()
-            # checkbox = FCCheckBox()
-            # checkbox.setCheckState(QtCore.Qt.Checked)
-            # qhboxlayout = QtWidgets.QHBoxLayout(plot_item)
-            # qhboxlayout.addWidget(checkbox)
-            # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
-            # qhboxlayout.setContentsMargins(0, 0, 0, 0)
-            plot_item = FCCheckBox()
-            plot_item.setLayoutDirection(QtCore.Qt.RightToLeft)
-            tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key))
-            if self.ui.plot_cb.isChecked():
-                plot_item.setChecked(True)
-
-            self.ui.cnc_tools_table.setItem(row_no, 1, dia_item)  # Diameter
-            self.ui.cnc_tools_table.setItem(row_no, 2, offset_item)  # Offset
-            self.ui.cnc_tools_table.setItem(row_no, 3, type_item)  # Toolpath Type
-            self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item)  # Tool Type
-
-            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
-            self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item)  # Tool unique ID)
-            self.ui.cnc_tools_table.setCellWidget(row_no, 6, plot_item)
-
-        # make the diameter column editable
-        # for row in range(tool_idx):
-        #     self.ui.cnc_tools_table.item(row, 1).setFlags(QtCore.Qt.ItemIsSelectable |
-        #                                                   QtCore.Qt.ItemIsEnabled)
-
-        for row in range(tool_idx):
-            self.ui.cnc_tools_table.item(row, 0).setFlags(
-                self.ui.cnc_tools_table.item(row, 0).flags() ^ QtCore.Qt.ItemIsSelectable)
-
-        self.ui.cnc_tools_table.resizeColumnsToContents()
-        self.ui.cnc_tools_table.resizeRowsToContents()
-
-        vertical_header = self.ui.cnc_tools_table.verticalHeader()
-        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
-        vertical_header.hide()
-        self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-
-        horizontal_header = self.ui.cnc_tools_table.horizontalHeader()
-        horizontal_header.setMinimumSectionSize(10)
-        horizontal_header.setDefaultSectionSize(70)
-        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(0, 20)
-        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
-        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
-        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(4, 40)
-        horizontal_header.setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed)
-        horizontal_header.resizeSection(4, 17)
-        # horizontal_header.setStretchLastSection(True)
-        self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
-
-        self.ui.cnc_tools_table.setColumnWidth(0, 20)
-        self.ui.cnc_tools_table.setColumnWidth(4, 40)
-        self.ui.cnc_tools_table.setColumnWidth(6, 17)
-
-        # self.ui.geo_tools_table.setSortingEnabled(True)
-
-        self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
-        self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
-
-        self.ui_connect()
-
-    def set_ui(self, ui):
-        FlatCAMObj.set_ui(self, ui)
-
-        FlatCAMApp.App.log.debug("FlatCAMCNCJob.set_ui()")
-
-        assert isinstance(self.ui, CNCObjectUI), \
-            "Expected a CNCObjectUI, got %s" % type(self.ui)
-
-        # this signal has to be connected to it's slot before the defaults are populated
-        # the decision done in the slot has to override the default value set bellow
-        self.ui.toolchange_cb.toggled.connect(self.on_toolchange_custom_clicked)
-
-        self.form_fields.update({
-            "plot": self.ui.plot_cb,
-            # "tooldia": self.ui.tooldia_entry,
-            "append": self.ui.append_text,
-            "prepend": self.ui.prepend_text,
-            "toolchange_macro": self.ui.toolchange_text,
-            "toolchange_macro_enable": self.ui.toolchange_cb
-        })
-
-        # Fill form fields only on object create
-        self.to_form()
-
-        # this means that the object that created this CNCJob was an Excellon
-        try:
-            if self.travel_distance:
-                self.ui.t_distance_label.show()
-                self.ui.t_distance_entry.setVisible(True)
-                self.ui.t_distance_entry.setDisabled(True)
-                self.ui.t_distance_entry.set_value('%.4f' % float(self.travel_distance))
-                self.ui.units_label.setText(str(self.units).lower())
-                self.ui.units_label.setDisabled(True)
-        except AttributeError:
-            pass
-
-        # set the kind of geometries are plotted by default with plot2() from camlib.CNCJob
-        self.ui.cncplot_method_combo.set_value(self.app.defaults["cncjob_plot_kind"])
-
-        try:
-            self.ui.annotation_cb.stateChanged.disconnect(self.on_annotation_change)
-        except (TypeError, AttributeError):
-            pass
-        self.ui.annotation_cb.stateChanged.connect(self.on_annotation_change)
-
-        # set if to display text annotations
-        self.ui.annotation_cb.set_value(self.app.defaults["cncjob_annotation"])
-
-        # Show/Hide Advanced Options
-        if self.app.defaults["global_app_level"] == 'b':
-            self.ui.level.setText(_(
-                '<span style="color:green;"><b>Basic</b></span>'
-            ))
-
-            self.ui.cnc_frame.hide()
-        else:
-            self.ui.level.setText(_(
-                '<span style="color:red;"><b>Advanced</b></span>'
-            ))
-            self.ui.cnc_frame.show()
-
-        self.ui.updateplot_button.clicked.connect(self.on_updateplot_button_click)
-        self.ui.export_gcode_button.clicked.connect(self.on_exportgcode_button_click)
-        self.ui.modify_gcode_button.clicked.connect(self.on_edit_code_click)
-
-        self.ui.tc_variable_combo.currentIndexChanged[str].connect(self.on_cnc_custom_parameters)
-
-        self.ui.cncplot_method_combo.activated_custom.connect(self.on_plot_kind_change)
-
-    def on_cnc_custom_parameters(self, signal_text):
-        if signal_text == 'Parameters':
-            return
-        else:
-            self.ui.toolchange_text.insertPlainText('%%%s%%' % signal_text)
-
-    def ui_connect(self):
-        for row in range(self.ui.cnc_tools_table.rowCount()):
-            self.ui.cnc_tools_table.cellWidget(row, 6).clicked.connect(self.on_plot_cb_click_table)
-        self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
-
-    def ui_disconnect(self):
-        for row in range(self.ui.cnc_tools_table.rowCount()):
-            self.ui.cnc_tools_table.cellWidget(row, 6).clicked.disconnect(self.on_plot_cb_click_table)
-        try:
-            self.ui.plot_cb.stateChanged.disconnect(self.on_plot_cb_click)
-        except (TypeError, AttributeError):
-            pass
-
-    def on_updateplot_button_click(self, *args):
-        """
-        Callback for the "Updata Plot" button. Reads the form for updates
-        and plots the object.
-        """
-        self.read_form()
-        self.plot()
-
-    def on_plot_kind_change(self):
-        kind = self.ui.cncplot_method_combo.get_value()
-        self.plot(kind=kind)
-
-    def on_exportgcode_button_click(self, *args):
-        self.app.report_usage("cncjob_on_exportgcode_button")
-
-        self.read_form()
-        name = self.app.collection.get_active().options['name']
-
-        if 'Roland' in self.pp_excellon_name or 'Roland' in self.pp_geometry_name:
-            _filter_ = "RML1 Files (*.rol);;" \
-                       "All Files (*.*)"
-        elif 'hpgl' in self.pp_geometry_name:
-            _filter_ = "HPGL Files (*.plt);;" \
-                       "All Files (*.*)"
-        else:
-            _filter_ = "G-Code Files (*.nc);;G-Code Files (*.txt);;G-Code Files (*.tap);;G-Code Files (*.cnc);;" \
-                       "G-Code Files (*.g-code);;All Files (*.*)"
-
-        try:
-            dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(
-                caption=_("Export Machine Code ..."),
-                directory=dir_file_to_save,
-                filter=_filter_
-            )
-        except TypeError:
-            filename, _f = QtWidgets.QFileDialog.getSaveFileName(caption=_("Export Machine Code ..."), filter=_filter_)
-
-        filename = str(filename)
-
-        if filename == '':
-            self.app.inform.emit(_(
-                "[WARNING_NOTCL] Export Machine Code cancelled ..."))
-            return
-
-        new_name = os.path.split(str(filename))[1].rpartition('.')[0]
-        self.ui.name_entry.set_value(new_name)
-        self.on_name_activate(silent=True)
-
-        preamble = str(self.ui.prepend_text.get_value())
-        postamble = str(self.ui.append_text.get_value())
-
-        gc = self.export_gcode(filename, preamble=preamble, postamble=postamble)
-        if gc == 'fail':
-            return
-
-        if self.app.defaults["global_open_style"] is False:
-            self.app.file_opened.emit("gcode", filename)
-        self.app.file_saved.emit("gcode", filename)
-        self.app.inform.emit(_("[success] Machine Code file saved to: %s") % filename)
-
-    def on_edit_code_click(self, *args):
-        preamble = str(self.ui.prepend_text.get_value())
-        postamble = str(self.ui.append_text.get_value())
-        gc = self.export_gcode(preamble=preamble, postamble=postamble, to_file=True)
-        if gc == 'fail':
-            return
-        else:
-            self.app.gcode_edited = gc
-
-        self.app.init_code_editor(name=_("Code Editor"))
-        self.app.ui.buttonOpen.clicked.connect(self.app.handleOpen)
-        self.app.ui.buttonSave.clicked.connect(self.app.handleSaveGCode)
-
-        # then append the text from GCode to the text editor
-        try:
-            for line in self.app.gcode_edited:
-                proc_line = str(line).strip('\n')
-                self.app.ui.code_editor.append(proc_line)
-        except Exception as e:
-            log.debug('FlatCAMCNNJob.on_edit_code_click() -->%s' % str(e))
-            self.app.inform.emit(_('[ERROR]FlatCAMCNNJob.on_edit_code_click() -->%s') % str(e))
-            return
-
-        self.app.ui.code_editor.moveCursor(QtGui.QTextCursor.Start)
-
-        self.app.handleTextChanged()
-        self.app.ui.show()
-
-    def gcode_header(self):
-        log.debug("FlatCAMCNCJob.gcode_header()")
-        time_str = "{:%A, %d %B %Y at %H:%M}".format(datetime.now())
-        marlin = False
-        hpgl = False
-        probe_pp = False
-
-        try:
-            for key in self.cnc_tools:
-                ppg = self.cnc_tools[key]['data']['ppname_g']
-                if ppg == 'marlin' or ppg == 'Repetier':
-                    marlin = True
-                    break
-                if ppg == 'hpgl':
-                    hpgl = True
-                    break
-                if "toolchange_probe" in ppg.lower():
-                    probe_pp = True
-                    break
-        except Exception as e:
-            log.debug("FlatCAMCNCJob.gcode_header() error: --> %s" % str(e))
-
-        try:
-            if self.options['ppname_e'] == 'marlin' or self.options['ppname_e'] == 'Repetier':
-                marlin = True
-        except Exception as e:
-            log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
-
-        try:
-            if "toolchange_probe" in self.options['ppname_e'].lower():
-                probe_pp = True
-        except Exception as e:
-            log.debug("FlatCAMCNCJob.gcode_header(): --> There is no such self.option: %s" % str(e))
-
-        if marlin is True:
-            gcode = ';Marlin(Repetier) G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date:    %s\n' % \
-                    (str(self.app.version), str(self.app.version_date)) + '\n'
-
-            gcode += ';Name: ' + str(self.options['name']) + '\n'
-            gcode += ';Type: ' + "G-code from " + str(self.options['type']) + '\n'
-
-            # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            #     gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
-
-            gcode += ';Units: ' + self.units.upper() + '\n' + "\n"
-            gcode += ';Created on ' + time_str + '\n' + '\n'
-        elif hpgl is True:
-            gcode = 'CO "HPGL CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date:    %s' % \
-                    (str(self.app.version), str(self.app.version_date)) + '";\n'
-
-            gcode += 'CO "Name: ' + str(self.options['name']) + '";\n'
-            gcode += 'CO "Type: ' + "HPGL code from " + str(self.options['type']) + '";\n'
-
-            # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            #     gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
-
-            gcode += 'CO "Units: ' + self.units.upper() + '";\n'
-            gcode += 'CO "Created on ' + time_str + '";\n'
-        elif probe_pp is True:
-            gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
-                    (str(self.app.version), str(self.app.version_date)) + '\n'
-
-            gcode += '(This GCode tool change is done by using a Probe.)\n' \
-                     '(Make sure that before you start the job you first do a rough zero for Z axis.)\n' \
-                     '(This means that you need to zero the CNC axis and then jog to the toolchange X, Y location,)\n' \
-                     '(mount the probe and adjust the Z so more or less the probe tip touch the plate. ' \
-                     'Then zero the Z axis.)\n' + '\n'
-
-            gcode += '(Name: ' + str(self.options['name']) + ')\n'
-            gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
-
-            # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            #     gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
-
-            gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
-            gcode += '(Created on ' + time_str + ')\n' + '\n'
-        else:
-            gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
-                    (str(self.app.version), str(self.app.version_date)) + '\n'
-
-            gcode += '(Name: ' + str(self.options['name']) + ')\n'
-            gcode += '(Type: ' + "G-code from " + str(self.options['type']) + ')\n'
-
-            # if str(p['options']['type']) == 'Excellon' or str(p['options']['type']) == 'Excellon Geometry':
-            #     gcode += '(Tools in use: ' + str(p['options']['Tools_in_use']) + ')\n'
-
-            gcode += '(Units: ' + self.units.upper() + ')\n' + "\n"
-            gcode += '(Created on ' + time_str + ')\n' + '\n'
-
-        return gcode
-
-    def gcode_footer(self, end_command=None):
-        """
-
-        :param end_command: 'M02' or 'M30' - String
-        :return:
-        """
-        if end_command:
-            return end_command
-        else:
-            return 'M02'
-
-    def export_gcode(self, filename=None, preamble='', postamble='', to_file=False):
-        gcode = ''
-        roland = False
-        hpgl = False
-
-        try:
-            if self.special_group:
-                self.app.inform.emit(_("[WARNING_NOTCL] This CNCJob object can't be processed because "
-                                     "it is a %s CNCJob object.") % str(self.special_group))
-                return 'fail'
-        except AttributeError:
-            pass
-
-        # detect if using Roland postprocessor
-        try:
-            for key in self.cnc_tools:
-                if self.cnc_tools[key]['data']['ppname_g'] == 'Roland_MDX_20':
-                    roland = True
-                    break
-                if self.cnc_tools[key]['data']['ppname_g'] == 'hpgl':
-                    hpgl = True
-                    break
-        except Exception as e:
-            try:
-                for key in self.cnc_tools:
-                    if self.cnc_tools[key]['data']['ppname_e'] == 'Roland_MDX_20':
-                        roland = True
-                        break
-            except Exception as e:
-                pass
-
-        # do not add gcode_header when using the Roland postprocessor, add it for every other postprocessor
-        if roland is False and hpgl is False:
-            gcode = self.gcode_header()
-
-        # detect if using multi-tool and make the Gcode summation correctly for each case
-        if self.multitool is True:
-            for tooluid_key in self.cnc_tools:
-                for key, value in self.cnc_tools[tooluid_key].items():
-                    if key == 'gcode':
-                        gcode += value
-                        break
-        else:
-            gcode += self.gcode
-
-        if roland is True:
-            g = preamble + gcode + postamble
-        elif hpgl is True:
-            g = self.gcode_header() + preamble + gcode + postamble
-        else:
-            # fix so the preamble gets inserted in between the comments header and the actual start of GCODE
-            g_idx = gcode.rfind('G20')
-
-            # if it did not find 'G20' then search for 'G21'
-            if g_idx == -1:
-                g_idx = gcode.rfind('G21')
-
-            # if it did not find 'G20' and it did not find 'G21' then there is an error and return
-            if g_idx == -1:
-                self.app.inform.emit(_(
-                    "[ERROR_NOTCL] G-code does not have a units code: either G20 or G21"
-                ))
-                return
-
-            g = gcode[:g_idx] + preamble + '\n' + gcode[g_idx:] + postamble + self.gcode_footer()
-
-        # if toolchange custom is used, replace M6 code with the code from the Toolchange Custom Text box
-        if self.ui.toolchange_cb.get_value() is True:
-            # match = self.re_toolchange.search(g)
-            if 'M6' in g:
-                m6_code = self.parse_custom_toolchange_code(self.ui.toolchange_text.get_value())
-                if m6_code is None or m6_code == '':
-                    self.app.inform.emit(_(
-                        "[ERROR_NOTCL] Cancelled. The Toolchange Custom code is enabled "
-                        "but it's empty."
-                    ))
-                    return 'fail'
-
-                g = g.replace('M6', m6_code)
-                self.app.inform.emit(_(
-                    "[success] Toolchange G-code was replaced by a custom code."
-                ))
-
-        # lines = StringIO(self.gcode)
-        lines = StringIO(g)
-
-        # Write
-        if filename is not None:
-            try:
-                with open(filename, 'w') as f:
-                    for line in lines:
-                        f.write(line)
-            except FileNotFoundError:
-                self.app.inform.emit(_(
-                    "[WARNING_NOTCL] No such file or directory"
-                ))
-                return
-            except PermissionError:
-                self.app.inform.emit(_("[WARNING] Permission denied, saving not possible.\n"
-                                       "Most likely another app is holding the file open and not accessible."))
-                return 'fail'
-        elif to_file is False:
-            # Just for adding it to the recent files list.
-            if self.app.defaults["global_open_style"] is False:
-                self.app.file_opened.emit("cncjob", filename)
-            self.app.file_saved.emit("cncjob", filename)
-
-            self.app.inform.emit("[success] Saved to: " + filename)
-        else:
-            return lines
-
-    def on_toolchange_custom_clicked(self, signal):
-        try:
-            if 'toolchange_custom' not in str(self.options['ppname_e']).lower():
-                if self.ui.toolchange_cb.get_value():
-                    self.ui.toolchange_cb.set_value(False)
-                    self.app.inform.emit(
-                        _(
-                            "[WARNING_NOTCL] The used postprocessor file has to have in it's name: 'toolchange_custom'"
-                        ))
-        except KeyError:
-            try:
-                for key in self.cnc_tools:
-                    ppg = self.cnc_tools[key]['data']['ppname_g']
-                    if 'toolchange_custom' not in str(ppg).lower():
-                        print(ppg)
-                        if self.ui.toolchange_cb.get_value():
-                            self.ui.toolchange_cb.set_value(False)
-                            self.app.inform.emit(
-                                _(
-                                    "[WARNING_NOTCL] The used postprocessor file has to have in it's name: "
-                                    "'toolchange_custom'"
-                                ))
-            except KeyError:
-                self.app.inform.emit(
-                    _(
-                        "[ERROR] There is no postprocessor file."
-                    ))
-
-    def get_gcode(self, preamble='', postamble=''):
-        # we need this to be able get_gcode separatelly for shell command export_gcode
-        return preamble + '\n' + self.gcode + "\n" + postamble
-
-    def get_svg(self):
-        # we need this to be able get_svg separately for shell command export_svg
-        pass
-
-    def on_plot_cb_click(self, *args):
-        if self.muted_ui:
-            return
-        kind = self.ui.cncplot_method_combo.get_value()
-        self.plot(kind=kind)
-        self.read_form_item('plot')
-
-        self.ui_disconnect()
-        cb_flag = self.ui.plot_cb.isChecked()
-        for row in range(self.ui.cnc_tools_table.rowCount()):
-            table_cb = self.ui.cnc_tools_table.cellWidget(row, 6)
-            if cb_flag:
-                table_cb.setChecked(True)
-            else:
-                table_cb.setChecked(False)
-        self.ui_connect()
-
-    def on_plot_cb_click_table(self):
-        # self.ui.cnc_tools_table.cellWidget(row, 2).widget().setCheckState(QtCore.Qt.Unchecked)
-        self.ui_disconnect()
-        # cw = self.sender()
-        # cw_index = self.ui.cnc_tools_table.indexAt(cw.pos())
-        # cw_row = cw_index.row()
-
-        kind = self.ui.cncplot_method_combo.get_value()
-
-        self.shapes.clear(update=True)
-
-        for tooluid_key in self.cnc_tools:
-            tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia']))
-            gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
-            # tool_uid = int(self.ui.cnc_tools_table.item(cw_row, 3).text())
-
-            for r in range(self.ui.cnc_tools_table.rowCount()):
-                if int(self.ui.cnc_tools_table.item(r, 5).text()) == int(tooluid_key):
-                    if self.ui.cnc_tools_table.cellWidget(r, 6).isChecked():
-                        self.plot2(tooldia=tooldia, obj=self, visible=True, gcode_parsed=gcode_parsed, kind=kind)
-
-        self.shapes.redraw()
-
-        # make sure that the general plot is disabled if one of the row plot's are disabled and
-        # if all the row plot's are enabled also enable the general plot checkbox
-        cb_cnt = 0
-        total_row = self.ui.cnc_tools_table.rowCount()
-        for row in range(total_row):
-            if self.ui.cnc_tools_table.cellWidget(row, 6).isChecked():
-                cb_cnt += 1
-            else:
-                cb_cnt -= 1
-        if cb_cnt < total_row:
-            self.ui.plot_cb.setChecked(False)
-        else:
-            self.ui.plot_cb.setChecked(True)
-        self.ui_connect()
-
-    def plot(self, visible=None, kind='all'):
-
-        # Does all the required setup and returns False
-        # if the 'ptint' option is set to False.
-        if not FlatCAMObj.plot(self):
-            return
-
-        visible = visible if visible else self.options['plot']
-
-        try:
-            if self.multitool is False:  # single tool usage
-                self.plot2(tooldia=float(self.options["tooldia"]), obj=self, visible=visible, kind=kind)
-            else:
-                # multiple tools usage
-                for tooluid_key in self.cnc_tools:
-                    tooldia = float('%.4f' % float(self.cnc_tools[tooluid_key]['tooldia']))
-                    gcode_parsed = self.cnc_tools[tooluid_key]['gcode_parsed']
-                    self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
-            self.shapes.redraw()
-        except (ObjectDeleted, AttributeError):
-            self.shapes.clear(update=True)
-            self.annotation.clear(update=True)
-
-        if self.ui.annotation_cb.get_value() and self.ui.plot_cb.get_value():
-            self.app.plotcanvas.text_collection.enabled = True
-        else:
-            self.app.plotcanvas.text_collection.enabled = False
-
-    def on_annotation_change(self):
-        if self.ui.annotation_cb.get_value():
-            self.app.plotcanvas.text_collection.enabled = True
-        else:
-            self.app.plotcanvas.text_collection.enabled = False
-        # kind = self.ui.cncplot_method_combo.get_value()
-        # self.plot(kind=kind)
-        self.annotation.redraw()
-
-    def convert_units(self, units):
-        factor = CNCjob.convert_units(self, units)
-        FlatCAMApp.App.log.debug("FlatCAMCNCjob.convert_units()")
-        self.options["tooldia"] = float(self.options["tooldia"]) * factor
-
-        param_list = ['cutz', 'depthperpass', 'travelz', 'feedrate', 'feedrate_z', 'feedrate_rapid',
-                      'endz', 'toolchangez']
-
-        temp_tools_dict = {}
-        tool_dia_copy = {}
-        data_copy = {}
-
-        for tooluid_key, tooluid_value in self.cnc_tools.items():
-            for dia_key, dia_value in tooluid_value.items():
-                if dia_key == 'tooldia':
-                    dia_value *= factor
-                    dia_value = float('%.4f' % dia_value)
-                    tool_dia_copy[dia_key] = dia_value
-                if dia_key == 'offset':
-                    tool_dia_copy[dia_key] = dia_value
-                if dia_key == 'offset_value':
-                    dia_value *= factor
-                    tool_dia_copy[dia_key] = dia_value
-
-                if dia_key == 'type':
-                    tool_dia_copy[dia_key] = dia_value
-                if dia_key == 'tool_type':
-                    tool_dia_copy[dia_key] = dia_value
-                if dia_key == 'data':
-                    for data_key, data_value in dia_value.items():
-                        # convert the form fields that are convertible
-                        for param in param_list:
-                            if data_key == param and data_value is not None:
-                                data_copy[data_key] = data_value * factor
-                        # copy the other dict entries that are not convertible
-                        if data_key not in param_list:
-                            data_copy[data_key] = data_value
-                    tool_dia_copy[dia_key] = deepcopy(data_copy)
-                    data_copy.clear()
-
-                if dia_key == 'gcode':
-                    tool_dia_copy[dia_key] = dia_value
-                if dia_key == 'gcode_parsed':
-                    tool_dia_copy[dia_key] = dia_value
-                if dia_key == 'solid_geometry':
-                    tool_dia_copy[dia_key] = dia_value
-
-                # if dia_key == 'solid_geometry':
-                #     tool_dia_copy[dia_key] = affinity.scale(dia_value, xfact=factor, origin=(0, 0))
-                # if dia_key == 'gcode_parsed':
-                #     for g in dia_value:
-                #         g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
-                #
-                #     tool_dia_copy['gcode_parsed'] = deepcopy(dia_value)
-                #     tool_dia_copy['solid_geometry'] = cascaded_union([geo['geom'] for geo in dia_value])
-
-            temp_tools_dict.update({
-                tooluid_key: deepcopy(tool_dia_copy)
-            })
-            tool_dia_copy.clear()
-
-        self.cnc_tools.clear()
-        self.cnc_tools = deepcopy(temp_tools_dict)
-
-# end of file

+ 0 - 92
FlatCAMTool.py

@@ -1,92 +0,0 @@
-# ########################################################## ##
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
-# MIT Licence                                              #
-# ########################################################## ##
-
-from PyQt5 import QtGui, QtCore, QtWidgets, QtWidgets
-from PyQt5.QtCore import Qt
-
-
-class FlatCAMTool(QtWidgets.QWidget):
-
-    toolName = "FlatCAM Generic Tool"
-
-    def __init__(self, app, parent=None):
-        """
-
-        :param app: The application this tool will run in.
-        :type app: App
-        :param parent: Qt Parent
-        :return: FlatCAMTool
-        """
-        QtWidgets.QWidget.__init__(self, parent)
-
-        # self.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)
-
-        self.layout = QtWidgets.QVBoxLayout()
-        self.setLayout(self.layout)
-
-        self.app = app
-
-        self.menuAction = None
-
-    def install(self, icon=None, separator=None, shortcut=None, **kwargs):
-        before = None
-
-        # 'pos' is the menu where the Action has to be installed
-        # if no 'pos' kwarg is provided then by default our Action will be installed in the menutool
-        # as it previously was
-        if 'pos' in kwargs:
-            pos = kwargs['pos']
-        else:
-            pos = self.app.ui.menutool
-
-        # 'before' is the Action in the menu stated by 'pos' kwarg, before which we want our Action to be installed
-        # if 'before' kwarg is not provided, by default our Action will be added in the last place.
-        if 'before' in kwargs:
-            before = (kwargs['before'])
-
-        # create the new Action
-        self.menuAction = QtWidgets.QAction(self)
-        # if provided, add an icon to this Action
-        if icon is not None:
-            self.menuAction.setIcon(icon)
-
-        # set the text name of the Action, which will be displayed in the menu
-        if shortcut is None:
-            self.menuAction.setText(self.toolName)
-        else:
-            self.menuAction.setText(self.toolName + '\t%s' % shortcut)
-
-        # add a ToolTip to the new Action
-        # self.menuAction.setToolTip(self.toolTip) # currently not available
-
-        # insert the action in the position specified by 'before' and 'pos' kwargs
-        pos.insertAction(before, self.menuAction)
-
-        # if separator parameter is True add a Separator after the newly created Action
-        if separator is True:
-            pos.addSeparator()
-
-        self.menuAction.triggered.connect(self.run)
-
-    def run(self):
-
-        if self.app.tool_tab_locked is True:
-            return
-        # Remove anything else in the GUI
-        self.app.ui.tool_scroll_area.takeWidget()
-
-        # Put ourself in the GUI
-        self.app.ui.tool_scroll_area.setWidget(self)
-
-        # Switch notebook to tool page
-        self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
-
-        # Set the tool name as the widget object name
-        self.app.ui.tool_scroll_area.widget().setObjectName(self.toolName)
-
-        self.show()

+ 50 - 0
Makefile

@@ -0,0 +1,50 @@
+
+# Install on Ubuntu(-like) systems
+
+# Install dependencies system-wide (including python modules)
+install_dependencies:
+	sudo -H ./setup_ubuntu.sh
+
+USER_ID = $(shell id -u)
+
+LOCAL_PATH = $(shell pwd)
+LOCAL_APPS_PATH = ~/.local/share/applications
+ASSEST_PATH = assets/linux
+
+INSTALL_PATH = /usr/share/flatcam-beta
+APPS_PATH = /usr/share/applications
+
+MIN_PY3_MINOR_VERSION := 5
+PY3_MINOR_VERSION := $(shell python3 --version | cut -d'.' -f2)
+
+ifneq ($(MIN_PY3_MINOR_VERSION), $(firstword $(sort $(PY3_MINOR_VERSION) $(MIN_PY3_MINOR_VERSION))))
+    $(info Current python version is 3.$(PY3_MINOR_VERSION))
+    $(error You must have at least 3.$(MIN_PY3_MINOR_VERSION) installed)
+endif
+
+install:
+ifeq ($(USER_ID), 0)
+	@ echo "Installing it system-wide"
+	cp -rf $(LOCAL_PATH) $(INSTALL_PATH)
+	@ sed -i "s|python_script_path=.*|python_script_path=$(INSTALL_PATH)|g" $(INSTALL_PATH)/assets/linux/flatcam-beta
+	ln -sf $(INSTALL_PATH)/assets/linux/flatcam-beta /usr/local/bin
+	cp -f $(ASSEST_PATH)/flatcam-beta.desktop $(APPS_PATH)
+	@ sed -i "s|Exec=.*|Exec=$(INSTALL_PATH)/$(ASSEST_PATH)/flatcam-beta|g" $(APPS_PATH)/flatcam-beta.desktop
+	@ sed -i "s|Icon=.*|Icon=$(INSTALL_PATH)/$(ASSEST_PATH)/icon.png|g" $(APPS_PATH)/flatcam-beta.desktop
+else
+	@ echo "Installing locally for $(USER) only"
+	cp -f $(ASSEST_PATH)/flatcam-beta.desktop $(LOCAL_APPS_PATH)
+	@ sed -i "s|Exec=.*|Exec=$(LOCAL_PATH)/$(ASSEST_PATH)/flatcam-beta|g" $(LOCAL_APPS_PATH)/flatcam-beta.desktop
+	@ sed -i "s|Icon=.*|Icon=$(LOCAL_PATH)/$(ASSEST_PATH)/icon.png|g" $(LOCAL_APPS_PATH)/flatcam-beta.desktop
+endif
+
+remove:
+ifeq ($(USER_ID), 0)
+	@ echo "Uninstalling it system-wide"
+	rm -rf $(INSTALL_PATH)
+	rm -f /usr/local/bin/flatcam-beta
+	rm -r $(APPS_PATH)/flatcam-beta.desktop
+else
+	@ echo "Uninstalling only for $(USER) user"
+	rm -f $(LOCAL_APPS_PATH)/flatcam-beta.desktop
+endif

+ 0 - 771
ObjectCollection.py

@@ -1,771 +0,0 @@
-# ########################################################## ##
-# FlatCAM: 2D Post-processing for Manufacturing            #
-# http://flatcam.org                                       #
-# Author: Juan Pablo Caram (c)                             #
-# Date: 2/5/2014                                           #
-# MIT Licence                                              #
-# ########################################################## ##
-
-# ########################################################## ##
-# File modified by: Dennis Hayrullin                       #
-# ########################################################## ##
-
-# from PyQt5.QtCore import QModelIndex
-from FlatCAMObj import *
-import inspect  # TODO: Remove
-import FlatCAMApp
-from PyQt5 import QtGui, QtCore, QtWidgets
-from PyQt5.QtCore import Qt
-# import webbrowser
-
-import gettext
-import FlatCAMTranslation as fcTranslate
-import builtins
-
-fcTranslate.apply_language('strings')
-if '_' not in builtins.__dict__:
-    _ = gettext.gettext
-
-
-class KeySensitiveListView(QtWidgets.QTreeView):
-    """
-    QtGui.QListView extended to emit a signal on key press.
-    """
-
-    def __init__(self, app, parent=None):
-        super(KeySensitiveListView, self).__init__(parent)
-        self.setHeaderHidden(True)
-        self.setEditTriggers(QtWidgets.QTreeView.SelectedClicked)
-
-        # self.setRootIsDecorated(False)
-        # self.setExpandsOnDoubleClick(False)
-
-        # Enable dragging and dropping onto the GUI
-        self.setAcceptDrops(True)
-        self.filename = ""
-        self.app = app
-
-    keyPressed = QtCore.pyqtSignal(int)
-
-    def keyPressEvent(self, event):
-        # super(KeySensitiveListView, self).keyPressEvent(event)
-        self.keyPressed.emit(event.key())
-
-    def dragEnterEvent(self, event):
-        if event.mimeData().hasUrls:
-            event.accept()
-        else:
-            event.ignore()
-
-    def dragMoveEvent(self, event):
-        self.setDropIndicatorShown(True)
-        if event.mimeData().hasUrls:
-            event.accept()
-        else:
-            event.ignore()
-
-    def dropEvent(self, event):
-        drop_indicator = self.dropIndicatorPosition()
-
-        m = event.mimeData()
-        if m.hasUrls:
-            event.accept()
-
-            for url in m.urls():
-                self.filename = str(url.toLocalFile())
-
-            # file drop from outside application
-            if drop_indicator == QtWidgets.QAbstractItemView.OnItem:
-                if self.filename == "":
-                    self.app.inform.emit(_("Open cancelled."))
-                else:
-                    if self.filename.lower().rpartition('.')[-1] in self.app.grb_list:
-                        self.app.worker_task.emit({'fcn': self.app.open_gerber,
-                                                   'params': [self.filename]})
-                    else:
-                        event.ignore()
-
-                    if self.filename.lower().rpartition('.')[-1] in self.app.exc_list:
-                        self.app.worker_task.emit({'fcn': self.app.open_excellon,
-                                                   'params': [self.filename]})
-                    else:
-                        event.ignore()
-
-                    if self.filename.lower().rpartition('.')[-1] in self.app.gcode_list:
-                        self.app.worker_task.emit({'fcn': self.app.open_gcode,
-                                                   'params': [self.filename]})
-                    else:
-                        event.ignore()
-
-                    if self.filename.lower().rpartition('.')[-1] in self.app.svg_list:
-                        object_type = 'geometry'
-                        self.app.worker_task.emit({'fcn': self.app.import_svg,
-                                                   'params': [self.filename, object_type, None]})
-
-                    if self.filename.lower().rpartition('.')[-1] in self.app.dxf_list:
-                        object_type = 'geometry'
-                        self.app.worker_task.emit({'fcn': self.app.import_dxf,
-                                                   'params': [self.filename, object_type, None]})
-
-                    if self.filename.lower().rpartition('.')[-1] in self.app.prj_list:
-                        # self.app.open_project() is not Thread Safe
-                        self.app.open_project(self.filename)
-                    else:
-                        event.ignore()
-            else:
-                pass
-        else:
-            event.ignore()
-
-
-class TreeItem(KeySensitiveListView):
-    """
-    Item of a tree model
-    """
-
-    def __init__(self, data, icon=None, obj=None, parent_item=None):
-        super(TreeItem, self).__init__(parent_item)
-        self.parent_item = parent_item
-        self.item_data = data  # Columns string data
-        self.icon = icon  # Decoration
-        self.obj = obj  # FlatCAMObj
-
-        self.child_items = []
-
-        if parent_item:
-            parent_item.append_child(self)
-
-    def append_child(self, item):
-        self.child_items.append(item)
-        item.set_parent_item(self)
-
-    def remove_child(self, item):
-        child = self.child_items.pop(self.child_items.index(item))
-        child.obj.clear(True)
-        child.obj.delete()
-        del child.obj
-        del child
-
-    def remove_children(self):
-        for child in self.child_items:
-            child.obj.clear()
-            child.obj.delete()
-            del child.obj
-            del child
-
-        self.child_items = []
-
-    def child(self, row):
-        return self.child_items[row]
-
-    def child_count(self):
-        return len(self.child_items)
-
-    def column_count(self):
-        return len(self.item_data)
-
-    def data(self, column):
-        return self.item_data[column]
-
-    def row(self):
-        return self.parent_item.child_items.index(self)
-
-    def set_parent_item(self, parent_item):
-        self.parent_item = parent_item
-
-    def __del__(self):
-        del self.icon
-
-
-class ObjectCollection(QtCore.QAbstractItemModel):
-    """
-    Object storage and management.
-    """
-
-    groups = [
-        ("gerber", "Gerber"),
-        ("excellon", "Excellon"),
-        ("geometry", "Geometry"),
-        ("cncjob", "CNC Job")
-    ]
-
-    classdict = {
-        "gerber": FlatCAMGerber,
-        "excellon": FlatCAMExcellon,
-        "cncjob": FlatCAMCNCjob,
-        "geometry": FlatCAMGeometry
-    }
-
-    icon_files = {
-        "gerber": "share/flatcam_icon16.png",
-        "excellon": "share/drill16.png",
-        "cncjob": "share/cnc16.png",
-        "geometry": "share/geometry16.png"
-    }
-
-    root_item = None
-    # app = None
-
-    def __init__(self, app, parent=None):
-
-        QtCore.QAbstractItemModel.__init__(self)
-
-        # ## Icons for the list view
-        self.icons = {}
-        for kind in ObjectCollection.icon_files:
-            self.icons[kind] = QtGui.QPixmap(ObjectCollection.icon_files[kind])
-
-        # Create root tree view item
-        self.root_item = TreeItem(["root"])
-
-        # Create group items
-        self.group_items = {}
-        for kind, title in ObjectCollection.groups:
-            item = TreeItem([title], self.icons[kind])
-            self.group_items[kind] = item
-            self.root_item.append_child(item)
-
-        # Create test sub-items
-        # for i in self.root_item.m_child_items:
-        #     print i.data(0)
-        #     i.append_child(TreeItem(["empty"]))
-
-        # ## Data # ##
-        self.checked_indexes = []
-
-        # Names of objects that are expected to become available.
-        # For example, when the creation of a new object will run
-        # in the background and will complete some time in the
-        # future. This is a way to reserve the name and to let other
-        # tasks know that they have to wait until available.
-        self.promises = set()
-
-        # same as above only for objects that are plotted
-        self.plot_promises = set()
-
-        self.app = app
-
-        # ## View
-        self.view = KeySensitiveListView(app)
-        self.view.setModel(self)
-
-        self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
-        self.view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
-        # self.view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
-        # self.view.setDragEnabled(True)
-        # self.view.setAcceptDrops(True)
-        # self.view.setDropIndicatorShown(True)
-
-        font = QtGui.QFont()
-        font.setPixelSize(12)
-        font.setFamily("Seagoe UI")
-        self.view.setFont(font)
-
-        # ## GUI Events
-        self.view.selectionModel().selectionChanged.connect(self.on_list_selection_change)
-        # self.view.activated.connect(self.on_item_activated)
-        self.view.keyPressed.connect(self.app.ui.keyPressEvent)
-        # self.view.clicked.connect(self.on_mouse_down)
-        self.view.customContextMenuRequested.connect(self.on_menu_request)
-
-        self.click_modifier = None
-
-    def promise(self, obj_name):
-        FlatCAMApp.App.log.debug("Object %s has been promised." % obj_name)
-        self.promises.add(obj_name)
-
-    def has_promises(self):
-        return len(self.promises) > 0
-
-    def plot_promise(self, plot_obj_name):
-        self.plot_promises.add(plot_obj_name)
-
-    def plot_remove_promise(self, plot_obj_name):
-        if plot_obj_name in self.plot_promises:
-            self.plot_promises.remove(plot_obj_name)
-
-    def has_plot_promises(self):
-        return len(self.plot_promises) > 0
-
-    def on_mouse_down(self, event):
-        FlatCAMApp.App.log.debug("Mouse button pressed on list")
-
-    def on_menu_request(self, pos):
-
-        sel = len(self.view.selectedIndexes()) > 0
-        self.app.ui.menuprojectenable.setEnabled(sel)
-        self.app.ui.menuprojectdisable.setEnabled(sel)
-        self.app.ui.menuprojectviewsource.setEnabled(sel)
-
-        self.app.ui.menuprojectcopy.setEnabled(sel)
-        self.app.ui.menuprojectedit.setEnabled(sel)
-        self.app.ui.menuprojectdelete.setEnabled(sel)
-        self.app.ui.menuprojectsave.setEnabled(sel)
-        self.app.ui.menuprojectproperties.setEnabled(sel)
-
-        if sel:
-            self.app.ui.menuprojectgeneratecnc.setVisible(True)
-            self.app.ui.menuprojectedit.setVisible(True)
-            self.app.ui.menuprojectsave.setVisible(True)
-            self.app.ui.menuprojectviewsource.setVisible(True)
-
-            for obj in self.get_selected():
-                if type(obj) != FlatCAMGeometry:
-                    self.app.ui.menuprojectgeneratecnc.setVisible(False)
-                if type(obj) != FlatCAMGeometry and type(obj) != FlatCAMExcellon:
-                    self.app.ui.menuprojectedit.setVisible(False)
-                if type(obj) != FlatCAMGerber and type(obj) != FlatCAMExcellon:
-                    self.app.ui.menuprojectviewsource.setVisible(False)
-        else:
-            self.app.ui.menuprojectgeneratecnc.setVisible(False)
-
-        self.app.ui.menuproject.popup(self.view.mapToGlobal(pos))
-
-    def index(self, row, column=0, parent=None, *args, **kwargs):
-        if not self.hasIndex(row, column, parent):
-            return QtCore.QModelIndex()
-
-        # if not parent.isValid():
-        #     parent_item = self.root_item
-        # else:
-        #     parent_item = parent.internalPointer()
-        parent_item = parent.internalPointer() if parent.isValid() else self.root_item
-
-        child_item = parent_item.child(row)
-        if child_item:
-            return self.createIndex(row, column, child_item)
-        else:
-            return QtCore.QModelIndex()
-
-    def parent(self, index=None):
-        if not index.isValid():
-            return QtCore.QModelIndex()
-
-        parent_item = index.internalPointer().parent_item
-
-        if parent_item == self.root_item:
-            return QtCore.QModelIndex()
-
-        return self.createIndex(parent_item.row(), 0, parent_item)
-
-    def rowCount(self, index=None, *args, **kwargs):
-        if index.column() > 0:
-            return 0
-
-        if not index.isValid():
-            parent_item = self.root_item
-        else:
-            parent_item = index.internalPointer()
-
-        return parent_item.child_count()
-
-    def columnCount(self, index=None, *args, **kwargs):
-        if index.isValid():
-            return index.internalPointer().column_count()
-        else:
-            return self.root_item.column_count()
-
-    def data(self, index, role=None):
-        if not index.isValid():
-            return None
-
-        if role in [Qt.DisplayRole, Qt.EditRole]:
-            obj = index.internalPointer().obj
-            if obj:
-                return obj.options["name"]
-            else:
-                return index.internalPointer().data(index.column())
-
-        if role == Qt.ForegroundRole:
-            color = QColor(self.app.defaults['global_proj_item_color'])
-            color_disabled = QColor(self.app.defaults['global_proj_item_dis_color'])
-            obj = index.internalPointer().obj
-            if obj:
-                return QtGui.QBrush(color) if obj.options["plot"] else QtGui.QBrush(color_disabled)
-            else:
-                return index.internalPointer().data(index.column())
-
-        elif role == Qt.DecorationRole:
-            icon = index.internalPointer().icon
-            if icon:
-                return icon
-            else:
-                return QtGui.QPixmap()
-        else:
-            return None
-
-    def setData(self, index, data, role=None):
-        if index.isValid():
-            obj = index.internalPointer().obj
-
-            if obj:
-                old_name = deepcopy(obj.options['name'])
-                new_name = str(data)
-                if old_name != new_name and new_name != '':
-                    # rename the object
-                    obj.options["name"] = deepcopy(data)
-
-                    # update the SHELL auto-completer model data
-                    try:
-                        self.app.myKeywords.remove(old_name)
-                        self.app.myKeywords.append(new_name)
-                        self.app.shell._edit.set_model_data(self.app.myKeywords)
-                        self.app.ui.code_editor.set_model_data(self.app.myKeywords)
-                    except Exception as e:
-                        log.debug(
-                            "setData() --> Could not remove the old object name from auto-completer model list. %s" %
-                            str(e))
-
-                    # obj.build_ui()
-                    self.app.inform.emit(_("Object renamed from <b>{old}</b> to <b>{new}</b>").format(old=old_name,
-                                                                                                      new=new_name))
-
-        return True
-
-    def supportedDropActions(self):
-        return Qt.MoveAction
-
-    def flags(self, index):
-        default_flags = QtCore.QAbstractItemModel.flags(self, index)
-
-        if not index.isValid():
-            return Qt.ItemIsEnabled | default_flags
-
-        # Prevent groups from selection
-        if not index.internalPointer().obj:
-            return Qt.ItemIsEnabled
-        else:
-            return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable | \
-                   Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
-
-        # return QtWidgets.QAbstractItemModel.flags(self, index)
-
-    def append(self, obj, active=False):
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.append()")
-
-        name = obj.options["name"]
-
-        # Check promises and clear if exists
-        if name in self.promises:
-            self.promises.remove(name)
-            # FlatCAMApp.App.log.debug("Promised object %s became available." % name)
-            # FlatCAMApp.App.log.debug("%d promised objects remaining." % len(self.promises))
-
-        # Prevent same name
-        while name in self.get_names():
-            # ## Create a new name
-            # Ends with number?
-            FlatCAMApp.App.log.debug("new_object(): Object name (%s) exists, changing." % name)
-            match = re.search(r'(.*[^\d])?(\d+)$', name)
-            if match:  # Yes: Increment the number!
-                base = match.group(1) or ''
-                num = int(match.group(2))
-                name = base + str(num + 1)
-            else:  # No: add a number!
-                name += "_1"
-        obj.options["name"] = name
-
-        obj.set_ui(obj.ui_type())
-
-        # Required before appending (Qt MVC)
-        group = self.group_items[obj.kind]
-        group_index = self.index(group.row(), 0, QtCore.QModelIndex())
-        self.beginInsertRows(group_index, group.child_count(), group.child_count())
-
-        # Append new item
-        obj.item = TreeItem(None, self.icons[obj.kind], obj, group)
-
-        # Required after appending (Qt MVC)
-        self.endInsertRows()
-
-        # Expand group
-        if group.child_count() is 1:
-            self.view.setExpanded(group_index, True)
-
-        self.app.should_we_save = True
-
-        self.app.object_status_changed.emit(obj, 'append')
-
-        # decide if to show or hide the Notebook side of the screen
-        if self.app.defaults["global_project_autohide"] is True:
-            # always open the notebook on object added to collection
-            self.app.ui.splitter.setSizes([1, 1])
-
-    def get_names(self):
-        """
-        Gets a list of the names of all objects in the collection.
-
-        :return: List of names.
-        :rtype: list
-        """
-
-        # FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + " --> OC.get_names()")
-        return [x.options['name'] for x in self.get_list()]
-
-    def get_bounds(self):
-        """
-        Finds coordinates bounding all objects in the collection.
-
-        :return: [xmin, ymin, xmax, ymax]
-        :rtype: list
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_bounds()")
-
-        # TODO: Move the operation out of here.
-
-        xmin = Inf
-        ymin = Inf
-        xmax = -Inf
-        ymax = -Inf
-
-        # for obj in self.object_list:
-        for obj in self.get_list():
-            try:
-                gxmin, gymin, gxmax, gymax = obj.bounds()
-                xmin = min([xmin, gxmin])
-                ymin = min([ymin, gymin])
-                xmax = max([xmax, gxmax])
-                ymax = max([ymax, gymax])
-            except Exception as e:
-                FlatCAMApp.App.log.warning("DEV WARNING: Tried to get bounds of empty geometry. %s" % str(e))
-
-        return [xmin, ymin, xmax, ymax]
-
-    def get_by_name(self, name, isCaseSensitive=None):
-        """
-        Fetches the FlatCAMObj with the given `name`.
-
-        :param name: The name of the object.
-        :type name: str
-        :param isCaseSensitive: whether searching of the object is done by name where the name is case sensitive
-        :return: The requested object or None if no such object.
-        :rtype: FlatCAMObj or None
-        """
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.get_by_name()")
-
-        if isCaseSensitive is None or isCaseSensitive is True:
-            for obj in self.get_list():
-                if obj.options['name'] == name:
-                    return obj
-        else:
-            for obj in self.get_list():
-                if obj.options['name'].lower() == name.lower():
-                    return obj
-        return None
-
-    def delete_active(self, select_project=True):
-        selections = self.view.selectedIndexes()
-        if len(selections) == 0:
-            return
-
-        active = selections[0].internalPointer()
-        group = active.parent_item
-
-        # send signal with the object that is deleted
-        # self.app.object_status_changed.emit(active.obj, 'delete')
-
-        # update the SHELL auto-completer model data
-        name = active.obj.options['name']
-        try:
-            self.app.myKeywords.remove(name)
-            self.app.shell._edit.set_model_data(self.app.myKeywords)
-            self.app.ui.code_editor.set_model_data(self.app.myKeywords)
-        except Exception as e:
-            log.debug(
-                "delete_active() --> Could not remove the old object name from auto-completer model list. %s" % str(e))
-
-        self.beginRemoveRows(self.index(group.row(), 0, QtCore.QModelIndex()), active.row(), active.row())
-
-        group.remove_child(active)
-
-        # after deletion of object store the current list of objects into the self.app.all_objects_list
-        self.app.all_objects_list = self.get_list()
-
-        self.endRemoveRows()
-
-        self.app.plotcanvas.redraw()
-
-        if select_project:
-            # always go to the Project Tab after object deletion as it may be done with a shortcut key
-            self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
-
-        self.app.should_we_save = True
-
-        # decide if to show or hide the Notebook side of the screen
-        if self.app.defaults["global_project_autohide"] is True:
-            # hide the notebook if there are no objects in the collection
-            if not self.get_list():
-                self.app.ui.splitter.setSizes([0, 1])
-
-    def delete_all(self):
-        FlatCAMApp.App.log.debug(str(inspect.stack()[1][3]) + "--> OC.delete_all()")
-
-        self.beginResetModel()
-
-        self.checked_indexes = []
-        for group in self.root_item.child_items:
-            group.remove_children()
-
-        self.endResetModel()
-
-        self.app.plotcanvas.redraw()
-
-        self.app.all_objects_list.clear()
-
-        self.app.geo_editor.clear()
-
-        self.app.exc_editor.clear()
-
-        self.app.dblsidedtool.reset_fields()
-
-        self.app.panelize_tool.reset_fields()
-
-        self.app.cutout_tool.reset_fields()
-
-        self.app.film_tool.reset_fields()
-
-    def get_active(self):
-        """
-        Returns the active object or None
-
-        :return: FlatCAMObj or None
-        """
-        selections = self.view.selectedIndexes()
-        if len(selections) == 0:
-            return None
-
-        return selections[0].internalPointer().obj
-
-    def get_selected(self):
-        """
-        Returns list of objects selected in the view.
-
-        :return: List of objects
-        """
-        return [sel.internalPointer().obj for sel in self.view.selectedIndexes()]
-
-    def get_non_selected(self):
-        """
-        Returns list of objects non-selected in the view.
-
-        :return: List of objects
-        """
-
-        obj_list = self.get_list()
-
-        for sel in self.get_selected():
-            obj_list.remove(sel)
-
-        return obj_list
-
-    def set_active(self, name):
-        """
-        Selects object by name from the project list. This triggers the
-        list_selection_changed event and call on_list_selection_changed.
-
-        :param name: Name of the FlatCAM Object
-        :return: None
-        """
-        try:
-            obj = self.get_by_name(name)
-            item = obj.item
-            group = self.group_items[obj.kind]
-
-            group_index = self.index(group.row(), 0, QtCore.QModelIndex())
-            item_index = self.index(item.row(), 0, group_index)
-
-            self.view.selectionModel().select(item_index, QtCore.QItemSelectionModel.Select)
-        except Exception as e:
-            log.error("[ERROR] Cause: %s" % str(e))
-            raise
-
-    def set_inactive(self, name):
-        """
-        Unselect object by name from the project list. This triggers the
-        list_selection_changed event and call on_list_selection_changed.
-
-        :param name: Name of the FlatCAM Object
-        :return: None
-        """
-        log.debug("ObjectCollection.set_inactive()")
-
-        obj = self.get_by_name(name)
-        item = obj.item
-        group = self.group_items[obj.kind]
-
-        group_index = self.index(group.row(), 0, QtCore.QModelIndex())
-        item_index = self.index(item.row(), 0, group_index)
-
-        self.view.selectionModel().select(item_index, QtCore.QItemSelectionModel.Deselect)
-
-    def set_all_inactive(self):
-        """
-        Unselect all objects from the project list. This triggers the
-        list_selection_changed event and call on_list_selection_changed.
-
-        :return: None
-        """
-        for name in self.get_names():
-            self.set_inactive(name)
-
-    def on_list_selection_change(self, current, previous):
-        # FlatCAMApp.App.log.debug("on_list_selection_change()")
-        # FlatCAMApp.App.log.debug("Current: %s, Previous %s" % (str(current), str(previous)))
-
-        try:
-            obj = current.indexes()[0].internalPointer().obj
-
-            if obj.kind == 'gerber':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='green', name=str(obj.options['name'])))
-            elif obj.kind == 'excellon':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='brown', name=str(obj.options['name'])))
-            elif obj.kind == 'cncjob':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='blue', name=str(obj.options['name'])))
-            elif obj.kind == 'geometry':
-                self.app.inform.emit(_('[selected]<span style="color:{color};">{name}</span> selected').format(
-                    color='red', name=str(obj.options['name'])))
-
-        except IndexError:
-            # FlatCAMApp.App.log.debug("on_list_selection_change(): Index Error (Nothing selected?)")
-            self.app.inform.emit('')
-            try:
-                self.app.ui.selected_scroll_area.takeWidget()
-            except Exception as e:
-                FlatCAMApp.App.log.debug("Nothing to remove. %s" % str(e))
-
-            self.app.setup_component_editor()
-            return
-
-        if obj:
-            obj.build_ui()
-
-    def on_item_activated(self, index):
-        """
-        Double-click or Enter on item.
-
-        :param index: Index of the item in the list.
-        :return: None
-        """
-        a_idx = index.internalPointer().obj
-        if a_idx is None:
-            return
-        else:
-            try:
-                a_idx.build_ui()
-            except Exception as e:
-                self.app.inform.emit(_("[ERROR] Cause of error: %s") % str(e))
-                raise
-
-    def get_list(self):
-        obj_list = []
-        for group in self.root_item.child_items:
-            for item in group.child_items:
-                obj_list.append(item.obj)
-
-        return obj_list
-
-    def update_view(self):
-        self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())

+ 108 - 2725
README.md

@@ -1,2740 +1,123 @@
-FlatCAM: 2D Computer-Aided PCB Manufacturing
-=================================================
-
-(c) 2014-2019 Juan Pablo Caram
+FlatCAM BETA (c) 2019 - by Marius Stanciu
+Based on FlatCAM: 
+2D Computer-Aided PCB Manufacturing by (c) 2014-2016 Juan Pablo Caram
+=====================================================================
 
 FlatCAM is a program for preparing CNC jobs for making PCBs on a CNC router.
 Among other things, it can take a Gerber file generated by your favorite PCB
 CAD program, and create G-Code for Isolation routing.
 
-=================================================
-
-10.08.2019
-
-- added new feature in NCC Tool: now another object can be used as reference for the area extent to be cleared of copper
-- fixed issue in the latest feature in NCC Tool: now it works also with reference objects made out of LineStrings (tool 'Path' in Geometry Editor)
-- translation files updated for the new strings (Google Translate)
-- RELEASE 8.93
-
-9.08.2019
-
-- added Exception handing for the case when the user is trying to save & overwrite a file already opened in another file
-- finished added 'Area' type of Paint in Paint Tool
-- fixed bug that created a choppy geometry for CNCJob when working in INCH
-- fixed bug that did not asked the user to save the preferences after importing a new set of preferences, after the user is trying to close the Preferences tab window
-
-7.08.2019
-
-- replaced setFixedWidth calls with setMinimumWidth
-- recoded the camlib.Geometry.isolation_geometry() function
-- started to work on Paint Area in Paint Tool
-
-6.08.2019
-
-- fixed bug that crashed the app after creating a new geometry, if a new object is loaded and the new geometry is deleted and then trying to select the just loaded new object
-- made some GUI elements in Edit -> Preferences to have a minimum width as opposed to the previous fixed one
-- fixed issue in the isolation function, if the isolation can't be done there will be generated no Geometry object 
-- some minor UI changes
-- strings added and translations updated
-
-5.08.2019
-
-- made sure that if using an negative Gerber isolation diameter, the resulting Geometry object will use a tool with positive diameter
-- fixed bug that when isolating a Gerber file made out of a single polygon, an RecursionException was issued together with inability to create tbe isolation
-- when applying a new language if there are any changes in the current project, the app will offer to save the project before the reboot
-
-3.08.2019
-
-- added project name to the window title
-- fulfilled request: When saving a CNC file, if the file name is changed in the OS window, the new name does appear in the “Selected” (in name) and “Project” tabs (in cnc_job)
-- solved bug such that the app is not crashing when some apertures in the Gerber file have no geometry. More than that, now the apertures that have geometry elements are bolded as opposed to the ones without geometry for which the text is unbolded
-- merged a pull request with language changes for Russian translate
-- updated the other translations
-
-31.07.2019
-
-- changed the order of the menu entries in the FIle -> Open ...
-- organized the list of recent files so the Project entries are to the top and separated from the other types of file
-- work on identification of changes in Preferences tab
-- added categories names for the recent files
-- added a detection if any values are changed in the Edit -> Preferences window and on close it will ask the user if he wants to save the changes or not
-- created a new menu entry in the File menu named Recent projects that will hold the recent projects and the previous "Recent files" will hold only the previous loaded files
-- updated all translations for the new strings
-- fixed bug recently introduced that when changing the units in the Edit -> Preferences it did not converted the values
-- fixed another bug that when selecting an Excellon object after disabling it it crashed the app
-- RELEASE 8.92
-
-30.07.2019
-
-- fixed bug that crashed the software when trying to edit a GUI value in Geometry selected tab without having a tool in the Tools Table
-- fixed bug that crashed the app when trying to add a tool without a tool diameter value
-- Spanish Google translation at 77%
-- changed the Disable plots menu entry in the context menu, into a Toggle Visibility menu entry
-- Spanish Google translation 100% but two strings (big ones) - needs review
-- added two more strings to translation strings (due of German language)
-- completed the Russian translation using the Google and Yandex translation engines (minus two big strings) - needs review
-
-28.07.2019
-
-- fixed issue with not using the current units in the tool tables after unit conversion
-- after unit conversion from Preferences, the default values are automatically saved by the app
-- in Basic mode, the tool type column is no longer hidden as it may create issues when using an painted geometry
-- some PEP8 clean-up in FlatCAMGui.py
-- fixed Panelize Tool to do panelization for multiple passes type of geometry that comes out of the isolation done with multiple passes
-
-20.07.2019
-
-- updated the CutOut tool so it will work on single PCB Gerbers or on PCB panel Gerbers
-- updated languages
-- 70% progress in Spanish Google translation
-
-19.07.2019
-
-- fixed bug in FlatCAMObj.FlatCAMGeometry.ui_disconnect(); the widgets signals were not disconnected from handlers when required therefore the signals were connected in an exponential way
-- some changes in the widgets used in the Selected tab for Geometry object
-- some PEP8 cleanup in FlatCAMObj.py
-- updated languages
-- 60% progress in Spanish Google translation
-
-17.07.2019
-
-- added some more strings to the translatable ones, especially the radio button labels
-- updated the .POT file and the available translations
-- 51% progress in Spanish Google translation
-- version date change
-
-16.07.2019
-
-- PEP8 correction in flatcamTools
-- merged the Brazilian-portuguese language from a pull request made by Carlos Stein
-- more PEP8 corrections
-
-15.07.2019
-
-- some PEP8 corrections
-
-13.07.2019
-
-- fixed a possible issue in Gerber Object class
-- added a new tool in Gerber Editor: Mark Area Tool. It will mark the polygons in a edited Gerber object with areas within a defined range, allowing to delete some of the not necessary  copper features
-- added new menu links in the Gerber Editor menu for Eraser Tool and Mark Area Tool
-- added key shortcuts for Eraser Tool (CTRL+E) and Mark Area Tool (ALT+A) and updated the shortcuts list
-
-9.07.2019
-
-- some changes in the app.on_togle_units() to make sure we don't try to convert empty parameters which may cause crashes on FlatCAM units change
-- updated setup_ubuntu.sh file
-- made sure to import certain libraries in some of the FlatCAM files and not to rely on chained imports
-
-8.07.2019
-
-- fixed bug that allowed empty tool in the tools generated in Geometry object
-- fixed bug in Tool Cutout that did not allow the transfer of used cutout tool diameter to the cutout geometry object
-
-5.07.2019
-
-- fixed bug in CutOut Tool
-- some other bug in CutOut tool fixed
-
-1.07.2019
-
-- Spanish translation at 36%
-
-28.06.2019
-
-- Spanish translation (Google Translate) at 21%
-
-27.06.2019
-
-- added new translation: Spanish. Finished 10%
-
-23.06.2019
-
-- fixes issues with units conversion when the tool diameters are a list of comma separated values (NCC Tool, SolderPaste Tool and Geometry Object)
-- fixed a "typo" kind of bug in SolderPaste Tool
-- RELEASE 8.919
-
-22.06.2019
-
-- some GUI layout optimizations in Edit -> Preferences
-- added the possibility for multiple tool diameters in the Edit -> Preferences -> Geometry -> Geometry General -> Tool dia separated by comma
-- fixed scaling for the multiple tool diameters in Edit -> Preferences -> Geometry -> Geometry General -> Tool dia, for NCC tools more than 2 and for Solderpaste nozzles more than 2
-- fixed bug in CNCJob where the CNC Tools table will show always only 2 decimals for Tool diameters regardless of the current measuring units
-- made the tools diameters decimals in case of INCH FlatCAM units to be 4 instead of 3
-- fixed bug in updating Grid values whenever toggling the FlatCAM units and the X, Y Grid values are linked, bugs which caused the Y value to be scaled incorrectly
-- set the decimals for Grid values to be set to 6 if the units of FlatCAM is INCH and to set to 4 if FlatCAM units are METRIC
-- updated translations
-- updated the Russian translation from 51% complete to 69% complete using the Yandex translation engine
-- fixed recently introduced bug in milling drills/slots functions
-- moved Substract Tool from Menu -> Edit -> Conversions to Menu -> Tool
-- fixed bug in Gerber isolation (Geometry expects now a value in string format and not float)
-- fixed bug in Paint tool: now it is possible to paint geometry generated by External Isolation (or Internal isolation)
-- fixed bug in editing a multigeo Geometry object if previously a tool was deleted
-- optimized the toggle of annotations; now there is no need to replot the entire CNCJob object too on toggling of the annotations
-- on toggling off the plot visibility the annotations are turned off too
-- updated translations; Russian translation at 76% (using Yandex translator engine - needs verification by a native speaker of Russian)
-
-20.06.2019
-
-- fixed Scale and Buffer Tool in Gerber Editor
-- fixed Editor Transform Tool in Gerber Editor
-- added a message in the status bar when copying coordinates to clipboard with SHIFT + LMB click combo
-- languages update
-
-19.06.2019
-
-- milling an Excellon file (holes and/or slots) will now transfer the chosen milling bit diameter to the resulting Geometry object
-
-17.06.2019
-
-- fixed bug where for Geometry objects after a successful object rename done in the Object collection view (Project tab), deselect the object and reselect it and then in the Selected tab the name is not the new one but the old one
-- for Geometry objects, adding a new tool to the Tools table after a successful rename will now store the new name in the tool data
-
-15.06.2019
-
-- fixed bug in Gerber parser that made the Gerber files generated by Altium Designer 18 not to be loaded
-- fixed bug in Gerber editor - on multiple edits on the same object, the aperture size and dims were continuously multiplied due of the file units not being updated
-- restored the FlatCAMObj.visible() to a non-threaded default
-
-11.06.2019
-
-- fixed the Edit -> Conversion -> Join ... functions (merge() functions)
-- updated translations
-- Russian translate by @camellan is not finished yet
-- some PEP8 cleanup in camlib.py
-- RELEASE 8.918
-
-9.06.2019
-
-- updated translations
-- fixed the the labels for shortcut keys for zoom in and zoom out both in the Menu links and in the Shortcut list
-- made sure the zoom functions use the global_zoom_ratio parameter from App.self.defaults dictionary.
-- some PEP8 cleanup
-
-8.06.2019
-
-- make sure that the annotation shapes are deleted on creation of a new project
-- added folder for the Russian translation
-- made sure that visibility for TextGroup is set only if index is not None in VisPyVisuals.TextGroup.visible() setter
-
-7.06.2019
-
-- fixed bug in ToolCutout where creating a cutout object geometry from another external isolation geometry failed
-- fixed bug in cncjob TclCommand where the gcode could not be correctly generated due of missing bounds params in obj.options dict
-- fixed a hardcoded tolerance in FlatCAMGeometry.generatecncjob() and in FlatCAMGeometry.mtool_gen_cncjob() to use the parameter from Preferences
-- updated translations
-
-5.06.2019
-
-- updated translations
-- some layout changes in Edit -> Preferences such that the German translation (longer words than English) to fit correctly
-- after editing an parameter the focus is lost so the user knows that something happened
-
-4.06.2019
-
-- PEP8 updates in FlatCAMExcEditor.py
-- added the Excellon Editor parameters to the Edit -> Preferences -> Excellon GUI
-- fixed a small bug in Excellon Editor
-- PEP8 cleanup in FlatCAMGui
-- finished adding the Excellon Editor parameters into the app logic and added a selection limit within Excellon Editor just like in the other editors
-
-3.06.2019
-
-- TclCommand Geocutout is now creating a new geometry object when working on a geometry, preserving also the origin object
-- added a new parameter in Edit -> Preferences -> CNCJob named Annotation Color; it controls the color of the font used for annotations
-- added a new parameter in Edit -> Preferences -> CNCJob named Annotation Size; it controls the size of the font used for annotations
-- made visibility change threaded in FlatCAMObj()
-
-2.06.2019
-
-- fixed issue with geometry name not being updated immediately after change while doing geocutout TclCommand
-- some changes to enable/disable project context menu entry handlers
-
-1.06.2019
-
-- fixed text annotation for CNC job so there are no overlapping numbers when 2 lines meet on the same point
-- fixed issue in CNC job plotting where some of the isolation polygons are painted incorrectly
-- fixed issue in CNCJob where the set circle steps is not used 
-
-31.05.2019
-
-- added the possibility to display text annotation for the CNC travel lines. The setting is both in Preferences and in the CNC object properties
-
-30.05.2019
-
-- editing a multi geometry will no longer pop-up a Tcl window
-- solved issue #292 where a new geometry renamed with many underscores failed to store the name in a saved project
-- the name for the saved projects are updated to the current time and not to the time of the app startup
-- some PEP8 changes related to comments starting with only one '#' symbol
-- more PEP8 cleanup
-- solved issue where after the opening of an object the file path is not saved for further open operations
-
-24.05.2019
-
-- added a toggle Grid button to the canvas context menu in the Grids submenu
-- added a toggle left panel button to the canvas context menu
-
-23.05.2019
-
-- fixed bug in Gerber editor FCDisk and FCSemiDisc that the resulting geometry was not stored into the '0' aperture where all the solids are stored
-- fixed minor issue in Gerber Editor where apertures were included in the saved object even if there was no geometric data for that aperture
-- some PEP8 cleanup in FlatCAMApp.py
-
-22.05.2019
-
-- Geo Editor - added a new editor tool, Eraser
-- some PEP8 cleanup of the Geo Editor
-- fixed some selection issues in the new tool Eraser in Geometry Editor
-- updated the translation files
-- RELEASE 8.917
-
-21.05.2019
-
-- added the file extension .ncd to the Excellon file extension list
-- solved parsing issue for Excellon files generated by older Eagle versions (v6.x)
-- Gerber Editor: finished a new tool: Eraser. It will erase certain parts of Gerber geometries having the shape of a selected shape.
-
-20.05.2019
-
-- more PEP8 changes in Gerber editor
-- Gerber Editor - started to work on a new editor tool: Eraser
-
-19.05.2019
-
-- fixed the Circle Steps parameter for both Gerber and Geometry objects not being applied and instead the app internal defaults were used.
-- fixed the Tcl command Geocutout issue that gave an error when using the 4 or 8 value for gaps parameter
-- made wider the '#' column for Apertures Table for Gerber Object and for Gerber Editor; in this way numbers with 3 digits can be seen
-- PEP8 corrections in FlatCAMGrbEditor.py
-- added a selection limit parameter for Geometry Editor
-- added entries in Edit -> Preferences for the new parameter Selection limit for both the Gerber and Geometry Editors.
-- set the buttons in the lower part of the Preferences Window to have a preferred minimum width instead of fixed width
-- updated the translation files
-
-18.05.2019
-
-- added a new toggle option in Edit -> Preferences -> General Tab -> App Preferences -> "Open" Behavior. It controls which path is used when opening a new file. If checked the last saved path is used when saving files and the last opened path is used when opening files. If unchecked then the path for the last action (either open or save) is used.
-- fixed App.convert_any2gerber to work with the new Gerber apertures data structure
-- fixed Tool Sub to work with the new Gerber apertures data structure
-- fixed Tool PDF to work with the new Gerber apertures data structure
-
-17.05.2019
-
-- remade the Tool Cutout to work on panels
-- remade the Tool Cutout such that on multiple applications on the same object it will yield the same result
-- fixed an issue in the remade Cutout Tool where when applied on a single Gerber object, the Freeform Cutout produced no cutout Geometry object
-- remade the Properties Tool such that it works with the new Gerber data structure in the obj.apertures. Also changed the view for the Gerber object in Properties
-- fixed issue with false warning that the Gerber object has no geometry after an empty Gerber was edited and added geometry elements
-
-16.05.2019
-
-- Gerber Export: made sure that if some of the coordinates in a Gerber object geometry are repeating then the resulting Gerber code include only one copy
-- added a new parameter/feature: now the spindle can work in clockwise mode (CW) or counter clockwise mode (CCW)
-
-15.05.2019
-
-- rewrited the Gerber Parser in camlib - success
-- moved the self.apertures[aperture]['geometry'] processing for clear_geometry (geometry made with Gerber LPC command) in Gerber Editor
-- Gerber Editor: fixed the Poligonize Tool to work with new geometric structure and took care of a special case
-- Gerber Export is fixed to work with the new Gerber object data structure and it now works also for Gerber objects edited in Gerber Editor
-- Gerber Editor: fixed units conversion for obj.apertures keys that require it
-- camlib Gerber parser - made sure that we don't loose goemetry in regions
-- Gerber Editor - made sure that for some tools the added geometry is clean (the coordinates are non repeating)
-- covered some possible issues in Gerber Export
-
-12.05.2019
-
-- some modifications to ToolCutout
-
-11.05.2019
-
-- fixed issue in camlib.CNCjob.generate_from_excellon_by_tool() in the drill path optimization algorithm selection when selecting the MH algorithm. The new API's for Google OR-tools required some changes and also the time parameter can be now just an integer therefore I modified the GUI
-- made the Feedrate Rapids parameter to depend on the type of postprocessor choosed. It will be showed only for a postprocessor which the name contain 'marlin' and for any postprocessor's that have 'custom' in the name
-- fixed the camlib.Gerber functions of mirror, scale, offset, skew and rotate to work with the new data structure for apertures geometry
-- fixed Gerber Editor selection to work with the new Gerber data structure in self.apertures
-- fixed Gerber Editor FCPad class to work with the new Gerber data structure in self.apertures
-- fixed camlib.Gerber issues related to what happen after parsing rectangular apertures 
-- wip in camblib.Gerber
-- completely converted the Gerber editor to the new data structure
-- Gerber Editor: added a threshold limit for how many elements a move selection can have. If above the threshold only a bounding box Poly will be painted on canvas as utility geometry.
-
-10.05.2019
-
-- Gerber Editor - working in conversion to the new data format
-- made sure that only units toggle done in Edit -> Preferences will toggle the data in Preferences. The menu entry Edit -> Toggle Units and the shortcut key 'Q' will change only the display units in the app
-- optimized Transform tool
-- RELEASE 8.916
-
-9.05.2019
-
-- reworked the Gerber parser
-
-8.05.2019
-
-- added zoom fit for Set Origin command
-- added move action for solid_geometry stored in the gerber_obj.apertures
-- fixed camlib.Gerber skew, rotate, offset, mirror functions to work for geometry stored in the Gerber apertures
-- fixed Gerber Editor follow_geometry reconstruction
-- Geometry Editor: made the tool to be able to continuously move until the tool is exited either by ESC key or by right mouse button click
-- Geometry Editor Move Tool: if no shape is selected when triggering this tool, now it is possible to make the selection inside the tool
-- Gerber editor Move Tool: fixed a bug that repeated the plotting function unnecessarily 
-- Gerber editor Move Tool: if no shape is selected the tool will exit
-
-7.05.2019
-
-- remade the Tool Panelize GUI
-- work in Gerber Export: finished the header export
-- fixed the Gerber Object and Gerber Editor Apertures Table to not show extra rows when there are aperture macros in the object
-- work in Gerber Export: finished the body export but have some errors with clear geometry (LPC)
-- Gerber Export - finished
-
-6.05.2019
-
-- made units change from shortcut key 'Q' not to affect the preferences
-- made units change from Edit -> Toggle Units not to affect the preferences
-- remade the way the aperture marks are plotted in Gerber Object
-- fixed some bugs related to moving an Gerber object with the aperture table in view
-- added a new parameter in the Edit -> Preferences -> App Preferences named Geo Tolerance. This parameter control the level of geometric detail throughout FlatCAM. It directly influence the effect of Circle Steps parameter.
-- solved a bug in Excellon Editor that caused app crash when trying to edit a tool in Tool Table due of missing a tool offset
-- updated the ToolPanelize tool so the Gerber panel of type FlatCAMGerber can be isolated like any other FlatCAMGerber object
-- updated the ToolPanelize tool so it can be edited
-- modified the default values for toolchangez and endz parameters so they are now safe in all cases
-
-5.05.2019
-
-- another fix for bug in clear geometry processing for Gerber apertures
-- added a protection for the case that the aperture table is part of a deleted object
-- in Script Editor added support for auto-add closing parenthesis, brace and bracket
-- in Script Editor added support for "CTRL + / " key combo to comment/uncomment line
-
-4.05.2019
-
-- fixed bug in camlib.parse_lines() in the clear_geometry processing section for self.apertures
-- fixed bug in parsing Gerber regions (a point was added unnecessary)
-- renamed the menu entry Edit -> Copy as Geo to Convert Any to Geo and moved it in the Edit -> Conversion
-- created a new function named Convert Any to Gerber and installed it in Edit -> Conversion. It's doing what the name say: it will convert an Geometry or Excellon FlatCAM object to a Gerber object.
-
-01.05.2019
-
-- the project items color is now controlled from Foreground Role in ObjectCollection.data()
-- made again plot functions threaded but moved the dataChanged signal (update_view() ) to the main thread by using an already existing signal (plots_updated signal) to avoid the errors with register QVector
-- Enable/Disable Object toggle key ("Space" key) will trigger also the datChanged signal for the Project MVC
-- added a new setting for the color of the Project items, the color when they are disabled.
-- fixed a crash when triggering 'Jump To' menu action (shortcut key 'J' worked ok)
-- made some mods to what can be translated as some of the translations interfered with the correct functioning of FlatCAM
-- updated the translations
-- fixed bugs in Excellon Editor
-- Excellon Editor:  made Add Pad tool to work until right click
-- Excellon Editor: fixed mouse right click was always doing popup context menu
-- GUIElements.FCEntry2(): added a try-except clause
-- made sure that the Tools Tab is cleared on Editors exit
-- Geometry Editor: restored the old behavior: a tool is active until it is voluntarily exited: either by using the 'ESC' key, or selecting the Select tool or new: right click on canvas
-- RELEASE 8.915
-
-30.04.2019
-
-- in ObjectCollection class, made sure that renaming an object in Project View does not result in an empty name. If new name is blank the rename is cancelled.
-- made ObjectCollection.TreeItem() inherit KeySensitiveListVIew and implicitly QTreeView (in the hope that the theme applied on app will be applied on the tree items, too (for MacOs new DarkUI theme)
-- renamed SilkScreen Tool to Substract Tool and move it's menu location in Edit -> Conversion
-- started to modify the Substract Tool to work on Geometry objects too
-- progress in the new Substract Tool for Geometry Objects
-- finished the new Substract Tool
-- added new setting for the color of the Project Tree items; it helps in providing contrast when using dark theme like the one in MacOS
-
-29.04.2019
-
-- solved bug in Gerber Editor: the '0' aperture (the region aperture) had no size which created errors. Made the size to be zero.
-- solved bug in editors: the canvas selection shape was not deleted on mouse release if the grid snap was OFF
-- solved bug in Excellon Editor: when selecting a drill hole on canvas the selected row in the Tools Table was not the correct one but the next highest row
-- finished the Silkscreen Tool but there are some limitations (some wires fragments from silkscreen are lost)
-- solved the issue in Silkscreen Tool with losing some fragments of wires from silkscreen
-
-26.04.2019
-
-- small changes in GUI; optimized contextual menu display
-- made sure that the Project Tab is disabled while one of the Editors is active and it is restored after returning to app
-- fixed some bugs recently introduced in Editors due of the changes done to the way mouse panning is detected 
-- cleaned up the context menu's when in Editors; made some structural changes
-- updated the code in camlib.CNCJob.generate_from_excellon_by_tools() to work with the new API from Google OR-Tools
-- all Gerber regions (G36 G37) are stored in the '0' aperture
-- fixed a bug that added geometry with clear polarity in the apertures where was not supposed to be
-
-25.04.2019
-
-- Geometry Editor: modified the intersection (if the selected shapes don't intersects preserve them) and substract functions (delete all shapes that were used in the process)
-- work in the ToolSub
-- for all objects, if in Selected the object name is changed to the same name, the rename is not done (because there is nothing changed)
-- fixed Edit -> Copy as Geom function handler to work for Excellon objects, too
-- made sure that the mouse pointer is restored to default on Editor exit
-- added a toggle button in Preferences to toggle on/off the display of the selection box on canvas when the user is clicking an object or selecting it by mouse dragging.
-
-24.04.2019
-
-- PDF import tool: working in making the PDF layer rendering multithreaded in itself (one layer rendered on each worker)
-- PDF import tool: solved a bug in parsing the rectangle subpath (an extra point was added to the subpath creating nonexisting geometry)
-- PDF import tool: finished layer rendering multithreading
-- New tool: Silkscreen Tool: I am trying to remove the overlapped geo with the soldermask layer from overlay layer; layed out the class and functions - not working yet
-
-23.04.2019
-
-- Gerber Editor: added two new tools: Add Disc and Add SemiDisc (porting of Circle and Arc from Geometry Editor)
-- Gerber Editor: made Add Pad repeat until user exits the Add Pad through either mouse right click, or ESC key or deselecting the Add Pad menu item
-- Gerber and Geometry Editors: fixed some issues with the Add Arc/Add Semidisc; in mode 132, the norm() function was not the one from numpy but from a FlatCAM Class. Also fixed some of the texts and made sure that when changing the mode, the current points are reset to prepare for the newly selected mode.
-- Fixed Measurement Tool to show the mouse coordinates on the status bar (it was broken at some point)
-- updated the translation files
-- added more custom mouse cursors in Geometry and Gerber Editors
-- RELEASE 8.914
-
-22.04.2019
-
-- added PDF file as type in the Recent File list and capability to load it from there
-- PDF's can be drag & dropped on the GUI to be loaded
-- PDF import tool: added support for save/restore Graphics stack. Only for scale and offset transformations and for the linewidth. This is the final fix for Microsoft PDF printer who saves in PDF format 1.7
-- PDF Import tool: added support for PDF files that embed multiple Gerber layers (top, bottom, outline, silkscreen etc). Each will be opened in it's own Gerber file. The requirement is that each one is drawn in a different color
-- PDF Import tool: fixed bugs when drag & dropping PDF files on canvas the files geometry previously opened was added to the new one. Also scaling issues. Solved.
-- PDF Import tool: added support for detection of circular geometry drawn with white color which means actually invisible color. When detected, FlatCAM will build an Excellon file out of those geoms.
-- PDF Import tool: fixed storing geometries in apertures with the right size (before they were all stored in aperture D10)
-
-21.04.2019
-
-- fixed the PDF import tool to work with files generated by the Microsoft PDF printer (chained subpaths)
-- in PDF import tool added support for paths filled and at the same time stroked ('B' and 'B*'commands)
-- added a shortcut key for PDF Import Tool (ALT+Q) and updated the Shortcut list (also with the 'T' and 'R' keys for Gerber Editor where they control the bend in Track and Region tool and the 'M' and 'D' keys for Add Arc tool in Geometry Editor)
-
-20.04.2019
-
-- finished adding the PDF import tool although it does not support all kinds of outputs from PDF printers. Microsoft PDF printer is not supported.
-
-19.04.2019
-
-- started to work on PDF import tool
-
-
-18.04.2019
-
-- Gerber Editor: added custom mouse cursors for each mode in Add Track Tool
-- Gerber Editor: Poligonize Tool will first fuse polygons that touch each other and at a second try will create a polygon. The polygon will be automatically moved to Aperture '0' (regions).
-- Gerber Editor: Region Tool will add regions only in '0' aperture
-- Gerber Editor: the bending mode will now survive until the tool is exited
-- Gerber Editor: solved some bugs related with deleting an aperture and updating the last_selected_aperture
-
-17.04.2019
-
-- Gerber Editor: added some messages to warn user if no selection exists when trying to do aperture deletion or aperture geometry deletion
-- fixed version check
-- added custom mouse cursors for some tools in Gerber Editor
-- Gerber Editor: added multiple modes to lay a Region: 45-degrees, reverse 45-degrees, 90-degrees, reverse 90-degrees and free-angle. Added also key shortcuts 'T' and 'R' to cycle forward, respectively in reverse through the modes.
-- Excellon Editor: fixed issue not remembering last tool after adding a new tool
-- added custom mouse cursors for Excellon and Geometry Editors in some of their tools
-
-16.04.2019
-
-- added ability to use ENTER key to finish tool adding in Editors, NCC Tool, Paint Tool and SolderPaste Tool.
-- Gerber Editor: started to add modes of laying a track
-- Gerber Editor: Add Track Tool: added 5 modes for laying a track: 45-degrees, reverse-45 degrees, 90-degrees, reverse 90-degrees and free angle. Key 'T' will cycle forward through the modes and key 'R' will cycle in reverse through the track laying modes.
-- Gerber Editor: Add Track Tool: first right click will finish the track. Second right click will exit the Track Tool and return to Select Tool.
-- Gerber Editor: added protections for the Pad Array and Pad Tool for the case when the aperture size is zero (the aperture where to store the regions)
-
-15.04.2019
-
-- working on a new tool to process automatically PcbWizard Excellon files which are generated in 2 files
-- finished ToolPcbWizard; it will autodetect the Excellon format, units from the INF file
-- Gerber Editor: reduced the delay to show UI when editing an empty Gerber object
-- update the order of event handlers connection in Editors to first connect new handlers then disconnect old handlers. It seems that if nothing is connected some VispY functions like canvas panning no longer works if there is at least once nothing connected to the 'mouse_move' event
-- Excellon Editor: update so always there is a tool selected even after the Excellon object was just edited; before it always required a click inside of the tool table, not you do it only if needed.
-- fixed the menu File -> Edit -> Edit/Close Editor entry to reflect the status of the app (Editor active or not)
-- added support in Excellon parser for autodetection of Excellon file format for the Excellon files generated by the following ECAD sw: DipTrace, Eagle, Altium, Sprint Layout
-- Gerber Editor: finished a new tool: Poligonize Tool (ALT+N in Editor). It will fuse a selection of tracks into a polygon. It will fill a selection of polygons if they are apart and it will make a single polygon if the selection is overlapped. All the newly created filled polygons will be stored in aperture '0' (if it does not exist it will be automatically created)
-- fixed a bug in Move command in context menu who crashed the app when triggered
-- Gerber Editor: when adding a new aperture it will be store as the last selected and it will be used for any tools that are triggered until a new aperture is selected.
-
-14.04.2019
-
-- Gerber Editor: Remade the processing of 'clear_geometry' (geometry generated by polygons made with Gerber LPC command) to work if more than one such polygon exists
-- Gerber Editor: a disabled/enabled sequence for the VisPy cursor on Gerber edit make the graphics better
-- Editors: activated an old function that was no longer active: each tool can have it's own set of shortcut keys, the Editor general shortcut keys that are letters are overridden
-- Gerber and Geometry editors, when using the Backspace keys for certain tools, they will backtrack one point but now the utility geometry is immediately updated
-- In Geometry Editor I fixed bug in Arc modes. Arc mode shortcut key is now key 'M' and arc direction change shortcut key is 'D'
-- moved the key handler out of the Measurement tool to flatcamGUI.FlatCAMGui.keyPressEvent()
-- Gerber Editor: started to add new function of poligonize which should make a filled polygon out of a shape
-- cleaned up Measuring Tool
-- solved bug in Gerber apertures size and dimensions values conversion when file units are different than app units
-
-13.04.2019
-
-- updating the German translation
-- Gerber Editor: added ability to change on the fly the aperture after one of the tools: Add Pad or Add Pad Array is activated
-- Gerber Editor: if a tool is cancelled via key shortcut ESCAPE, the selection is now deleted and any other action require a new selection
-- finished German translation (Google translated with some adjustments)
-- final fix for issue #277. Previous fix was applied only for one case out of three.
-- RELEASE 8.913
-
-12.04.2019
-
-- Gerber Editor: added support for Oblong type of aperture
-- fixed an issue with automatically filled in aperture code when the edited Gerber file has no apertures; established an default with value 10 (according to Gerber specifications)
-- fixed a bug in editing a blank Gerber object
-- added handlers for the Gerber Editor context menu
-- updated the translation template POT file and the EN PO/MO files
-- Gerber Editor: added toggle effect to the Transform Tool
-- Gerber Editor: added shortcut for Transform Tool and also toggle effect here, too
-- updated the shortcut list with the Gerber Editor shortcut keys
-- Gerber Editor: fixed error when adding an aperture with code value lower than the ones that already exists
-- when adding an aperture with code '0' (zero) it will automatically be set with size zero and type: 'REG' (from region); here we store all the regions from a Gerber file, the ones without a declared aperture
-- Gerber Editor: added support for Gerber polarity change commands (LPD, LPC)
-- moved the polarity change processing from FlatCAMGrbEditor() class to camlib.Gerber().parse_lines()
-- made optional the saving of an edited object. Now the user can cancel the changes to the object.
-- replaced the standard buttons in the QMessageBox's used in the app with custom ones that can have text translated
-- updated the POT translation file and the MO/PO files for English and Romanian language
-
-11.04.2019
-
-- changed the color of the marked apertures to the global_selection_color
-- Gerber Editor: added Transformation Tool and Rotation key shortcut
-- in all Editors, manually deactivating a button in the editor toolbar will automatically select the 'Select' button
-- fixed Excellon Editor selection: when a tool is selected in Tools Table, all the drills belonging to that tool are selected. When a drill is selected on canvas, the associated tool will be selected without automatically selecting all other drills with same tool
-- Gerber Editor: added Add Pad Array tool
-- Gerber Editor: in Add Pad Array tool, if the pad is not circular type, for circular array the pad will be rotated to match the array angle
-- Gerber Editor: fixed multiple selection with key modifier such that first click selects, second deselects
-
-10.04.2019
-
-- Gerber Editor: added Add Track and Add Region functions
-- Gerber Editor: fixed key shortcuts
-- fixed setting the Layout combobox in Preferences according to the current layout
-- created menu links and shortcut keys for adding a new empty Gerber objects; on update of the edited Gerber, if the source object was an empty one (new blank one) this source obj will be deleted
-- removed the old apertures editing from Gerber Obj selected tab
-- Gerber Editor: added Add Pad (circular or rectangular type only)
-- Gerber Editor: autoincrement aperture code when adding new apertures
-- Gerber Editor: automatically calculate the size of the rectangular aperture
-
-9.04.2019
-
-- Gerber Editor: added buffer and scale tools
-- Gerber Editor: working on aperture selection to show on Aperture Table
-- Gerber Editor: finished the selection on canvas; should be used as an template for the other Editors
-- Gerber Editor: finished the Copy, Aperture Add, Buffer, Scale, Move including the Utility geometry
-- Trying to fix bug in Measurement Tool: the mouse events don't disconnect
-- fixed above bug in Measurement Tool (but there is a TODO there)
-
-7.04.2019
-
-- default values for Jump To function is jumping to origin (0, 0)
-
-6.04.2019
-
-- fixed bug in Geometry Editor in buffer_int() function that created an Circular Reference Error when applying buffer interior on a geometry.
-- fixed issue with not possible to close the app after a project save.
-- preliminary Gerber Editor.on_aperture_delete() 
-- fixed 'circular reference' error when creating the new Gerber file in Gerber Editor
-- preliminary Gerber Editor.on_aperture_add()
-
-5.04.2019
-
-- Gerber Editor: made geometry transfer (which is slow) to Editor to be multithreaded
-- Gerber Editor: plotting process is showed in the status bar
-- increased the number of workers in FlatCAM and made the number of workers customizable from Preferences
-- WIP in Gerber Editor: geometry is no longer stored in a Rtree storage as it is not needed
-- changed the way delayed plot is working in Gerber Editor to use a Qtimer instead of python threading module
-- WIP in Gerber Editor
-- fixed bug in saving the maximized state
-- fixed bug in applying default language on first start
-~~- on activating 'V' key shortcut (zoom fit) the mouse cursor is now jumping to origin (0, 0)~~
-- fixed bug in saving toolbars state; the file was saved before setting the self.defaults['global_toolbar_view]
-
-4.04.2019
-
-- added support for Gerber format specification D (no zero suppression) - PCBWizard Gerber files support
-- added support for Excellon file with no info about tool diameters - PCB Wizard Excellon file support
-- modified the bogus diameters series for Excellon objects that do not have tool diameter info
-- made Excellon Editor aware of the fact that the Excellon object that is edited has fake (bogus) tool diameters and therefore it will not sort the tools based on diameter but based on tool number
-- fixed bug on Excellon Editor: when diameter is edited in Tools Table and the target diameter is already in the tool table, the drills from current tool are moved to the new tool (with new dia) - before it crashed
-- fixed offset after editing drill diameters in Excellon Editor.
-
-3.04.2019
-
-- fixed plotting in Gerber Editor
-- working on GUI in Gerber Editor
-- added a Gcode end_command: default is M02
-- modified the calling of the editor2object() slot function to fix an issue with updating geometry imported from SVG file, after edit
-- working on Gerber Editor - added the key shortcuts: wip
-- made saving of the project file non-blocking and also while saving the project file, if the user tries again to close the app while project file is being saved, the app will close only after saving is complete (the project file size is non zero)
-- fixed the camlib.Geometry.import_svg() and camlib.Gerber.bounds() to work when importing SVG files as Gerber
-
-31.03.2019
-
-- fixed issue #281 by making generation of a convex shape for the freeform cutout in Tool Cutout a choice rather than the default
-- fixed bug in Tool Cutout, now in manual cutout mode the gap size reflect the value set
-- changed Measuring Tool to use the mouse click release instead of mouse click press; also fixed a bug when using the ESC key.
-- fixed errors when the File -> New Project is initiated while an Editor is still active.
-- the File->Exit action handler is now self.final_save() 
-- wip in Gerber editor
-
-29.03.2019
-
-- update the TCL keyword list
-- fix on the Gerber parser that makes searching for '%%' char optional when doing regex search for mode, units or image polarity. This allow loading Gerber files generated by the ECAD software TCl4.4
-- fix error in plotting Excellon when toggling units
-- FlatCAM editors now are separated each in it's own file
-- fixed TextTool in Geometry Editor so it will open the notebook on activation and close it after finishing text adding
-- started to work on a Gerber Editor
-- added a fix in the Excellon parser by allowing a comma in the tool definitions between the diameter and the rest
-
-28.03.2019
-
-- About 45% progress in German translation
-- new feature: added ability to edit MultiGeo geometry (geometry from Paint Tool)
-- changed all the info messages that are of type warning, error or success so they have a space added after the keyword
-- changed the Romanian translation by adding more diacritics  
-- modified Gerber parser to copy the follow_geometry in the self.apertures
-- modified the Properties Tool to show the number of elements in the follow_geometry for each aperture
-- modified the copy functions to copy the follow_geometry and also the apertures if it's possible (only for Gerber objects)
-
-27.03.2019
-
-- added new feature: user can delete apertures in Advanced mode and then create a new FlatCAM Gerber object
-- progress in German translation. About 27% done.
-- fixed issue #278. Crash on name change in the Name field in the Selected Tab.
-
-26.03.2019
-
-- fixed an issue where the Geometry plot function protested that it does not have an parameter that is used by the CNCJob plot function. But both inherit from FaltCAMObj plot function which does not have that parameter so something may need to be changed. Until then I provided a phony keyboard parameter to make that function 'shut up'
-- fixed bug: after using Paint Tool shortcut keys are disabled
-- added CNCJob geometry for the holes created by the drills from Excellon objects
-
-25.03.2019
-
-- in the TCL completer if the word is already complete don't add it again but add a space
-- added all the TCL keywords in the completer keyword list
-- work in progress in German translation ~7%
-- after any autocomplete in TCL completer, a space is added
-- fixed an module import issue in NCC Tool
-- minor change (optimization) of the CNCJob UI
-- work in progress in German translation ~20%
-
-22.03.2019
-
-- fixed an error that created a situation that when saving a project with some of the CNCJob objects disabled, on project reload the CNCJob objects are no longer loaded
-- fixed the Gerber.merge() function. When some of the Gerber files have apertures with same id, create a new aperture id for the object that is fused because each aperture id may hold different geometries.
-- changed the autoname for saving Preferences, Project and PNG file
-
-20.03.2019
-
-- added autocomplete finish with ENTER key for the TCL Shell
-- made sure that the autocomplete function works only for FlatCAM Scripts
-- ESC key will trigger normal view if in full screen and the ESC key is pressed
-- added an icon and title text for the Toggle Units QMessageBox
-
-19.03.2019
-
-- added autocomplete for Code editor;
-- autocomplete in Code Editor is finished by hitting either TAB key or ENTER key
-- fixed the Gerber.merge() to work for the case when one of the merged Gerber objects solid_geometry type is Polygon and not a list
-
-18.03.2019
-
-- added ability to create new scripts and open scripts in FlatCAM Script Editor
-- the Code Editor tab name is changed according to the task; 'save' and 'open' buttons will have filters installed for the QOpenDialog fit to the task
-- added ability to run a FlatCAM Tcl script by double-clicking on the file
-- in Code Editor added shortcut combo key CTRL+SHIFT+V to function as a Special Paste that will replace the '\' char with '/' so the Windows paths will be pasted correctly for TCL Shell. Also doing SHIFT + LMB on the Paste in contextual menu is doing the same.
-
-17.03.2019
-
-- remade the layout in 2Sided Tool
-- work in progress for translation in Romanian - 91%
-- changed some of the app strings formatting to work better with Poedit translation software
-- fixed bug in Drillcncjob TclCommand
-- finished translation in Romanian
-- made the translations work when the app is frozen with CX_freeze
-- some formatting changes for the application strings
-- some changes on how the first layout is applied
-- minor bug fixes (typos from copy/paste from another part of the program)
-
-16.03.2019
-
-- fixed bug in Paint Tool - Single Poly: no geometry was generated
-- work in progress for translation in Romanian - 70%
-
-13.03.2019
-
-- made the layout combobox current item from Preferences -> General window to reflect the current layout
-- remade the POT translate file
-- work in progress in translation for Romanian language 44%
-- fix for showing tools by activating them from the Menu - final fix.
-
-11.03.2019
-
-- changed some icons here and there
-- fixed the Properties Project menu entry to work on the new way
-- in Properties tool now the Gerber apertures show the number of polygons in 'solid_geometry' instead of listing the objects
-- added a visual cue in Menu -> Edit about the entries to enter the Editor and to Save & Exit Editor. When one is enabled the other is disabled.
-- grouped all the UI files in flatcamGUI folder
-- grouped all parser files in flatcamParsers folder
-- another changes to the final_save() function
-- some strings were left outside the translation formatting - fixed
-- finished the replacement of '_' symbols throughout the app which conflicted with the _() function used by the i18n
-- reverted changes in Tools regarding the toggle effect - now they work as expected
-
-10.03.2019
-
-- added a fix in the Gerber parser when adding the geometry in the self.apertures dict for the case that the current aperture is None (Allegro does that)
-- finished support for internationalization by adding a set of .po/.mo files for the English language. Unfortunately the final action can be done only when Beta will be out of Beta (no more changes) or when I will decide to stop working on this app.
-- changed the tooltip for 'feedrate_rapids' parameter to point out that this parameter is useful only for the Marlin postprocessor
-- fix app crash for the case that there are no translation files
-- fixed some forgotten strings to be prepared for internationalization in ToolCalculators
-- fixed Tools menu no longer working due of changes
-- added some test translation for the ToolCalculators (in Romanian)
-- fixed bug in ToolCutOut where for each tool invocation the signals were reconnected
-- fixed some issues with ToolMeasurement due of above changes
-- updated the App.final_save() function
-- fixed an issue created by the fact that I used the '_' char inside the app to designate unused info and that conflicted with the _() function used by gettext
-- made impossible to try to reapply current language that it's already applied (un-necessary)
-
-8.03.2019
-
-- fixed issue when doing th CTRL (or SHIFT) + LMB, the focus is automatically moved to Project Tab
-- further work in internationalization, added a fallback to English language in case there is no translation for a string
-- fix for issue #262: when doing Edit-> Save & Close Editor on a Geometry that is not generated through first entering into an Editor, the geometry disappear
-- finished preparing for internationalization for the files: camlib and objectCollection
-- fixed tools shortcuts not working anymore due of the new toggle parameter for the .run().
-- finished preparing for internationalization for the files: FlatCAMEditor, FlatCAMGUI
-- finished preparing for internationalization for the files: FlatCAMObj, ObjectUI
-- sorted the languages in the Preferences combobox
-
-7.03.2019
-
-- made showing a shape when hovering over objects, optional, by adding a Preferences -> General parameter
-- starting to work in internationalization using gettext()
-- Finished adding _() in FlatCAM Tools
-- fixed Measuring Tool - after doing a measurement the Notebook was switching to Project Tab without letting the user see the results
-- more work on the translation engine; the app now restarts after a language is applied
-- added protection against using Travel Z parameter with negative or zero value (in Geometry).
-- made sure that when the Measuring Tools is active after last click the Status bar is no longer deleted
-
-6.03.2019
-
-- modified the way the FlatCAM Tools are run from toolbar as opposed of running them from other sources
-- some Gerber UI changes
-
-5.03.2019
-
-- modified the grbl-laser postprocessor lift_code()
-- treated an error created by Z_Cut parameter being None
-- changed the hover and selection box transparency
-
-4.03.2019
-
-- finished work on object hovering
-- fixed Excellon object move and all the other transformations
-- starting to work on Manual Cutout Tool
-- remade the CutOut Tool
-- finished Manual Cutout Tool by adding utility geometry to the cutting geometry
-- added CTRL + click behavior for adding manual bridge gaps in Cutout Tool
-- in Tool Cutout added shortcut key 'Escape' to cancel the current adding of bridge gaps
-
-3.03.2019
-
-- minor UI changes for Gerber UI
-- ~~after an object move, the apertures plotted shapes are deleted from canvas and the 'mark all' button is deselected~~
-- after move tool action or any other transform (rotate, skew, scale, mirror, offset), the plotted apertures are kept plotted.
-- changing units now will convert all the default values from one unit type to another
-- prettified the selection shape and the moving shape
-- initial work in object hovering shape
-
-02.03.2019
-
-- fixed offset, rotate, scale, skew for follow_geometry. Fixed the move tool also.
-- fixed offset, rotate, scale, skew for 'solid_geometry' inside the self.apertures.
-
-28.02.2019
-
-- added a change that when a double click is performed in a object on canvas resulting in a selection, if the notebook is hidden then it will be displayed
-- progress in ToolChange Custom commands replacement and rename
-
-27.02.2019
-
-- made the Custom ToolChange Text area in CNCJob Selected Tab depend on the status of the ToolChange Enable Checkbox even in the init stage.
-- added some parameters throughout camlib gcode generation functions; handled some possible errors (e.g like when attempting to use an empty Custom GCode Toolchange)
-- added toggle effect for the tools in the toolbar.
-- enhanced the toggle effect for the tools in the Tools Toolbar and also for Notebook Tab selection: if the current tool is activated it will toggle the notebook side but only if the installed widget is itself. If coming from another tool, the notebook will stay visible
-- upgraded the Tool Cutout when done from Gerber file to create a convex_hull around the Gerber file rather than trying to isolate it
-- added some protections for the FlatCAM Tools run after an object was loaded
-
-26.02.2019
-
-- added a function to read the parameters from ToolChange macro Text Box (I need to move it from CNCJob to Excellon and Geometry)
-- fixed the geometry adding to the self.apertures in the case when regions are done without declaring any aperture first (Allegro does that). Now, that geometry will be stored in the '0' aperture with type REG
-- work in progress to Toolchange_Custom code replacement -> finished the parse and replace function
-- fixed mouse selection on canvas, mouse drag, mouse click and mouse double click
-- fixed Gerber Aperture Table dimensions
-- added a Mark All button in the Gerber aperture table.
-- because adding shapes to the shapes collection (when doing Mark or Mark All) is time consuming I made the plot_aperture() threaded.
-- made the polygon fusing in modified Gerber creation, a list comprehension in an attempt for optimization
-- when right clicking the files in Project tab, the Save option for Excellon no longer export it but really save the original. 
-- in ToolChange Custom Code replacement, the Text Box in the CNCJob Selected tab will be active only if there is a 'toolchange_custom' in the name of the postprocessor file. This assume that it is, or was created having as template the Toolchange Custom postprocessor file.
-
-
-25.02.2019
-
-- fixed the Gerber object UI layout
-- added ability to mark individual apertures in Gerber file using the Gerber Aperture Table
-- more modifications for the Gerber UI layout; made 'follow' an advanced Gerber option
-- added in Preferences a new Category: Gerber Advanced Options. For now it controls the display of Gerber Aperture Table and the "follow" attribute4
-- fixed FlatCAMGerber.merge() to merge the self.apertures[ap]['solid_geometry'] too
-- started to work on a new feature that allow adding a ToolChange GCode macro - GUI added both in CNCJob Selected tab and in CNCJob Preferences
-- added a limited 'sort-of' Gerber Editor: it allows buffering and scaling of apertures
-
-24.02.2019
-
-- fixed a small bug in the Tool Solder Paste: the App don't take into consideration pads already filled with solder paste.
-- prettified the defaults files and the recent file. Now they are ordered and human readable
-- added a Toggle Code Editor Menu and key shortcut
-- added the ability to open FlatConfig configuration files in Code Editor, Modify them and then save them.
-- added ability to double click the FlatConfig files and open them in the FlatCAM Code Editor (to be verified)
-- when saving a file from Code Editor and there is no object active then the OpenFileDialog filters are reset to FlatConfig files.
-- reverted a change in GCode that might affect Gerber polarity change in Gerber parser
-- ability to double click the FlatConfig files and open them in the FlatCAM Code Editor - fixed and verified
-- fixed the Set To Origin function when Escape was clicked
-- added all the Tools in a new ToolBar
-- fixed bug that after changing the layout all the toolbar actions are no longer working
-- fixed bug in Set Origin function
-- fixed a typo in Toolchange_Probe_MACH3 postprocessor
-
-23.02.2019
-
-- remade the SolderPaste geometry generation function in ToolSoderPaste to work in certain scenarios where the Gerber pads in the SolderPaste mask Gerber may be just pads outlines
-- updated the Properties Tool to include more information's, also details if a Geometry is of type MultiGeo or SingleGeo
-- remade the Preferences GUI to include the Advanced Options in a separate way so it is obvious which are displayed when App Level is Advanced.
-- added protection, not allowing the user to make a Paint job on a MultiGeo geometry (one that is converted in the Edit -> Conversion menu)) because it is not supported
-
-22.02.2019
-
-- added Repetier postprocessor file
-- removed "added ability to regenerate objects (it's actually deletion followed by recreation)" because of the way Python pass parameters to functions by reference instead of copy
-- added ability to toggle globally the display of ToolTips. Edit -> Preferences -> General -> Enable ToolTips checkbox.
-- added true fullscreen support (for Windows OS)
-- added the ability of context menu inside the GuiElements.FCCombobox() object.
-- remade the UI for ToolSolderPaste. The object comboboxes now have context menu's that allow object deletion. Also the last object created is set as current item in comboboxes.
-- some GUI elements changes
-
-21.02.2019
-
-- added protection against creating CNCJob from an empty Geometry object (with no geometry inside)
-- changed the shortcut key for YouTube channel from F2 to key F4
-- changed the way APP LEVEL is showed both in Edit -> Preferences -> General tab and in each Selected Tab. Changed the ToolTips content for this.
-- added the functions for GCode View and GCode Save in Tool SolderPaste
-- some work in the Gcode generation function in Tool SolderPaste
-- added protection against trying to create a CNCJob from a solder_paste dispenser geometry. This one is different than the default Geometry and can be handled only by SolderPaste Tool.
-- ToolSolderPaste tools (nozzles) now have each it's own settings
-- creating the camlib functions for the ToolSolderPaste gcode generation functions
-- finished work in ToolSolderPaste
-- fixed issue with not updating correctly the plot kind (all, cut, travel) when clicking in the CNC Tools Table plot buttons
-- made the GCode Editor for ToolSolderPaste clear the text before updating the Code Editor tab
-- all the Tabs in Plot Area are closed (except Plot Area itself) on New Project creation
-- added ability to regenerate objects (it's actually deletion followed by recreation)
-
-20.02.2019
-
-- finished added a Tool Table for Tool SolderPaste
-- working on multi tool solder paste dispensing
-- finished the Edit -> Preferences defaults section
-- finished the UI, created the postprocessor file template
-- finished the multi-tool solder paste dispensing: it will start using the biggest nozzle, fill the pads it can, and then go to the next smaller nozzle until there are no pads without solder.
-
-19.02.2019
-
-- added the ability to compress the FlatCAM project on save with LZMA compression. There is a setting in Edit -> Preferences -> Compression Level between 0 and 9. 9 level yields best compression at the price of RAM usage and time spent.
-- made FlatCAM able to load old type (uncompressed) FlatCAM projects
-- fixed issue with not loading old projects that do not have certain information's required by the new versions of FlatCAM
-- compacted a bit more the GUI for Gerber Object
-- removed the Open Gerber with 'follow' menu entry and also the open_gerber Tcl Command attribute 'follow'. This is no longer required because now the follow_geometry is stored by default in a Gerber object attribute gerber_obj.follow_geometry
-- added a new parameter for the Tcl CommandIsolate, named: 'follow'. When follow = 1 (True) the resulting geometry will follow the Gerber paths.
-- added a new setting in Edit -> Preferences -> General that allow to select the type of saving for the FlatCAM project: either compressed or uncompressed. Compression introduce an time overhead to the saving/restoring of a FlatCAM project.
-- started to work on Solder Paste Dispensing Tool
-- fixed a bug in rotate from shortcut function
-- finished generating the solder paste dispense geometry
-
-18.02.2019
-
-- added protections again wrong values for the Buffer and Paint Tool in Geometry Editor
-- the Paint Tool in Geometry Editor will load the default values from Tool Paint in Preferences
-- when the Tools in Geometry Editor are activated, the notebook with the Tool Tab will be unhidden. After execution the notebook will hide again for the Buffer Tool.
-- changed the font in Tool names
-- added in Geometry Editor a new Tool: Transformation Tool.
-- in Geometry Editor by selecting a shape with a selection shape, that object was added multiple times (one per each selection) to the selected list, which is not intended. Bug fixed.
-- finished adding Transform Tool in Geometry Editor - everything is working as intended
-- fixed a bug in Tool Transform that made the user to not be able to capture the click coordinates with SHIFT + LMB click combo
-- added the ability to choose an App QStyle out of the offered choices (different for each OS) to be applied at the next app start (Preferences -> General -> Gui Pref -> Style Combobox)
-- added support for FlatCAM usage with High DPI monitors (4k). It is applied on the next app startup after change in Preferences -> General -> Gui Settings -> HDPI Support Checkbox
-- made the app not remember the window size if the app is maximized and remember in QSettings if it was maximized. This way we can restore the maximized state but restore the windows size unmaximized
-- added a button to clear the GUI preferences in Preferences -> General -> Gui Settings -> Clear GUI Settings
-- added key shortcuts for the shape transformations within Geometry Editor: X, Y keys for Flip(mirror), SHIFT+X, SHIFT+Y combo keys for Skew and ALT+X, ALT+Y combo keys for Offset
-- adjusted the plotcanvas.zomm_fit() function so the objects are better fit into view (with a border around) 
-- modified the GUI in Objects Selected Tab to accommodate 2 different modes: basic and Advanced. In Basic mode, some of the functionality's are hidden from the user.
-- added Tool Transform preferences in Edit -> Preferences and used them through out the app
-- made the output of Panelization Tool a choice out of Gerber and Geometry type of objects. Useful for those who want to engrave multiple copies of the same design.
-
-17.02.2019
-
-- changed some status bar messages
-- New feature: added the capability to view the source code of the Gerber/Excellon file that was loaded into the app. The file is also stored as an object attribute for later use. The view option is in the project context menu and in Menu -> Options -> View Source
-- Serialized the source_file of the Objects so it is saved in the FlatCAM project and restored.
-- if there is a single tool in the tool list (Geometry , Excellon) and the user click the Generate GCode, use that tool even if it is not selected
-- fixed issue where after loading a project, if the default kind of CNCjob view is only 'cuts' the plot will revert to the 'all' type
-- in Editors, if the modifier key set in Preferences (CTRL or SHIFT key) is pressed at the end of one tool operation it will automatically continue to that action until the modifier is no longer pressed when Select tool will be automatically selected.
-- in Geometry Editor, on entry the notebook is automatically hidden and restored on Geometry Editor exit.
-- when pressing Escape in Geometry Editor it will automatically deselect any shape not only the currently selected tool.
-- when deselecting an object in Project menu the status bar selection message is deleted
-- added ability to save the Gerber file content that is stored in FlatCAM on Gerber file loading. It's useful to recover from saved FlatCAM projects when the source files are no longer available.
-- fixed an issue where the function handler that changed the layout had a parameter changed accidentally by an index value passed by the 'activate' signal to which was connected
-- fixed bug in paint function in Geometry Editor that didn't allow painting due of overlap value
-
-16.02.2019
-
-- added the 'Save' menu entry to the Project context menu, for CNCJob: it will export the GCode.
-- added messages in info bar when selecting objects in the Project View list
-- fixed DblSided Tool so it correctly creates the Alignment Drills Excellon file using the new structure
-- fixed DblSided Tool so it will not crash the app if the user tries to make a mirror using no coordinates
-- added some relevant status bar messages in DblSided Tool
-- fixed DblSided Tool to correctly use the Box object (until now it used as reference only Gerber object in spite of Excellon or Geometry objects being available)
-- fixed DblSided Tool crash when trying to create Alignment Drills object without a Tool diameter specified
-- fixed DblSided Tool issue when entering Tool diameter values with comma decimal separator instead of decimal dot separator
-- fixed Cutout Tool Freeform to generate cutouts with options: LR, TB. 2LR, 2TB which didn't worked previously
-- fixed Excellon parser to detect correctly the units and zeros for Excellon's generated by Eagle 9.3.0
-- modified the initial size of the canvas on startup
-- modified the build file (make_win.py) to solve the issue with suddenly not accepting the version as Beta
-- changed the initial layout to 'compact'
-- updated the install scripts to uninstall a previously installed FlatCAM Beta (that has the same GUID)
-
-15.02.2019
-
-- rearranged the File and Edit menu's and added some explanatory tooltips on certain menu items that could be seen as cryptic
-- added Excellon Export Options in Edit -> Preferences
-- started to work in using the Excellon Export parameters
-- remade the Excellon export function to work with parameters entered in Edit -> Preferences -> Excellon Export
-- added a new entry in the Project Context Menu named 'Save'. It will actually work for Geometry and it will do Export DXF and for Excellon and it will do Export Excellon
-- reworked the offer to save a project so it is done only if there are objects in the project but those objects are new and/or are modified since last project load (if an old project was loaded.)
-- updated the Excellon plot function so it can plot the Excellon's from old projects
-- removed the message boxes that popup on Excellon Export errors and replaced them with status bar messages
-- small change in tab width so the tabs looks good in Linux, too.
-
-14.02.2019
-
-- added total travel distance for CNCJob object created from Excellon Object in the CNCJob Selected tab
-- added 'FlatCAM ' prefix to any detached tab, for easy identification
-- remade the Grids context menu (right mouse button click on canvas). Now it has values linked to the units type (inch or mm). Added ability to add or delete grid values and they are persistent.
-- updated the function for the project context menu 'Generate CNC' menu entry (Action) to use the modernized function FlatCAMObj.FlatCAMGeometry.on_generatecnc_button_click()
-- when linked, the grid snap on Y will copy the value in grid snap on X in real time
-- in Gerber aperture table now the values are displayed in the current units set in FlatCAM
-- added shortcut key 'J' (jump to location) in Editors and added an icon to the dialog popup window
-- the notebook is automatically collapsed when there are no objects in the collection and it is showed when adding an object
-- added new options in Edit -> Preferences -> General -> App Preferences to control if the Notebook is showed at startup and if the notebook is closed when there are no objects in the collection and showed when the collection has objects.
-
-13.02.2019
-
-- added new parameter for Excellon Object in Preferences: Fast Retract. If the checkbox is checked then after reaching the drill depth, the drill bit will be raised out of the hole asap.
-- started to work on GUI forms simplification
-- changed the Preferences GUI for Geometry and Excellon Objects to make a difference between parameters that are changed often and those that are not.
-- changed the layout in the Selected Tab UI
-- started to add apertures table support
-- finished Gerber aperture table display
-- made the Gerber aperture table not visible as default and added a checkbox that can toggle the visibility
-- fixed issue with plotting in CNCJob; with Plot kind set to something else than 'all' when toggling Plot, it was defaulting to kind = 'all'
-- added (and commented) an experimental FlatCAMObj.FlatCAMGerber.plot_aperture()
-
-12.02.2019
-
-- whenever a FlatCAM tool is activated, if the notebook side is hidden it will be unhidden
-- reactivated the Voronoi classes
-- added a new parameter named Offset in the Excellon tool table - work in progress
-- finished work on Offset parameter in Excellon Object (Excellon Editor, camlib, FlatCAMObj updated to take this param in consideration)
-- fixed a bug where in Excellon editor when editing a file, a tool was automatically added. That is supposed to happen only for empty newly created Excellon Objects.
-- starting to work on storing the solid_geometry for each tool in part in Excellon Object
-- stored solid_geometry of Excellon object in the self.tools dictionary
-- finished the solid_geometry restore after edit in Excellon Editor
-- finished plotting selection for each tool in the Excellon Tool Table
-- fixed the camlib.Excellon.bounds() function for the new type of Excellon geometry therefore fixed the canvas selection, too
-
-
-10.02.2019
-
-- the SELECTED type of messages are no longer printed to shell from 2 reasons: first, too much spam and second, issue with displaying html
-- on set_zero function and creation of new geometry or new excellon there is no longer a zoom fit 
-- repurposed shortcut key 'Delete' to delete tools in tooltable when the mouse is over the Seleted tab (with Geometry inside) or in Tools tab (when NCC Tool or Paint Tool is inside). Or in Excellon Editor when mouse is hovering the Selected tab selecting a tool, 'Delete' key will delete that tool, if on canvas 'Delete' key will delete a selected shape (drill). In rest, will delete selected objects.
-- adjusted the postprocessor files so the Spindle Off command (M5) is done before the move to Toolchange Z
-- adjusted the Toolchange Manual postprocessor file to have more descriptive messages on the toolchange event
-- added a strong focus to the object_name entry in the Selected tab
-- the keypad keyPressed are now detected correctly
-- added a pause and message/warning to do a rough zero for the Z axis, in case of Toolchange_Probe_MACH3 postprocessor file
-- changes in Toolchange_Probe_MACH3 postprocessor file
-
-9.02.2019
-
-- added a protection for when saving a file first time, it require a saved path and if none then it use the current working directory
-- added into Preferences the Calculator Tools
-- made the Preferences window scrollable on the horizontal side (it was only vertically scrollable before)
-- fixed an error in Excellon Editor -> add drill array that could appear by starting the function to add a drill array by shortcut before any mouse move is registered while in Editor
-- changed the messages from status bar on new object creation/selection
-- in Geometry Editor fixed the handler for the Rotate shortcut key ('R')
-
-8.02.2019
-
-- when shortcut keys 1, 2, 3 (tab selection) are activated, if the splitter left side (the notebook) is hidden it will be made visible
-- changed the menu entry Toggle Grid name to Toggle Grid Snap
-- fixed errors in Toggle Axis
-- fixed error with shortcut key triggering twice the keyPressEvent when in the Project List View
-- moved all shortcut keys handlers from Editors to the keyPressEvent() handler from FLatCAMGUI
-- in Excellon Editor added a protection for Tool_dia field in case numbers using comma as decimal separator are used. Also added a QDoubleValidator forcing a number with max 4 decimals and from 0.0000 to 9.9999
-- in Excellon Editor added a shortcut key 'T' that popup a window allowing to enter a new Tool with the set diameter
-- in App added a shortcut key 'T' that popup a windows allowing to enter a new Tool with set diameter only when the Selected tab is on focus and only if a Geometry object is selected
-- changed the shortcut key for Transform Tool from 'T' to 'ALT+T'
-- fixed bug in Geometry Selected tab that generated error when used tool offset was less than half of either total length or half of total width. Now the app signal the issue with a status bar message
-- added Double Validator for the Offset value so only float numbers can be entered.
-- in App added a shortcut key 'T' that popup a windows allowing to enter a new Tool with set diameter only when the Tool tab is on focus and only if a NCC Tool or Paint Area Tool object is installed in the Tool Tab
-- if trying to add a tool using shortcut key 'T' with value zero the app will react with a message telling to use a non-zero value.
-
-7.02.2019
-
-- in Paint Tool, when painting single polygon, when clicking on canvas for the polygon there is no longer a selection of the entire object
-- commented some debug messages
-- imported speedups for shapely
-- added a disable menu entry in the canvas contextual menu
-- small changes in Tools layout
-- added some new icons in the help menu and reorganized this menu
-- added a new function and the shortcut 'leftquote' (left of Key 1) for toggle of the notebook section
-- changed the Shortcut list shortcut key to F3
-- moved some graphical classes out of Tool Shell to GUIElements.py where they belong
-- when selecting an object on canvas by single click, it's name is displayed in status bar. When nothing is selected a blank message (nothing) it's displayed
-- in Move Tool I've added the type of object that was moved in the status bar message
-- color coded the status bar bullet to blue for selection
-- the name of the selected objects are displayed in the status bar color coded: green for Gerber objects, Brown for Excellon, Red for Geometry and Blue for CNCJobs.
+=====================================================================
 
-6.02.2019
+-------------------------- Installation instructions ----------------
 
-- fixed the units calculators crash FlatCAM when using comma as decimal separator
-- done a regression on Tool Tab default text. It somehow delete Tools in certain scenarios so I got rid of it
-- fixed bug in multigeometry geometry not having the bounds in self.options and crashing the GCode generation
-- fixed bug that crashed whole application in case that the GCode editor is activated on a Tool gcode that is defective. 
-- fixed bug in Excellon Slots milling: a value of a dict key was a string instead to be an int. A cast to integer solved it.
-- fixed the name self-insert in save dialog file for GCode; added protection in case the save path is None
-- fixed FlatCAM crash when trying to make drills GCode out of a file that have only slots.
-- changed the messages for Units Conversion
-- all key shortcuts work across the entire application; moved all the shortcuts definitions in FlatCAMGUI.keyPressEvent()
-- renamed the theme to layout because it is really a layout change
-- added plot kind for CNC Job in the App Preferences
-- combined the geocutout and cutout_any TCL commands - work in progress
-- added a new function (and shortcut key Escape) that when triggered it deselects all selected objects and delete the selection box(es) 
-- fixed bug in Excellon Gcode generation that made the toolchange X,Y always none regardless of the value in Preferences
-- fixed the Tcl Command Geocutout to work with Gerber objects too (besides Geometry objects)
+Works with Python version 3.5 or greater and PyQt5.
+More on the YouTube channel: https://www.youtube.com/playlist?list=PLVvP2SYRpx-AQgNlfoxw93tXUXon7G94_
 
-5.02.3019
+You can contact me on my email address found in the app in:
+Menu -> Help -> About FlatCAM -> Programmers -> Marius Stanciu
 
-- added a text in the Selected Tab which is showed whenever the Selected Tab is selected but without having an object selected to display it's properties
-- added an initial text in the Tools tab
-- added possibility to use the shortcut key for shortcut list in the Notebook tabs
-- added a way to set the Probe depth if Toolchange_Probe postprocessors are selected
-- finished the postprocessor file for MACH3 tool probing on toolchange event
-- added a new parameter to set the feedrate of the probing in case the used postprocessor does probing (has toolchange_probe in it's name)
-- fixed bug in Marlin postprocessor for the Excellon files; the header and toolchange event always used the parenthesis witch is not compatible with GCode for Marlin
-- fixed a issue with a move to Z_move before any toolchange
+- Make sure that your OS is up-to-date
+- Download sources from: https://bitbucket.org/jpcgt/flatcam/downloads/
+- Unzip them on a HDD location that your user has permissions for.
 
-4.02.2019
+1.Windows
 
-- modified the Toolchange_Probe_general postprocessor file to remove any Z moves before the actual toolchange event
-- created a prototype postprocessor file for usage with tool probing in MACH3
-- added the default values for Tool Film and Tool Panelize to the Edit -> Preferences
-- added a new parameter in the Tool Film which control the thickness of the stroke width in the resulting SVG. It's a scale parameter.
-- whatever was the visibility of the corresponding toolbar when we enter in the Editor, it will be set after exit from the Editor (either Geometry Editor or Excellon Editor).
-- added ability to be detached for the tabs in the Notebook section (Project, Selected and Tool)
-- added ability for all detachable tabs to be restored to the same position from where they were detached.
-- changed the shortcut keys for Zoom In, Zoom Out and Zoom Fit from 1, 2, 3 to '-', '=' respectively 'V'. Added new shortcut keys '1', '2', '3' for Select Project Tab, Select Selected Tab and Select Tool Tab.
-- formatted the Shortcut List Tab into a HTML table
+- download the provided installer (for your OS flavor 64bit or 32bit) from:
+https://bitbucket.org/jpcgt/flatcam/downloads/
+- execute the installer and install the program. It is recommended to install as a Local User.
 
-3.3.2019
+or from sources:
+- download the sources from the same location
+- unzip them on a safe location on your HDD that your user has permissions for
+- install WinPython e.g WinPython 3.8 downloaded from here: https://sourceforge.net/projects/winpython/files/WinPython_3.8/
+Use one of the versions (64bit or 32it) that are compatible with your OS. 
+To save space use one of the versions that have the smaller size (they offer 2 versions: one with size of few hundred MB and one smaller with size of few tens of MB)
 
-- updated the new shortcut list with the shortcuts added lately
-- now the special messages in the Shell are color coded according to the level. Before they all were RED. Now the WARNINGS are yellow, ERRORS are red and SUCCESS is a dark green. Also the level is in CAPS LOCK to make them more obvious
-- some more changes to GUI interface (solved issues)
-- added some status bar messages in the Geometry Editor to guide the user when using the Geometry Tools
-- now the '`' shortcut key that shows the 'shortcut key list' in Editors points to the same window which is created in a tab no longer as a pop-up window. This tab can be detached if needed.
-- added a remove_tools() function before install_tools() in the init_tools() that is called when creating a new project. Should solve the issue with having double menu entry's in the TOOLS menu
-- fixed remove_tools() so the Tcl Shell action is readded to the Tools menu and reconnected to it's slot function
-- added an automatic name on each save operation based on the object name and/or the current date
-- added more information's for the statistics
-
-2.2.2019
-
-- code cleanup in Tools
-- some GUI structure optimization's
-- added protection against entering float numbers with comma separator instead of decimal dot separator in key points of FlatCAM (not everywhere)
-- added a choice of plotting the kind of geometry for the CNC plot (all, travel and cut kind of geometries) in CNCJob Selected Tab
-- added a new postprocessor file named: 'probe_from_zmove' which allow probing to be done from z_move position on toolchange event 
-- fixed the snap magnet button in Geometry Editor, restored the checkable property to True
-- some more changes in the Editors GUI in deactivate() function
-- a fix for saving as empty an edited new and empty Excellon Object
-
-1.02.2019
-
-- fixed postprocessor files so now the bounds values are right aligned (assuming max string length of 9 chars which means 4 digits and 4 decimals)
-- corrected small type in list_sys Tcl command; added a protection of the Plot Area Tab after a successful edit.
-- remade the way FlatCAM saves the GUI position data from a file (previously) to use PyQt QSettings
-- added a 'theme' combo selection in Edit -> Preferences. Two themes are available: standard and compact.
-- some code cleanup
-- fixed a source of possible errors in DetachableTab Widget.
-- fixed gcode conversion/scale (on units change) when multiple values are found on each line
-- replaced the pop-up window for the shortcut list with a new detachable tab
-- removed the pop-up messages from the rotate, skew, flip commands
-
-31.01.2019
-
-- added a parameter ('Fast plunge' in Edit -> Preferences -> Geometry Options and Excellon Options) to control if the fast move to Z_move is done or not
-- added new function to toggle fullscreen status in Menu -> View -> Toggle Full Screen. Shortcut key: Alt+F10
-- added key shortcuts for Enable Plots, Disable Plots and Disable other plots functions (Alt+1, Alt+2, Alt+3)
-- hidden the snap magnet entry and snap magnet toggle from the main view; they are now active only in Editor Mode
-- updated the camlib.CNCJob.scale() function so now the GCode is scaled also (quite a HACK :( it will need to be replaced at some point)). Units change work now on the GCODE also.
-- added the bounds coordinates to the GCODE header
-- FlatCAM saves now to a file in self.data_path the toolbar positions and the position of TCL Shell
-- Plot Area Tab view can now be toggled, added entry in View Menu and shortcut key CTRL+F10
-- All the tabs in the GUI right side are (Plot Are, Preferences etc) are now detachable to a separate windows which when closed it returns in the previous location in the toolbar. Those detached tabs can be also reattached by drag and drop.
-
-30.01.2019
-
-- added a space before Y coordinate in end_code() function in some of the postprocessor files
-- added in Calculators Tool an Electroplating Calculator.
-- remade the App Menu for Editors: now they will be showed only when the respective Editor is active and hidden when the Editor is closed.
-- added a traceback report in the TCL Shell for the errors that don't allow creation of an object; useful to trace exceptions/errors
-- in case that the Toolchange X,Y parameter in Selected (or in Preferences) are deleted then the app will still do the job using the current coordinates for toolchange
-- fixed an issue in camlib.CNCJob where tha variable self.toolchange_xy was used for 2 different purposes which created loss of information.
-- fixed unit conversion functions in case the toolchange_xy parameter is None
-- more fixes in camlib.CNCJob regarding usage of toolchange (in case it is None)
-- fixed postprocessor files to work with toolchange_xy parameter value = None (no values in Edit - Preferences fields)
-- fixed Tcl commands CncJob and DrillCncJob to work with toolchange
-- added to the postprocessor files the command after toolchange to go with G00 (fastest) to "Z Move" value of Z pozition.
-
-29.01.2019
-
-- fixed issue in Tool Calculators when a float value was entered starting only with the dot.
-- added protection for entering incorrect values in Offset and Scale fields for Gerber and Geometry objects (in Selected Tab)
-- added more shortcut keys in the Geometry Editor and in Excellon Editor; activated also the zoom (fit, in, out) shortcut keys ('1' , '2', '3') for the editors
-- disabled the context menu in tools table on Paint Tool in case that the painting method is single.
-- added protection when trying to do Intersection in Geometry Editor without having selected Geometry items.
-- fixed the scale, mirror, rotate, skew functions to work with Geometry Objects of multi-geometry type.
-- added a GUI for Excellon Search time for OR-TOOLS path optimization in Edit -> Preferences -> Excellon General -> Optimization Time
-- more changes in Edit -> Preferences -> Geometry, Gerber and in CNCJob
-- added new option for Cutout Tool Freeform Gaps in Edit -> Preferences -> Tools
-- fixed Freeform Cutout gaps issue (it was double than the value set)
-- added protection so the Cutout (either Freeform or Rectangular) cannot be done on a multigeo Geometry
-- added 2Sided Tool default values in Edit -> Preferences -> Tools
-- optimized the FlatCAMCNCJob.on_plot_cb_click_table() plot function and solved a bug regarding having tools numbers not in sync with the cnc tool table
-
-28.01.2018
-
-- fixed the FlatCAMGerber.merge() function
-- added a new menu entry for the Gerber Join function: Edit -> Conversions -> "Join Gerber(s) to Gerber" allowing joining Gerber objects into a final Gerber object
-- moved Paint Tool defaults from Geometry section to the Tools section in Edit -> Preferences
-- added key shortcuts for Open Manual = F1 and for Open Online VideoHelp = F2
-
-27.01.2018
-
-- added more key shortcuts into the application; they are now displayed in the GUI menu's
-- reorganized the Edit -> Preferences -> Global
-- redesigned the messagebox that is showed when quiting ot creating a New Project: now it has an option ('Cancel') to abort the process returning to the app
-- added options for trace segmentation that can be useful for auto-levelling (code snippet from Lei Zheng from a rejected pull request on FlatCAM https://bitbucket.org/realthunder/ )
-- added shortcut key 'L' for creating 'New Excellon' 
-- added shortcut key combo 'SHIFT+S' for Running a Script.
-- modified grbl_laser postprocessor file so it includes a Sxxxx command on the line with M03 (laser active) whenever a value is enter in the Spindlespeed entry field
-- remade the EDIT -> PREFERENCES window, the Excellon and Gerber sections. Created a new section named TOOLS
-
-26.01.2019
-
-- fixed grbl_11 postprocessor in linear_code() function
-- added icons to the Project Tab context menu
-- added new entries to the Canvas context menu (Copy, Delete, Edit/Save, Move, New Excellon, New Geometry, New Project)
-- fixed grbl_laser postprocessor file
-- updated function for copy of an Excellon object for the case when the object has slots
-- updated FlatCAMExcellon.merge() function to work in case some (or all) of the merged objects have slots  
-
-25.01.2019
-
-- deleted junk folders
-- remade the Panelize Tool: now it is much faster, it is multi-threaded, it works with multitool geometries and it works with multigeo geometries too.
-- made sure to copy the options attribute to the final object in the case of: FlatCAMGeometry.merge(), FlatCAMGerber.merge() and for the Panelize Tool
-- modified the panelize TclCommand to take advantage of the new panelize() function; added a 'threaded' parameter (default value is 1) which controls the execution of the panelize TclCommand: threaded or non-threaded
-- fixed TclCommand Cutout
-- added a new TclCommand named CutoutAny. Keyword: cutout_any
-
-24.01.2019
-
-- trying to fix painting single when the actual painted object it's a MultiPolygon
-- fixed the Copy Object function when the object is Gerber
-- added the Copy entry to the Project context menu
-- made the functions behind Disable and Enable project context menu entries, non-threaded to fix a possible issue
-- added multiple object selection on Open ... and Import ... (idea and code snippet came from Travers Carter, BitBucket user https://bitbucket.org/travc/)
-- fixed 'grbl_laser' postprocessor bugs (missing functions)
-- fixed display geometry for 'grbl_laser' postprocessor
-- Excellon Editor - added possibility to create an linear drill array rotated at an custom angle
-- added the Edit and Properties entries to the Project context menu
-
-23.01.2019
-
-- added a new postprocessor file named 'line_xyz' which have x, y, z values on the same GCode line
-- fixed calculation of total path for Excellon Gcode file
-- modified the way FlatCAM preferences are saved. Now they can be saved as new files with .FlatConfig extension by the user and shared.
-- added possibility to open the folder where FlatCAM is saving the preferences files
-
-21.01.2019
-
-- changed some tooltips
-- added tooltips in Excellon tool table headers
-- in Excellon Tool Table the columns are now only selectable by clicking on the header (sorting is done automatically)
-- if CNCJob from Excellon then hide the CNC tools table in CNCJob Object
+- add Python folder and Python\Scripts folder to your Windows Path (https://docs.microsoft.com/en-us/previous-versions/office/developer/sharepoint-2010/ee537574(v%3Doffice.14))
+- verify that the pip package can be run by opening Command Prompt(Admin) and running the command:
+```
+pip -V
+```
 
+- look in the requirements.txt file (found in the sources folder) and install all the dependencies using the pip package. 
+The required wheels can be downloaded either from:
+https://www.lfd.uci.edu/~gohlke/pythonlibs/
+or
+https://pypi.org/
  
-20.01.2019
-
-- fixed the HPGL code geometry rendering when travel
-- fixed the message box layout when asking to save the current work
-- made sure that whenever the HPGL postprocessor is selected the Toolchange is always ON and the MultiDepth is OFF
-- the HPGL postprocessor entry is not allowed in Excellon Object postprocessor selection combobox as it is only applicable for Geometry
-- when saving HPGL code it will be saved as a file with extension .plt
-- the units mentioned in HPGL format are only METRIC therefore if FlatCAM units are in INCH they will be transform to METRIC
-- the minimum unit in HPGL is 0.025mm therefore the coordinates are rounded to a multiple of 0.025mm
-- removed the raise statement in do_worker_task() function as this is fatal in conjunction with PyQt5
-- added a try - except clause for the situations when for a font can't be determined the family and name
-- moved font parsing to the Geometry Editor: it is done everytime the Text tool is invoked
-- made sure that the HPGL postprocessor is not populated in the Excellon postprocessors in Preferences as it make no sense (HPGL is useful only for Geometries)
-
-19.01.2019
-
-- added initial implementation of HPGL postprocessor
-- fixed display HPGL code geometry on canvas
-
-11.01.2019
-
-- added a status message for font parsing
-
-9.01.2019
-
-- added a fix to allow creating of Excellon geometry even when there are points with no tools by skipping those points and warning the user about this in a Tcl message
-- added a message box asking users if they want to save the project in case that either New Project menu entry is clicked or if Exit menu entry is clicked or if the app is closed from the close button. The message box will be showed only if there are objects in the collection.
-- modified the first line in the Gcode header to show the FlatCAM version and version_date
-
-8.01.2019
-
-- added checkboxes in Preferences -> General -> Global Preferences to switch on/off version check at application startup and also to control if the app will send anonymous statistics about FlatCAM usage to help improve FlatCAM
-
-7.01.2019
-
-- added tooltips in Edit->Convert menu
-- fixed cutting from copper features when doing Gerber isolation with multiple passes
-
-6.01.2019
-
-- fixed the Marlin postprocessor detection in GCode header
-- the version date in GCode header is now the one set in FlatCAMApp.App.version_date
-- fixed bug in postprocessor files: number of drills is now calculated only for the Excellon objects in toolchange function (only Excellon objects have drills) 
-
-5.01.2019
-
-- fixed cncjob TclCommand - it used the default values for parameters
-- fixed the layout in ToolTransform
-- fixed the initial text in the ToolShell
-- reactivated the version check in case the release is not BETA; FlatCAMApp.App has now a beta object that when set True the application will show in the Title and help-> About that is Beta (and it disable version checking)
-- added a new name (mine: for good and/or bad) to the contributors list
-- fixed the Join function to work on Gerber and Excellon, Gerber and Gerber, Excellon and Excelon combination of objects. The merged property is the solid_geometry and the result is a FlatCAMGeometry object.
-
-3.01.2019
-
-- initial merge into FlatCAM regular
-
-28.12.2018
-
-- changed the workspace drawing from 'gl' to 'agg'. 'gl' has better performance but it messes with the overlapping graphics
-- removed the initial obj.build_ui() in App.editor2object()
-
-25.12.2018
-
-- fixed bugs in Excellon Editor due of PyQt5 port
-- fixed bug when loading Gerber with follow
-- fixed bug that when a Gerber was loaded with -follow parameter it could not be isolated external and full
-- changed multiple status bar messages
-- changed some assertions to (status error message + return) combo
-- fixed issues in 32bit installers
-- added protection against using Excellon joining on different kind of objects
-- fixed bug in ToolCutout where the Rectangular Cutout used the Type of Gaps from Freeform Cutout
-- fixed bug that didn't allowed saving SVG file from a Gerber file
-- modified setup_ubuntu.sh file for PyQt5 packages
-
-23.12.2018
-
-- added move (as in Tool Move) capability for CNCJob object and the GCode is updated on each move --> finished both for Gcode loaded and for CNCJob generated in the app
-- fixed some errors related to DialogOpen widget that I've missed in PyQt5 porting
-- added a bounds() method for CNCJob class in camlib (perhaps overdone as it worked well with the one inherited)
-- small changes in Paint Tool - the rest machining is working only partially
-- added more columns in CNCjob Tool Table showing more info about the present tools
-- make the columns in CNCJob Tool Table not editable as it has no sense
-
-22.12.2018
-
-- fixed issues in Transform Tool regarding the message boxes
-- fixed more error in Double Sided Tool and added some more information's in ToolTips
-- added more information's in CutOut Tool ToolTips
-- updated the tooltips in amost all FlatCAM tools; in Tool Tables added column header ToolTips
-- fixed NCC rest machining in NCC Tool; added status message and stop object creation if there is no geometry on any tool
-- fixed version number: now it will made of a number in format main_version.secondary_version/working_version
-- modified the makefile for windows builds to accommodate both 32bit and 64bit executable generation
-
-21.12.2018
-
-- added shortcut "SHIFT + W" for workspace toggle
-- updated the list of shortcuts
-- forbid editing for the MultiGeo type of Geometry because the Geometry Editor is not prepared for this
-- finished a "sort" of rest-machining for Non Copper Clearing Tool but it's time consuming operation
-- reworked the NCC Tool as it was fundamental wrong - still has issues on the rest machining
-- added a parameter reset for each run of Paint Tool and NCC Tool
-
-20.12.2018
-
-- porting application to PyQt5
-- adjusted the level of many status bar messages
-- created new bounds() methods for Excellon and Gerber objects as the one inherited from Geometry failed in conjunction with PyQt5
-- fixed some small bugs where a string was divided by a float finally casting the result to an integer
-- removed the 'raise' conditions everywhere I could and make protections against loading files in the wrong place
-- fixed a "PyCharm stupid paste on the previous tab level even after one free line " in Excellon.bounds()
-- in Geometry object fixed error in tool_delete regarding deletion while iterating a dict
-- started to rework the NCC Tool to generate one file only
-- in Geometry Tool Table added checkboxes for individual plot of tools in case of MultiGeo Geometry
-- rework of NCC Tool UI
-- added a automatic selector: if the system is 32bit the OR-tools imports are not done and the OR-tools drill path optimizations are replaced by a default Travelling Salesman drill path optimization
-- created a Win32 make file to generate a Win32 executable
-- disabled the Plot column in Geometry Tool Table when the geometry is SingleGeo as it is not needed
-- solved a issue when doing isolation, if the solid_geometry is not a list will make it a list
-- added tooltips in the Geometry Tool Table headers explaining each column
-- added a new Tcl Command: clear. It clears the Tcl Shell of all text and restore it to the original state
-- fixed Properties Tool area calculation; added status bar messages if there is no object selected show an error and successful showing properties is confirmed in status bar
-- when Preferences are saved, now the default values are instantly propagated within the application
-- when a geometry is MultiGeo and all the tools are deleted, it will have no geometry at all therefore all that it's plotted on canvas that used to belong to it has to be deleted and because now it is an empty object we demote it to SingleGeo so it can be edited
-
-19.12.2018
-
-- fixed SVG_export for MultiGeo Geometries
-- fixed DXF_export for MultiGeo Geometries
-- fixed SingleGeo to MultiGeo conversion plotting bug
-
-18.12.2018
-
-- small changes in FlatCAMGeometry.plot()
-- updated the FlatCAMGeometry.merge() function and the Join Geometry feature to accommodate the different types of geometries: singlegeo and multigeo type
-- added Conversion submenu in Edit where I moved the Join features and added the Convert from MultiGeo to SingleGeo type and the reverse
-- added Copy Tool (on a selection of tools) feature in Geometry Object UI 
-- fixed the bounds() method for the MultiGeo geometry object so the canvas selection is working and also the Properties Tool
-- fixed Move Tool to support MultiGeo geometry objects moving
-- added tool edit in Geometry Object Tool Table
-- added Tool Table context menu in Geometry Object and in Paint Tool
-- modified some Status Bar messages in Geometry Object
-
-17.12.2018
-
-- added support for multiple solid_geometry in a geometry object; each tool can now have it's own geometry. Plot, project save/load are OK.
-- added support for single GCode file generation from multi-tool PaintTool job
-- added protection for results of Paint Tool job that do not have geometry at all. An Error will be issued. It can happen if the combination of Paint parameters is not good enough
-- solved a small bug that didn't allow the Paint Job to be done with lines when the results were geometries not iterable 
-- added protection for the case when trying to run the cncjob Tcl Command on a Geometry object that do not have solid geometry or one that is multi-tool
-- Paint Tool Table: now it is possible to edit a tool to a new diameter and then edit another tool to the former diameter of the first edited tool
-- added a new type of warning, [WARNING_NOTCL]
-- fixed conflict with "space" keyboard shortcut for CNC job
-
-16.12.2018
-
-- redone the Options menu; removed the Transfer Options as they were not used
-- deleted some folders in the project structure that were never used
-- Paint polygon Single works only for left mouse click allowing mouse panning
-- added ability to print errors in status bar without raising Tcl Shell
-- fixed small bug: when doing interiors isolation on a Gerber that don't allow it, no object is created now and an error in the status bar is issued
-- fixed bug in Paint All for Geometry made from exteriors Gerber isolation
-- fixed the join geometry: when the geometries has different tools the join will fail with a status bar message (as it should). Allow joining of geometries that have no tool. // Reverted on 18.12.2018
-- changed the error messages that are simple to the kind that do not open the TCl shell
-- fixed some issues in Geometry Object
-- Paint Tool - reworked the UI and made it compatible with the Geometry Object UI
-- Paint Tool - tool edit functional
-- added Clear action in the Context menu of the TCl Shell
-
-14.12.2018
-
-- fixed typo in setup_ubuntu.sh
-- minor changes in Excellon Object UI
-- added Tool Table in Paint Tool
-- now in Paint Tool and Non Copper Clearing Tool a selection of tools can be deleted (not only one by one)
-- minor GUI changes (added/corrected tooltips)
-- optimized vispy startup time from about >6 sec to ~3 seconds
-- removed vispy text collection starting in plotcanvas as it did nothing // RESTORED 18.12.2018 as it messed the graphical presentation
-- fixed cncjob TclCommand for the new type of Geometry
-- make sure that when using the TclCommands, the object names are case insensitive
-- updated the TCL Shell auto-complete function; now it will index also the names of objects created or loaded in the application
-- on object removal the name is removed from the Shell auto-complete model
-
-13.12.2018
-
-NEW Geometry Object and CNC Object architecture (3rd attempt) which allow multiple tools for one geometry
-
-- fixed issue with cumulative G-code after successive delete/create of a CNCJob on the same geometry (some references were kept after deletion of CNCJob object which kept the deleted tools data and added it to a new one)
-- fixed plot and export G-code in new format
-- fixed project save/load in the new format for geometry
-- added new feature in CNCJob Object UI: since we may have multiple tools per CNCJob object due of having multiple tool in Geometry Object,
-now there is a Tool Table in CNC Object UI and each tool GCode can be enabled or disabled
-
-12.12.2018
-
-- Geometry Tool Table: when the Offset type is 'custom' each tool it's storing the value and it is updated on UI when that tool is selected in UI table
-- Geometry Tool Table: fixed tool offset conversion when the Offset in Tool Table UI is set to Custom
-
-11.12.2018
-
-- cleaned up the generatecncjob() function in FlatCAMObj
-- created a new function for generating cncjob out of multitool geometry, mtool_generate_cncjob()
-- cleaned up the generate_from_geometry_2() method in camlib
-- Geometry Tool Table: new tool added copy all the form fields (data) from the last tool
-- finished work on generation of a single CNC Job file (therefore a single GCODE file) even for multiple tools in Geo Tool Table
-- GCode header is added only on saving the file therefore the time generation will be reflected in the file
-- modified postprocessors to accommodate the new CNC Job file with multiple tools
-- modified postprocessors so the last X,Y move will be to the toolchange X,Y pos (set in Preferences)
-- save_project and load_project now work with the new type of multitool geometry and cncjob objects
-
-10.12.2018
-
-- added new feature in Geometry Tool Table: if the Offset type in tool table is 'Offset' then a new entry is unhidden and the user can use custom offset
-- Geometry Tool Table: fixed add new tool with diameter with many decimals
-- Geometry Tool Table: when editing the tip dia or tip angle for the V Shape tool, the CutZ is automatically calculated
-
-9.12.2018
-
-- new Geometry Tool Table has functional unit conversion
-- when entering a float number in Spindle Speed now there is no error and only the integer part is used, the decimals are discarded
-- finished the Geometry Tool Table in the form that generates only multiple files
-- if tool type is V-Shape ('V') then the Cut Z entry is disabled and new 'Tip Dia' and 'Tip Angle' fields are showed. The values entered will calculate the Cut Z parameter
-
-5.12.2018
-
-- remade the Geometry Tool Table, before this change each tool could not store it's own set of data in case of multiple tools with same diameter
-- added a new column in Geo Tool Table where to specify which type of tool to use: C for circular, B for Ball and V for V-shape
-
-4.12.2018
-
-- new geometry/excellon object name is now only "new_g"/"new_e" as the type is clear from the category is into (and the associated icon)
-- always autoselect the first tool in the Geometry Tool table
-- issue error message if the user is trying to generate CNCJob without a tool selected in Geometry Tool Table
-- add the whole data from Geometry Object GUI as dict in the geometry tool dict so each tool (file) will have it's own set of data
-
-3.12.2018
-
-- Geometry Tool table: delete multiple tools with same diameter = DONE
-- Geometry Tool table: possibility to cut a path inside or outside or on path = DONE
-- Geometry Tool table: fixed situation when user tries to add a tool but there is no tool diameter entered
-- if a geometry is a closed shape then create a Polygon out of it
-- some fixes in Non Copper Clearing Tool
-- Geometry Tool table: added option to delete_tool function for delete_all
-- Geometry Tool table: added ability to delete even the last tool in tool_table and added an warning if the user try to generate a CNC Job without a tool in tool table
-- if a geometry is painted inside the Geometry Editor then it will store the tool diameter used for this painting. Only one tool cn be stored (the last one) so if multiple paintings are done with different tools in the same geometry it will store only the last used tool.
-- if multiple geometries have different tool diameters associated (contain a paint geometry) they aren't allowed to be joined and a message is displayed letting the user know
-
-2.12.2018
-
-- started to work on a geometry Tool Table
-- renamed FlatCAMShell as ToolShell and moved it (and termwidget) to flatcamTools folder
-- cleaned up the ToolShell by removing the termwidget separate file and added those classes to ToolShell
-- added autocomplete for TCL Shell - the autocomplete key is 'TAB'
-- covered some possible exceptions in rotate/skew/mirror functions
-- Geometry Tool table: add/delete tools = DONE
-- Geometry Tool table: add multiple tools with same diameter = DONE
-
-1.12.2018
-
-- fixed Gerber parser so now the Gerber regions that have D02 operation code just before the end of the region will be processed correctly. Autotrax Dex Gerbers are now loaded
-- fixed an issue with temporary geo storage "geo" being referenced before assignment
-- moved all FlatCAM Tools into a single directory
-
-30.11.2018
-
-- remade the CutOut Tool. I've put together the former Freeform Cutout tool and the Cutout Object fount in Gerber Object GUI and left only a link in the Gerber Object GUI. This tidy the GUI a bit.
-- created a Paint Tool and replaced the Paint Area section in Geometry Object GUI with a link to this tool.
-- fixed bug in former Paint Area and in the new Paint Tool that made the paint method not to be saved in App preferences
-- solved a bug in Gerber parser: in case that an operation code D? was encountered alone it was not remembered - fixed
-- fixed bug related to the newly entered toolchange feature for Geometry: it was trying to evaluate toolchange_z as a comma separated value like for toolchange x,y
-- fixed bug in scaling units in CNC Job which made the unit change between INCH and MM not possible if a CNC Job was present in the project objects
-
-29.11.2018
-
-- added checks for using a Z Cut with positive value. The Z Cut parameter has to be negative so if the app will detect a positive value it will automatically convert it to negative
-- started to implement rest-machining for Non Copper clearing Tool - for now the results are not great
-- added Toolchange X,Y position parameters and modified the default and manual_toolchange postprocessor file to use them
-For now they are used only for Excellon objects who do have toolchange events
-- added Toolchange event selection for Geometry objects; for now it is as before, single tool on each file
-- remade the GUI for objects and in Preferences to have uniformity
-- fixed bug: after editing a newly created excellon/geometry object the object UI used to not keep the original settings
-- fixed some bugs in Tool Add feature of the new Non Copper Clear Tool
-- added some messages in the Non Copper Clear Tool
-- added parameters for coordinates no of decimals and for feedrate no of decimals used in the resulting GCODE. They are in EDIT -> Preferences -> CNC Job Options
-- modified the postprocessors to use the "decimals" parameters
-
-28.11.2018
-
-- added different methods of copper clearing (standard, seed, line_based) and "connect", "contour" options found in Paint function
-- remake of the non-copper clearing tool as a separate tool
-- modified the "About" menu entry to mention the main contributors to FlatCAM 3000 
-- modified Marlin postprocessor according to modifications made by @redbull0174 user from FlatCAM.org forum
-- modified Move Tool so it will detect if there is no object to move and issue a message
-
-27.11.2018
-
-- fixed bug in isolation with multiple passes
-- cosmetic changes in Buffer and Paint tool from Geometry Editor
-- changed the way selection box is working in Geometry Editor; now cumulative selection is done with modifier key (SHIFT or CONTROL) - before it was done by default
-- changed the default value for CNCJob tooldia to 1mm
-
-25.11.2018
-
-- each Tool change the name of the Tools tab to it's name
-- all open objects are no longer autoselected upon creation. Only on new Geometry/Excellon object creation it will be autoselected
-
-24.11.2018
-
-- restored the selection method in Geometry Editor to the original one found in FlatCAM 8.5
-- minor changes in Clear Copper function
-- minor changes in some postprocessors
-- change Join Geometry menu entry to Join Geo/Gerber
-- added menu entry for Toggle Axis in Menu -> View
-- added menu entry for Toggle Workspace in Menu -> View
-- added Bounding box area to the Properties (when metric units, in cm2)
-- non-copper clearing function optimization
-- fixed Z_toolchange value in the GCODE header
-
-21.11.2018
-
-- not very precise jump to location function
-- added shortcut key for jump to coordinates (J) and for Tool Transform (T)
-- some work in shortcut key
-
-19.11.2018
-
-- fixed issue with nested comment in postprocessors
-- fixed issue in Paint All; reverted changes
-
-18.11.2018
-
-- renamed FlatCAM 2018 to FlatCAM 3000
-- added new entries in the Help menu; one will show shortcut list and the other will start a YouTube webpage with a playlist where I will publish future training videos for this version of FlatCAM
-- if a Gerber region has issues the file will be loaded bypassing the error but there will be a TCL message letting the user know that there are parser errors. 
-
-17.11.2018
-
-- added Excellon parser support for units defined outside header
-
-
-12.11.2018
-
-- fixed bug in Paint Single Polygon
-- added spindle speed in laser postprocessor
-- added Z start move parameter. It controls the height at which the tool travel on the fist move in the job. Leave it blank if you don't need it.
-
-9.11.2018
-
-- fixed a reported bug generated by a typo for feedrate_z object in camlib.py. Because of that, the project could not be saved.
-- fixed a G01 usage (should be G1) in Marlin postprocessor.
-- changed the position of the Tool Dia entry in the Object UI and in FlatCAMGUI
-- fixed issues in the installer
-
-30.10.2018
-
-- fixed a bug in Freeform Cutout Tool - it was missing a change in the name of an object
-
-29.10.2018
-
-- added Excellon export menu entry and functionality that can export in fixed format 2:4 LZ INCH (format that Altium can load and it is a more generic format).
-It will be usefull for those who need FlatCAM to only convert the Excellon to a more useful format and visualize Gerbers.
-The other Excellon Export menu entry is exporting in units either Metric or INCH depending on the current units in FlatCAM, but it will always use the decimal format which may not be loaded in all cases.
-- disabled the Selected Tab while in Geometry Editor; the user is not supposed to have access to those functions while in Geometry Editor
-- added an menu entry in Menu -> File -> Recent Files named Clear Recent files which does exactly that
-- fixed issue: when a New Project is created but there is a Geometry still in Geometry Editor (or Excellon Editor) not saved, now that geometry is deleted
-- fixed problem when doing Clear Copper with Cut over 1st point option active. When the shape is not closed then it may cut over copper features. Originally the feature was meant to be used only with isolation geometry which is closed. Fixed
-
-28.10.2018
-
-- fixed Excellon Editor shortcut messages; also fixed differences in messages between usage by shortcuts and usage by menu toolbar actions
-- fixed Excellon Editor bug: it was triggering exceptions when the user selected a tool in tooltable and then tried to add a drill (or array) by clicking on canvas
-Clicking on canvas by default clear all the used tools, therefore the action could not be done. Fixed.
-- fixed bug Excellon Editor: when all the drills from a tool are resized, after resize they can't be selected.
-- Excellon Editor: added ability to delete multiple tools at once by doing multiple selection on the tooltable
-- Excellon Editor: if there are no more drills to a tool after doing drills resize then delete that tool from the tooltable
-- Excellon Editor: always select the last tool added to the tooltable
-- Excellon Editor: added a small canvas context menu for Excellon Editor
-
-27.10.2018
-
-- added a Paint tool toolbar icon and added shortcut key 'I' for Paint Tool
-- fixed unreliable multiple selection in Geometry Editor; some clicks were not registered
-- added utility geometry for Add Drill Array in Excellon Editor
-- fixed bug Excellon Editor: drills in drill array start now from the array start point (x, y); previously array start point was used only for calculating the radius
-- fixed bug Excellon Editor: Measurement Tool was not acting correctly in Exc Editor regarding connect/disconnect of events
-- in Excellon Editor every time a tool is clicked (except Select which is the default) the focus will return to Selected tab
-- added protection in Excellon Editor: if there is no tool/drill selected no operation over drills can be performed and a status bar message will be displayed
-- Excellon Editor: added relevant messages for all actions
-- fixed bug Excellon Editor: multiple selection with key modifier pressed (CTRL/SHIFT) either by simple click or through selection box is now working
-- fixed dwell parameter for Excellon in Preferences to be default Off
-
-26.10.2018
-
-- when objects are disabled they can't be selected
-- added Feedrate_z (Plunge) parameter for Geometry Object
-- fixed bug in units convert for Geometry Tab; added some missing parameters to the conversion list
-- fixed bug in isolation Geometry when the isolated Gerber was a single Polygon
-- updated the Paint function in Geometry Editor
-
-25.10.2018
-
-- added a verification on project saving to make sure that the project was saved successfully. If not, a message will be displayed in the status bar saying so.
-
-20.10.2018
-
-- fixed the SVG import as Gerber. But unfortunately, when there is a ground pour in a imported PCB SVG, the ground pour will be isolated inside
-instead to be isolated outside like every other feature. That's no way around this. The end result will be thinner features
-for the ground pour and if one is relying on those thin connections as GND links then it will not work as intended ,they may be broken.
-Of course one can edit the isolation geometry and delete the isolation for the ground pour.
-- delete selection shapes on double clicking on object as we may not want to have selection shape while Selected tab is active
-
-19.10.2018
-
-- solved some value update bugs in tool_table in Excellon Editor when editing tools followed by deleting another tool,
-and then re-adding the just-deleted tool.
-- added support for chaining blocks in DXF Import
-- fixed the DXF arc import
-- added support for a type of Gerber files generated by OrCAD where the Number format is combined with G74 on the same line
-- in Geometry Editor added the possibility for buffer to use different kinds of corners
-- added protection against loading an GCODE file as Excellon through drag & drop on canvas or file open dialog
-- added shortcut key 'B' for buffer operation inside Geometry Editor
-- added shell message in case the Font used in Text Tool in Geometry editor is not supported. Only Regular, Bold, Italic adn BoldItalic are supported as of yet.
-- added shortcut key 'T' for Text Tool inside Geometry Editor
-- added possibility for Drag & Drop on FlatCAM GUI with multiple files at once 
-
-18.10.2018
-
-- fixed DXF arc import in case of extrusion enabled
-- added on Geo Editor Toolbar the button for Buffer Geometry; added the possibility to create exterior and interior buffer
-- fixed a numpy import error
-
-17.10.2018
-
-- added Spline support and Ellipse (chord) support in DXF Import: chord might have issues
-(borrowed from the work of Vasilis Vlachoudis, https://github.com/vlachoudis/bCNC)
-- added Block support in DXF Import - no support yet for chained blocks (INSERT in block)
-- support for repasted block insertions
-
-16.10.2018
-
-- added persistent toolbar view: the enabled toolbars will be active at the next app startup while those that are not enabled will not be
-enabled at the next app startup. To enable/disable toolbars right click on the toolbar.
-
-15.10.2018
-
-- DXF Export works now also for Exteriors only and Interiors only geometry generated from Gerber Object
-- when a Geometry is edited, now the interiors and exterior of a Polygon that is part of the Geometry can be selected individually. In practice, if
-doing full isolation geometry, now both external and internal trace can be selected individually.
-
-13.10.2018
-
-- solved issue in CNC Code Editor: it appended text to the previous one even if the CNC Code Editor was closed
-- added .GBD Gerber extension to the lists
-- added support for closed polylines/lwpolylines in Import DXF; now PCB patterns found in PDF format can be imported in INKSCAPE
-and saved as DXF. FlatCAM can import DXF as Gerber and the user now can do isolation on it.
-
-12.10.2018
-
-- added zoom in, zoom out and zoom fit buttons on the View toolbar
-- fixed bug that on Double Sided Tool when a Excellon Alignment is created does not reset the list of Alignment drills
-- added a message warning the user to add Point coordinates in case the reference used in Double Sided Tool is Point
-- added new feature: DXF Export for Geometry
-
-10.10.2018
-
-- fixed a small bug in Setup Recent Files
-- small fix in Freeform Cutout Tool regarding objects populating the combo boxes
-- Excellon object name will reflect the number of edits performed on it
-
-9.10.2018
-
-- In Geometry Editor, now Path and Polygon draw mode can be finished not only with shortcut key Enter but also with right click on canvas
-- fixes regarding of circle linear approximation - final touch
-- fix for interference between Geo Editor and Excellon Editor
-- fixed Cut action in Geometry Editor so it can now be done multiple times on the target geometry without need for saving in between.
-- initial work on DXF import; made the GUI interface and functional structure
-- added import functions for DXF import
-- finished DXF Import (no blocks support, no SPLINE support for now)
-
-8.10.2018
-
-- completed toggle canvas selection when there is only one object under click position for the case when clicking the object is done
-while other object is already selected.
-- added static utility geometry just upon activating an Editor function
-- changed the way the canvas is showed on FlatCAM startup
-
-7.10.2018
-
-- solved mouse click not setting relative measurement origin to zero
-- solved bug that always added one drill when copying a selection of drills in the EXCELLON EDITOR
-- solved bug that the number of copied drills in Excellon Editor was not updated in the tool table
-- work in the Excellon Editor: found useful to change the diameter of one tool to another already in the list;
-could help for all those tools that are a fraction difference that comes from imperial to mm (or reverse) conversion,
-to reduce the tool changes - Done
-- in Excellon Editor, always auto-select the last tool added
-- in Excellon Editor fixed shortcuts for drill add and drill_array add: they were reversed. Now key 'A' is for array add
-and key 'D' is for drill add
-- solved a small bug in Excellon export: even when there were no slots in the file, it always added the tools list that
-acted as unnecessary toolchanges
-- after Move action, all objects are deselected
-
-
-6.10.2018
-
-- Added basic support for SVG text in SVG import. Will not work if some letters in a word have different style (italic bold or both)
-- added toggle selection to the canvas selection if there is only one object under the click position
-- added support for "repeat" command in Excellon file
-- added support for Allegro Gerber and Excellon files
-- Python 3.7 is used again; solved bug where the activity icon was not playing when FlatCAM active
-
-5.10.2018
-
-- fixed undesired setting focus to Project Tab when doing the SHIFT + LMB combo (to capture the click coordinates)
-
-4.10.2018
-
-- Excellon Editor: finished Add Drill Array - Linear type action
-- Excellon Editor: finished Add Drill Array - Circular type action
-- detected bug in shortcuts: Fixed
-- Excellon Editor: added constrain for adding circular array, if the number of drills multiplied by angle is more than 360
-the app will return with an message
-- solved sorting bug in the Excellon Editor tool table
-- solved bug in Menu -> Edit -> Sort Origin ; the selection box was not updated after offset
-- added Excellon Export in Menu -> File -> Export -> Export Excellon
-- added support to save the slots in the Excellon file in case there were some in the original file
-- fixed Double Sided Tool for the case of using the box as mirroring reference.
-
-2.10.2018
-
-- made slots persistent after edit
-- bug detected: in Excellon Editor if new tool added diameter is bigger than 10 it mess things up: SOLVED
-- Excellon Editor: finished Drill Resize action
-- after an object is deleted from the Project list, if the current tab in notebook is not Project,
-always focus in the Project Tab (deletion can be done by shortcut key also)
-- changed the initial view to include the possible enabled workspace guides
-
-1.10.2018
-
-- added GUI for Excellon Editor in the Tool Tab
-- Excellon Editor: created and populated the tool list
-- Excellon Editor: added possibility to add new tools in the list
-- Excellon Editor: added possibility to delete a tool (and the drills that it contain) by selecting a row in the tool table and 
-clicking the Delete Tool button
-- Excellon Editor: added possibility to change the tool diameter in the tool list for existing tool diameters.
-- Excellon Editor: when selecting a drill, it will highlight the tool in the Tool table
-- Excellon Editor: optimized single click selection
-- Excellon Editor: added selection for all drills with same diameter upon tool selection in tool table; fix in tool_edit
-- Excellon Editor: added constrain to selection by single click, it will select if within a certain area around the drill
-- Excellon Editor: finished Add Drill action
-- Excellon Editor: finished Move Drill action
-- Excellon Editor: finished Copy Drill action
-
-- fixed issue: when an object is selected before entering the Editor mode, now the selecting shape is deleted before entry 
-in the Editor (be it Geometry or Excellon).
-- fixed a few glitches regarding the units change
-- when an object is deselected on the Plot Area, the notebook will switch to Project Tab
-- changed the selection behavior for the dragging rectangle selection box in Editor (Geometry, Excellon): by dragging a
-selection box and selecting is cumulative: it just adds. To remove from selection press key Ctrl (or Shift depending of 
-the setting in the Preferences) and drag the rectangle across the objects you want to deselect.
-
-29.09.2018
-
-- optimized the combobox item population in Panelization Tool and in Film Tool
-- FlatCAM now remember the last path for saving files not only for opening
-- small fix in GUI
-- work on Excellon Editor. Excellon editor working functions are: loading an Excellon object into Editor, 
-saving an Excellon object from editor to FlatCAM, selecting drills by left click, selection of drills by dragging rectangle, deletion of drills.
-- fixed Excellon merge
-- added more Gcode details (depthperpass parameter in Gcode header) in postprocessors
-- deleted the Tool informations from header in postprocessors due to Mach3 not liking the lot of square brackets
-- more corrections in postprocessors
-
-
-28.09.2018
-
-- added a save_defaults() call on App exit from action on Menu -> File -> Exit
-- solved a small bug in Measurement Tool
-- disabled right mouse click functions when Measurement Tools is active so the user can do panning and find the destination point easily
-- added a new button named "Measure" in Measurement Tool that allow easy access to Measurement Tool from within the tool
-- fixed a bug in Gerber parser that when there was a rectangular aperture used within a region, some artifacts were generated.
-- some more work on Excellon Editor
-
-27.09.2018
-
-- fixed bug when creating a new project, if a previous object was selected on screen, the selection shape
-survived the creation of a new project
-- added compatibility with old type of FlatCAM projects
-- reverted modifications to the way that Excellon geometry was stored to the old way.
-- added exceptions for Paint functions so the user can know if something failed.
-- modified confirmation messages to use the color coded messages (error = red, success = green, warning = yellow)
-- restored activity icon
-
-26.09.2018
-
-- disabled selection of objects in Project Tab when in Editor
-- the Editor Toolbar is hidden in normal mode and it is showed when Editor
-is activated. I may change this behaviour back.
-- changed names in classes, functions to prepare for the Excellon editor
-
-- fixed bugs in Paint All function
-- fixed a bug in ParseSVG module in parse_svg_transform(), related to 'scale'
-
-- moved all the Editor menu/toolbar creation to FlatCAMUI where they belong
-- fixed a Gerber parse number issue when Gerber zeros are TZ (keep trailing zeros)
-
-- changed the way of how the solid_geometry for Excellon files is stored
-and plotted. Before everything was put in the same "container". Now,
-the geometries of drills and slots are organized into dictionaries having
-as keys the tool diameters and as values list of Shapely objects (polygons)
-- fix for Excellon plotting for newly created empty Excellon Object
-- fixed geometry.bounds() in camlib to work with the new format of the Excellon geometry (list of dicts)
-
-24.09.2018
-
-- added packages in the Requirements and setup_ubuntu.sh. Tested in Ubuntu and
-it's OK
-- added Replace (All) feature in the CNC Code Editor
-- made CNC Code generation for Excellon to show progress
-- added information about transforms in the object properties (like skew
-and how much, if it was mirrored and so on)
-- made all the transforms threaded and make them show progress in the progress bar
-- made FlatCAM project saving, threaded.
- 
-23.09.2018
-
-- added support for "header-less" Excellon files. It seems that Mentor PADS does generate such
-non-standard Excellon files. The user will have to guess: units (IN/MM), type of zero suppression LZ/TZ 
-(leading zeros or trailing zeros are kept) and Excellon number format(digits and decimals). 
-All of those can be adjusted in Menu -> Edit -> Preferences -> Excellon Object -> Excellon format
-- fixed svgparse for Path. Now PCB rasted images can traced in Inkscape or PDF's can be converted
-and then saved as SVG files which can be imported into FlatCAM. This is a convolute way to convert a PDF
-to Gerber file.
-
-22.09.2018
-
-- added Drag & Drop capability. Now the user can drag and drop to FlatCAM GUI interface a file 
-(with the right extension) that can be a FlatCAM project file (.FlatPrj) a Gerber file, 
-an Excellon file, a G-Code file or a SVG file.
-- made the Move Tool command threaded
-- added Image import into FlatCAM
-
-21.09.2018
-
-- added new information's in the object properties: all used Tool-Table items
-are included in a new entry in self.options dictionary
-- modified the postprocessor files so they now include information's about
-how many drills (or slots) are for each tool. The Gcode will have this
-information displayed on the message from ToolChange.
-- removed some log.debug and add new log.debug especially for moments when some process is finished
-- fixed the utility geometry for Font geometry in Geometry Editor
-- work on selection in Geometry Editor
-- added multiple selection key as a Preference in Menu -> Edit -> Preferences
-It can be either Shift or Ctrl.
-- fixed bug in Gerber Object -> Copper Clearing.
-- added more comprehensive tooltips in Non-copper Clearing as advice on how to proceed.
-- adjusted make_win32.py file so it will work with Python 3.7 (cx_freeze can't copy OpenGL files, so
-it has to be done manually)
-
-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.
-
-- 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
-- made the workspace app default state = False
-- made the workspace to resize when units are changed
-- disabled automatic defaults save (might create SSD wear)
-- added an automatic defaults save on FlatCAM application close
-- made the draw method for the Workspace lines 'agg' so the quality of the FC objects will not be affected
-
-- added Area constrain to the Panelization Tool: if the resulting area is too big to fit within constrains, the number
-of columns and/or rows will be reduced to the maximum that still fits is.
-- removed the Flip command from Panelization Tools because Flipping (Mirroring) should be done properly with the 
-Transform Tool or using the provided shortcut keys.
-
-- made Font parsing threaded so the application will not wait for the font parsing to complete therefore the app start
-is faster
-
-
-17.09.2018
-
-- fixed Measuring Tool not working when grid is turned OFF
-- fixed Roland MDX20 postprocessor
-- added a .GBR extension in the open_gerber filter
-- added ability to Scale and Offset (for all types of objects) to just
-press Enter after entering a value in the Entry just like in Tool Transform
-- added capability in Tool Transform to mirror(flip) around a certain Point.
-The point coordinates can either be entered by hand or they can be captured
-by left clicking while pressing key "SHIFT" and then clicking the Add button
-- added the .ROL extension when saving Machine Code
-- replaced strings that reference to G-Code from G-Code to CNC Code
-- added capability to open a project by serving the path/project_name.FlatPrj as a parameter
-to FlatCAM.py
-
-15.09.2018
-
-- removed dwell line generator and included dwell generation in the postprocessor files
-- added a proposed RML1 Roland_MDX20 postprocessor file.
-- added a limit of 15mm/sec (900mm/min) to the feedrate and to the feedrate_rapid. Anything faster than this
-will be capped to 900mm/min regardless what is entered in the program GUI. This is because Roland MDX-20 has
-a mechanical limit of the speed to 15mm/sec (900mm/min in GUI)
-
-14.09.2018
-- remade the Double Sided Tool so it now include mirroring of Excellon and Geometry Objects along Gerber.
-Made adding points easier by adding buttons to GUI that allow adding the coordinates captured by
-left mouse click + SHIFT key
-- added a few fixes in code to the other FlatCAM tools regarding reset_fields() function. The issue
-was present when clicking New Project entry in Menu -> File.
-- FIXED: fix adding/updating bounding box coords for the mirrored objects in Double side Tool.
-- FIXED: fix the bounding box values from within FlatCAM objects, upon units change.
-- fixed issue with running again the constructor of the drawing tools after the tool action was complete,
-in Geometry Editor
-- fixed issue with Tool tab not closed after Text Input tool is finished.
-- fixed issue with TEXT to GEOMETRY tool, the resulting geometry was not scaled depending of current units
-- fixed case when user is clicking on the canvas to place a Font Geometry without clicking apply button first
-or the Font Geometry is empty, in Geometry Editor - > Text Input tool
-- reworked Measuring Tool by adding more information's (START, STOP point coordinates) and remade the 
-strings
-- added to Double Sided Tool the ability to use as reference box Excellon and Geometry Objects
-
-12.09.2018
-
-- fixed Excellon Object class such that Excellon files that have both drills and slots are supported
-- remade the GUI interface for the Excellon Object in a more compact way; added a column with slots numbers
-(if any) along the drills numbers so now there is only one tool table for drills and slots.
-- remade the GUI in Preferences and removed unwanted stretch that was broken the layout.
-- if for a certain tool, the slots number is zero it will not be displayed
-- reworked Text to Geometry feature to work in Linux and MacOS
-- remade the Text to Geometry so font collection process is done once at app start-up improving the performance
-
-
-09.09.2018
-
-- added TEXT ENTRY SUPPORT in Geometry Editor. It will convert strings of True Type Fonts to geometry. 
-The actual dimensions are approximations because font size is in points and not in metric or inch units.
-For now full support is limited to Windows. In Linux/MacOS only the fonts for which the font name is the same 
-as the font filename are supported. Italic and Bold functions may not work in Linux/MacOS.
-- solved bug: some Drawing menu entries not having connected functions
-
-28.08.2018
-
-- fixed Gerber parser so now G01 "moving" rectangular 
-aperture is supported.
-- fixed import_svg function; it can import SVG as geometry (solved bug)
-- fixed import_svg function; it can import SVG as Gerber (it did not work previously)
-- added menu entry's for SVG import as Gerber and separated import as Geometry
-
-27.08.2018
-
-- fixed Gerber parser so now FlatCAM can load Gerber files generated by Mentor Graphics EDA programs.
-
-26.08.2018
-
-- added awareness for missing coordinates in Gerber parsing. It will try to use the previous coordinates but if there
-are not any those lines will be ignored and an Warning will be printed in Tcl Shell.
-- fixed TCL commands AlignDrillGrid and DrilCncJob
-- added TCL script file load_and_run support in GUI
-- made the tool_table in Excellon to automatically adjust the table height depending on the number of rows such that
-all the rows will be displayed.
-- structural changes in the Excellon build_ui()
-- icon changes and menu compress
-
-23.08.2018
-
-- added Excellon routing support
-- solved a small bug that crippled Excellon slot G85 support when the coordinates
-are with period.
-- changed the way selection is done in Geometry Editor; now it should work
-in all cases (although the method used may be computationally intensive,
-because sometimes you have to click twice to make selection if you do it too fast)
-
-21.08.2018
-
-- added Excellon slots support when using G85 command for generation of
-the slots file. Inspired from the work of @mgix. Thanks.
-Routing format support for slots will follow. 
-- minor bug solved: option "Cut over 1st pt" now has same name both in
-Preferences -> Geometry Options and in Selected tab -> Geomety Object.
-Solves #3
-- added option to select Climb or Conventional Milling in Gerber Object options
-Solves #4
-- made "Combine passes" option to be saved as an app preference
-- added Generate Exteriors Geo and Generate Interiors Geo buttons in the
-Gerber Object properties
-- added configuration for the number of steps used for Gerber circular aperture
-linear approximation. The option is in Preferences -> Gerber Options
-- added configuration for the number of steps used for Gcode circular aperture
-linear approximation. The option is in Preferences -> CNCjob Options
-- added configuration for the number of steps used for Geometry circular aperture
-linear approximation. The option is in Preferences -> Geometry Options. It is used 
-on circles/arcs made in Geometry Editor and for other types of geometries generated in 
-the app.
-
-
-17.07.2018
-
-- added the required packages in Requirements.txt file
-- added required packages in setup_ubuntu.sh file
-- added color control over almost all the colors in the application; those
-settings are in Menu -> Edit -> Preferences -> General Tab
-- added configuration of which mouse button to be used when panning (MMB or RMB)
-- fixed bug with missing 'drillz' parameter in function generate_from_excellon_by_tool()
-(credits for finding it goes to Stefan Smith https://bitbucket.org/stefan064/)
-- load Factory defaults in Preferences will load the defaults that are used just after
-first install. Load Defaults option in Preferences will load the User saved Defaults.
-
-03.07.2018
-
-- fixed bug in rotate function that didn't update the bounding box of the
-modified object (rotated) due of not emitting the right signal parameter.
-- removed the Options tab from the Notebook (the left area where is located
-also the Project tab). Replaced it with the Preferences Tab launched with
-Menu -> Edit -> Preferences
-- when FlatCAM is used under MacOS, multiple selection of shapes in Editor
-mode is done using SHIFT key instead of CTRL key due of MacOS interpreting
-CTRL+LMB_click as a RMB click
-- when in Editor, clicking not on a shape, reset the index of selected shapes
-to zero
-- added a new Tab in the Plot Area named Gcode Editor. It allow the user to
-edit the Gcode and then Save it or Print it.
-- added a fix so the 'preamble' Gcode is correctly inserted between the
-comments header and the actual GCODE
-- added Find function in G-Code Editor
-
-
-27.06.2018
-
-- the Plot Area tab is changing name to "Editor Area" when the Editor is
-activated and returns to the "Plot Area" name upon exiting the Editor
-- made the labels shorter in Transform Tool in anticipation of
-Options Tab removal from Notebook and replacing it with Preferences
-- the Excellon Editor is not finished (not even started yet) so the
-Plot Area title should stay "Plot Area" not change to "Editor Area" when
-attempting to edit an Excellon file. Solved.
-- added a header comment block in the generated Gcode with useful
-information's
-- fixed issue that did not allow the Nightly's to be run in
-Windows 7 x64. The reason was an outdated DLL file (freetype.dll) used
-by Vispy python module.
-
-
-25.06.2018
-
-- "New" menu entry in Menu -> File is renamed to "New Project"
-- on "New Project" action, all the Tools are reinitialized so the Tools
-tab will work as expected
-- fixed issue in Film Tool when generating black film
-- fixed Measurement Tool acquiring and releasing the mouse/key events
-- fixed cursor shape is updated on grid_toggle
-- added some infobar messages to show the user when the Editor was
-activated and when it was closed (control returned to App).
-- added thread usage for Film tool; now the App is no longer blocked on
-film generation and there is a visual clue that the App is working
-
-22.06.2018
-
-- added export PNG image functionality and menu entry in
-Menu -> File -> Export PNG ...
-- added a command to set focus on canvas inside the mouve move event
-handler; once the mouse is moved the focus is moved to canvas so the
-shortcuts work immediatly.
-- solved a small bug when using the 'C' key to copy name of the selected
-object to clipboard
-
-- fixed millholes() function and isolate() so now it works even when the
-tool diameter is the same as the hole diameter.
-
-Actually if the passed value to  the buffer() function is zero, I
-artificially add a value of 0.0000001 (FlatCAM has a precision of
-6 decimals so I use a tenth of that value as a pseudo "zero")
-because the value has to be positive. This may have solved for some use
-cases the user complaints that on clearing the areas of copper there is
-still copper leftovers.
-
-- added shortcut "SHIFT+G" to toggle the axis presence. Useful when one
-wants to save a PNG file.
-- changed color of the grid from 'gray' to 'dimgray'
-
-- the selection shape is deleted when the object is deleted
-
-- the plot area is now in a TAB.
-- solved bug that allowed middle button click to create selection
-- fixed issue with main window geometry restore (hopefully).
-- made view toolbar to be hidden by default as it is not really needed
-(we have the functions in menu, zoom is done with mouse wheel, and there
-is also the canvas context menu that holds the functionality)
-- remade the GUIElements.FCInput() and made a GUIElements.FCTab()
-- on visibility plot toogle the selection shape is deleted
-
-- made sure that on panning in Geometry editor, the context menu is not
-displayed
-- disabled App shortcut keys on entry in Geometry Editor so only the
-local shortcut keys are working
-
-- deleted metric units in canvas context menu
-- added protection so object deletion can't be done until Geometry
-Editor session is finished. Solved bug when the shapes on Geometry
-Editor were not transfered to the New_geometry object yet and the
-New_Geometry object is deleted. In this case the drawn shapes are left
-in a intermediary state on canvas.
-
-- added selection shape drawing in Geometry Editor preserving the
-current behavior: click to select, click on canvas clear selection,
-CTRL+click add to selection new shape but remove from selection
-if already selected. Drag LMB from left to right select enclosed
-shapes, drag LMB from right to left select touching shapes. Now the
-selection is made based on
-- added info message to be displayed in infobar, when a object is
-renamed
-
-20.06.2018
-
-- there are two types of mouse drag selection (rectangle selection)
-If there is a rectangle selection from left to right, the color of the
-selection rectangle is blue and the selection is "enclosing" - this
-means that the object to be selected has to be enclosed by the selecting
-blue rectangle shape.
-If there is a rectangle selection fro right to left, the color of the
-selection rectangle is green and the selection is "touching" - this
-means that it's enough to touch with the selecting green rectangle the
-object(s) to be selected so they become selected
-- changed the modifier key required to be pressed when LMB is ckicked
-over canvas in order to copy to clipboard the coordinates of the click,
-from CTRL to SHIFT. CTRL will be used for multiple selection.
-- change the entry names in the canvas context menu
-- disconnected the app mouse event functions while in geometry editor
-since the geometry editor has it's own mouse event functions and there
-was interference between object and geometry items. Exception for the
-mouse release event so the canvas context menu still work.
-- solved a bug that did not update the obj.options after a geometry
-object was edited in geometry editor
-- solved a bug in the signal that saved the position and dimensions of
-the application window.
-- solved a bug in app.on_preferences() that created an error when run
-in Linux
-
-18.06.2018 Update 1
-
-- reverted the 'units' parameter change to 'global_units' due of a bug
-that did not allow saving of the project
-- modified the camlib transform (rotate, mirror, scale etc) functions
-so now they work with Gerber file loaded with 'follow' parameter
-
-18.06.2018
-
-- reworked the Properties context menu option to a Tool that displays
-more informations on the selected object(s)
-- remade the FlatCAM project extension as .FlatPrj
-- rearranged the toolbar menu entries to a more properly order
-- objects can now be selected on canvas, a blue polygon is drawn around
-when selected
-- reworked the Tool Move so it will work with the new canvas selection
-- reworked the Measurement Tool so it will work with the new canvas
-selection
-- canvas selection can now be done by dragging left mouse boutton and
-creating a selection box over the objects
-- when the objects are overlapped on canvas, the mouse click
-selection works in a circular way, selecting the first, then the second,
-then ..., then the last and then again the first and so on.
-- double click on a object on canvas will open the Selected Tab
-- each object store the bounding box coordinates in the options dict
-- the bbox coordinates are updated on the obj options when the object
-is modified by a transform function (rotate, scale etc)
-
-
-15.06.2018
-
-- the selection marker when moving is now a semitransparent Polygon
-with a blue border
-- rectified a small typo in the ToolTip for Excellon Format for
-Diptrace excellon format; from 4:2 to 5:2
-- corrected an error that cause no Gcode could be saved
-
-
-14.06.2018
-
-- more work on the contextual menu
-- added Draw context menu
-- added a new tool that bring together all the transformations, named
-Transformation Tool (Rotate, Skew, Scale, Offset, Flip)
-- added shorcut key 'Q' which toggle the units between IN and MM
-- remade the Move tool, there is now a selection box to show where the
-move is done
-- remade the Measurement tool, there is now a line between the start
-point of measurement and the end point of the measurement.
-- renamed most of the system variables that have a global app effect to
-global_name where name is the parameter (variable)
-
-
-9.06.2018
-
-- reverted to PyQt4. PyQt5 require too much software rewrite
-- added calculators: units_calculator and V-shape Tool calculator
-- solved bug in Join Excellon
-- added right click menu over canvas
-
-6.06.2018 Update
-
-- fixed bug: G-Code could not be saved
-- fixed bug: double clicking a category in Project Tab made the app to
-crash
-- remade the bounds() function to work with nested lists of objects as
-per advice from JP which made the operation less performance taxing.
-- added shortcut Shift+R that is complement to 'R'
-- shorcuts 'R' and 'SHIFT+R' are working now in steps of 90 degrees
-instead of previous 45 degrees.
-- added filters in the open ... FlatCAM projects are saved automatically
-as *.flat, the Gerber files have few categories. So the Excellons and
-G-Code and SVG.
-
-6.06.2018
-
-- remade the transform functions (rotate, flip, skew) so they are now
-working for joined objects, too
-- modified the Skew and Rotate comamands: if they are applied over a
-selection of objects than the origin point will be the center of the
-biggest bounding box. That allow for perfect sync between the selected
-objects
-- started to modify the program so the exceptions are handled correctly
-- solved bug where a crash occur when ObjCollection.setData didn't
-return a bool value
-- work in progress for handling situations when a different file is
-loaded as another (like loading a Gerber file using Open Excellon
- commands.
-- added filters on open_gerber and open_excellon Dialogs. There is still
-the ability to select All Files but this should reduce the cases when
-the user is trying to oprn a file from a wrong place.
-
-4.06.2018
-
-- finished PyQt4 to PyQt4 port on the Vispy variant (there were some changes
-compared with the Matplotlib version for which the port was finished
-some time ago)
-- added Ctrl+S shortcut for the Geometry Editor. When is activated it will
-save de geometry ("update") and return to the main App.
-- modified the Mirror command for the case when multiple objects are
-selected and we want to mirror all together. In this case they should mirror
-around a bounding box to fill all.
-
-3.06.2018
-
-- removed the current drill path optimizations as they are inefficient
-- implemented Google OR-tools drill path optimization in 2 flavors;
-Basic OR-tools TSP algorithm and OR-Tools Metaheuristics Guided Local Path
-- Move tool is moved to Menu -> Edit under the name Move Object
-
-- solved some internal bugs (info command was creating an non-fatal
-error in PyQt, regarding using QPixMaps outside GUI thread
-- reworked camlib number parsing (still had some bugs)
-- working in porting the application from usage of PyQt4 to PyQt4
-- added TclCommands save_sys and list_sys. save_sys is saving all the
-system default parameters and list_sys is listing them by the first
-letters. listsys with no arguments will list all the system parameters.
-
-29.05.2018
-
-- modified the labels for the X,Y and Dx,Dy coordinates
-- modified the menu entries, added more icons
-- added initial work on a Excellon Editor
-- modified the behavior of when clicking on canvas the coordinates were
-copied to cliboard: now it is required to press CTRL key for this to
-happen, and it will only happen just for left mouse button click
-- removed the autocopy of the object name on new object creation
-- remade the Tcl commands drillcncjob and cncjob
-- added fix so the canvas is focused on the start of the program,
-therefore the shortcuts work without the need for doing first a click
-on canvas.
-
-
-
-28.05.2018
-
-- added total drill count column in Excellon Tool Table which displays the
-total number of drills
-- added aliases in panelize Tool (pan and panel should work)
-- modified generate_milling method which had issues from the Python3 port
-(it could not sort the tools due of dict to dict comparison no longer
-possible).
-- modified the 'default' postprocessor in order to include a space
-between the value of Xcoord and the following Y
-- made optional the using of threads for the milling command; by default
-it is OFF (False) because in the current configuration it creates issues
-when it is using threads
-- modified the Panelize function and Tcl command Panelize. It was having
-issues due to multithreading (kept trying to modify a dictionary in
-redraw() method)and automatically selecting the last created object
-(feature introduced by me). I've added a parameter to
-the new_object method, named autoselected (by default it is True) and
-in the panelize method I initialized it with False.
-By initializing the plot parameter with False for the temporary objects,
-I have increased dramatically the  generation speed of the panel because
-now the temporary object are no longer ploted which consumed time.
-- replaced log.warn() with log.warning() in camlib.py. Reason: deprecated
-- fixed the issue that the "Defaults" button was having no effect when
-clicked and Options Combo was in Project Options
-- fixed issue with Tcl Shell loosing focus after each command, therefore
-needing to click in the edit line before we type a new command (borrowed
-from @brainstorm
-- added a header in the postprocessor files mentioning that the GCODE
-files were generated by FlatCAM.
-- modified the number of decimals in some of the line entries to 4.
-- added an alias for the millholes Tcl Command: 'mill'
-
-27.04.2018
-
-- modified the Gerber.scale() function from camlib.py in order to
-allow loading Gerber files with 'follow' parameter in other units
-than the current ones
-- snap_max_entry is disabled when the DRAW toolbar is disabled (previous
-fix didn't work)
-- added drill count column in Excellon Tool Table which displays the
-total number of drills for each tool
-
-- added a new menu entry in Menu -> EDIT named "Join Excellon". It will
-merge a selection of Excellon files into a new Excellon file
-- added menu stubs for other Excellon based actions
-
-- solved bug that was not possible to generate film from joined geometry
-- improved toggle active/inactive of the object through SPACE key. Now
-the command works not only for one object but also for a selection
-
-26.05.2018
-
-- made conversion to Python3
-- added Rtree Indexing drill path optimization
-- added a checkbox in Options Tab -> App Defaults -> Excellon
-Group named Excellon Optim. Type from which it can be selected
-the default optimization type: TS stands for Travelling
-Salesman algorithm and Rtree stands for Rtree Indexing
-- added a checkbox on the Grid Toolbar that when checked
-(default status is checked) whatever value entered in the GridX entry
-will be used instead of the now disabled GridY entry
-- modified the default behavior on when a line_entry is clicked.
-Now, on each click on a line_entry, the content is automatically
-selected.
-- snap_max_entry is disabled when the DRAW toolbar is disabled
-
-24.05.2015
-
-- in Geometry Editor added a initial form of Rotate Geometry command in
-toolbar
-- changed the way the geometry is finished if it requires a key: before
-it was using key 'Space' now it uses 'Enter'
-- added Shortcut for Rotate Geometry to key 'Space'
-- after using a tool in Geometry Editor it automatically defaults to
-'Select Tool'
-
-23.05.2018
-
-Added key shortcut's in FlatCAMApp and in Geometry Editor.
-
-FlatCAMApp shortcut list:
-1      Zoom Fit
-2      Zoom Out
-3      Zoom In
-C      Copy Obj_Name
-E      Edit Geometry (if selected)
-G      Grid On/Off
-M      Move Obj
-
-N      New Geometry
-R      Rotate
-S      Shell Toggle
-V      View Fit
-X      Flip on X_axis
-Y      Flip on Y_axis
-~      Show Shortcut List
-
-Space:   En(Dis)able Obj Plot
-CTRL+A   Select All
-CTRL+C   Copy Obj
-CTRL+E   Open Excellon File
-CTRL+G   Open Gerber File
-CTRL+M   Measurement Tool
-CTRL+O   Open Project
-CTRL+S   Save Project As
-Delete   Delete Obj'''
-
-
-Geometry Editor Key shortcut list:
-A       Add an 'Arc'
-C       Copy Geo Item
-G       Grid Snap On/Off
-K       Corner Snap On/Off
-M       Move Geo Item
-
-N       Add an 'Polygon'
-O       Add a 'Circle'
-P       Add a 'Path'
-R       Add an 'Rectangle'
-S       Select Tool Active
-
-
-~        Show Shortcut List
-Space:   Rotate Geometry
-Enter:   Finish Current Action
-Escape:  Abort Current Action
-Delete:  Delete Obj
-
-22.05.2018
-
-- Added Marlin postprocessor
-- Added a new entry into the Geometry and Excellon Object's UI:
-Feedrate rapid: the purpose is to set a feedrate for the G0
-command that some firmwares like Marlin don't intepret as
-'move with highest speed'
-- FlatCAM was not making the conversion from one type of units to
-another for a lot of parameters. Corrected that.
-- Modified the Marlin Postprocessor so it will generate the required
-GCODE.
-
-21.05.2018
-
-- added new icons for menu entries
-- added shortcuts that work on the Project tab but also over
-Plot. Shorcut list is accesed with shortcut key '~' sau '`'
-- small GUI modification: on each "New File" command it will switch to
-the Project Tab regardless on which tab we were.
-
-- removed the global shear entries and checkbox as they can be
-damaging and it will build effect upon effect, which is not good
-- solved bug in that the Edit -> Shear on X (Y)axis could adjust
-only in integers. Now the angle can be adjusted in float with
-3 decimals.
-- changed the tile of QInputDialog to a more general one
-- changed the "follow" Tcl command to the new format
-- added a new entry in the Menu -> File, to open a Gerber with
-the follow parameter = True
-- added a new checkbox in the Gerber Object Selection Tab that
-when checked it will create a "follow" geometry
-- added a few lines in Mill Holes Tcl command to check if there are
-promises and raise an Tcl error if there are any.
-- started to modify the Export_Svg Tcl command
-
-20.05.2018
-
-- changed the interpretation of the axis for the rotate and skew commands.
-Actually I reversed them to reflect reality.
-- for the rotate command a positive angle now rotates CW. It was reversed.
-- added shortcuts (for outside CANVAS; the CANVAS has it's own set of shortcuts)
-CTRL+C will copy to clipboard the name of the selected object
-CTRL+A will Select All objects
-
-"X" key will flip the selected objects on X axis
-
-"Y" key will flip the selected objects on Y axis
-
-"R" key will rotate CW with a 45 degrees step
-- changed the layout for the top of th Options page. Added a checkbox and entries
-for parameters for skew command. When the checkbox is checked it will save (and
-load at the next startup of the program) the option that at each CNCJob generation
-(be it from Excellon or Geometry) it will perform the Skew command with the 
-parametrs set in the nearby field boxes (Skew X and Skey Y angles).
-It is useful in case the CNC router is not perfectly alligned between the X and Y axis
-
-- added some protection in case the skew command receive a None parameter
-
-- BUG solved: made an UGLY (really UGLY) HACK so now, when there is a panel geometry
-generated from GUI, the project WILL save. I had to create a copy of the generated 
-panel geometry and delete the original panel geometry. This way there is no complain
-from JSON module about circular reference.
-
-Supplimentary:
-- removed the Save buttons previously added on each Group in Application Defaults.
-Replaced them with a single Save button that stays always on top of the Options TAB
-- added settings for defaults for the Grid that are persistent
-- changed the default view at FlatCAM startup: now the origin is in the center of the screen
-
-
-19.05.2018
-
-- last object that is opened (created) is always automatically selected and
-the name of the object is automatically copied to clipboard; useful when
-using the TCL command :)
-
-- added new commands in MENU -> EDIT named: "Copy Object" and
-"Copy Obj as Geom". The first command will duplicate any object (Geometry,
-Gerber, Excellon).
-The second command will duplicate the object as a geometry. For example,
-holes in Excello now are just circles that can be "painted" if one wants it.
-
-- added new Tool named ToolFreeformCutout. It does what it says, it will
-make a board cutout from a "any shape" Gerber or Geometry file
-
-- solved bug in the TCL command "drillcncjob" that always used the endz
-parameter value as the toolchangez parameter value and for the endz value
-used a default value = 1
-
-- added postprocessor name into the TCL command "drillcncjob" parameters
-
-- when adding a new geometry the default name is now: "New_Geometry" instead
-of "New Geometry". TCL commands don't handle the spaces inside the name and
-require adding quotes.
-
-- solved bug in "cncjob" TCL command in which it used multidepth parameter as
-always True regardless of the argument provided
-
-- added a checkbox for Multidepth in the Options Tab -> Application Defaults
-
-
-18.05.2018
-
-- added an "Defaults" button in Excellon Defaults Group; it loads the
-following configuration (Excellon_format_in 2:4, Excellon_format_mm 3:3,
-Excellon_zeros LZ)
-- added Save buttons for each Defaults Group; in the future more 
-parameters will be propagated in the app, for now they are a few
-- added functions for Skew on X axis and for Skew on Y menu stubs.
-Now, clicking on those Menu -> Options -> Transform Object menu entries
-will trigger those functions
-- added a CheckBox button in the Options Tab -> Application Defaults that control
-the behaviour of the TCL shell: checking it will make the TCL shell window visible
-at each start-up, unchecking it the TCL shell window will be hidden until needed
-- Depth/pass parameter from Geometry Object CNC Job is now in the
-defaults and it will keep it's value until changed in the Application
-Defaults.
-
-17.05.2018
-
-- added messages box for the Flip commands to show error in case there
-is no object selected when the command is executed
-- added field entries in the Options TAB - > Application Defaults for the
-following newly introduced parameters: 
-excellon_format_upper_in
-excellon_format_lower_in
-excellon_format_upper_mm
-excellon_format_lower_mm
-
-The ones with upper indicate how many digits are allocated for the units
-and the ones with lower indicate how many digits from coordinates are 
-alocated for the decimals.
-
-[  Eg: Excellon format 2:4 in INCH
-   excellon_format_upper_in = 2
-   excellon_format_lower_in = 4
-where the first 2 digits are for units and the last 4 digits are
-decimals so from a number like 235589 we will get a coordinate 23.5589
-]
-
-- added Radio button in the Options TAB - > Application Defaults for the
-Excellon_zeros parameter
-
-After each change of those parameters the user will have to press 
-"Save defaults" from File menu in order to propagate the new values, or
-wait for the autosave to kick in (each 20sec).
-
-Those parameters can be set in the set_sys TCL command.
-
-15.05.2018
-- modified SetSys TCL command: now it can change units
-- modified SetSys TCL command: now it can set new parameters:
-excellon_format_mm and excellon_format_in. the first one is when the
-excellon units are MM and the second is for when the excellon units are
-in INCH. Those parameters can be set with a number between 1 and 5 and it
-signify how many digits are before coma.
-- added new GUI command in EDIT -> Select All. It will select all
-objects on the first mouse click and on the second will deselect all
-(toggle action)
-- added new GUI commands in Options -> Transform object. Added Rotate selection,
-Flip on X axis of the selection and Flip on Y axis of the selection
-For the Rotate selection command, negative numbers means rotation CCW and
-positive numbers means rotation CW.
-
-- cleaned up a bit the module imports
-- worked on the excellon parsing for the case of trailing zeros.
-If there are more than 6digits in the 
-coordinates, in case that there is no period, now the software will 
-identify the issue and attempt to correct it by dividing the coordinate 
-further by 10 for each additional digit over 6. If the number of digits
-is less than 6 then the software will multiply by 10 the coordinates
-
-14.05.2018
-
-- fixed bug in Geometry CNCJob generation that prevented generating 
-the object
-- added GRBL 1.1 postprocessor and Laser postprocessor (adapted from 
-the work of MARCO A QUEZADA)
-
-
-13.05.2018
-
-- added postprocessing in correct form
-- added the possibility to select an postprocessor for Excellon Object
-- added a new postprocessor, manual_toolchange.py. It allows to change 
-the tools and adjust the drill tip to touch the surface manually, always
-in the X=0, Y=0, Z = toolchangeZ coordinates.
-- fixed drillcncjob TCL command by adding toolchangeZ parameter
-- fixed the posprocessor file template 'default.py' in the toolchange
-command section
-- after I created a feature that the message in infobar is cleared by
-moving mouse on canvas, it generated a bug in TCL shell: everytime 
-mouse was moved it will add a space into the TCL read only section.
-Now this bug is fixed.
-- added an EndZ parameter for the drillcncjob and cncjob TCL commands: it
-will specify at what Z value to park the CNC when job ends
-- the spindle will be turned on after the toolchange and it will be turned off
-just before the final end move.
-
-Previously:
-- added GRID based working of FLATCAM
-- added Set Origin command
-- added FilmTool, PanelizeTool GUI, MoveTool
-- and others
-
-
-24.04.2018
-
-- Remade the Measurement Tool: it now ask for the Start point of the measurement and then for the Stop point. After it will display the measurement until we left click again on the canvas and so on. Previously you clicked the start point and reset the X and Y coords displayed and then you moved the mouse pointer wherever you wanted to measure, but moving the mouse from there you lost the measurement.
-- Added Relative measurement on the main plot
-- Now both the measuring tool and the relative measurement will work only with the left click of the mouse button because middle mouse click and right mouse click are used for panning
-- Renamed the tools files starting with Tool so they are grouped (in the future they may have their own folder like for TCL Commands)
-
-- Commented some shortcut keys and functions for features that are not present anymore or they are planned to be in the future but unfinished (like buffer tool, paint tool)
-- minor corrections regarding PEP8 (Pycharm complains about the m)
-- solved bug in TclCommandsSetSys.py Everytime that the command was executed it complain about the parameter not being in the list (something like this). There was a missing “else:”
-- when using the command “set_sys excellon_zeros” with parameter in lower case (either ‘l’ or ‘t’) now it is always written in the defaults file as capital letter
-
-- solved a bug introduced by me: when apertures macros were detected in Excellon file, FlatCam will complain about missing dictionary key “size”. Now it first check if the aperture is a macro and perform the check for zero value only for apertures with “size” key
-- solved a bug that didn't allowed FC to detect if Excellon file has leading zeros or trailing zeros
-- solved a bug that FC was searching for char ‘%’ that signal end of Excellon header even in commented lines (latest versions of Eagle end the commented line with a ‘%’)
-
-
-============================================
-
-This fork features:
-
-- Added buttons in the menu bar for opening of Gerber and Excellon files;
-- Reduced number of decimals for drill bits to two decimals;
-- Updated make_win32.py so it will work with cx_freeze 5.0.1 
-- Added capability so FlatCAM can now read Gerber files with traces having zero value (aperture size is zero);
-- Added Paint All / Seed based Paint functions from the JP's FlatCAM;
-- Added Excellon move optimization (travelling salesman algorithm) cherry-picked from David Kahler: https://bitbucket.org/dakahler/flatcam
-- Updated make_win32.py so it will work with cx_freeze 5.0.1 Corrected small typo in DblSidedTool.py
-- Added the TCL commands in the new format. Picked from FLATCAM master.
-- Hack to fix the issue with geometry not being updated after a TCL command was executed. Now after each TCL command the plot_all() function is executed and the canvas is refreshed.
-- Added GUI for panelization TCL command
-- Added GUI tool for the panelization TCL command: Changed some ToolTips.
-
-
-============================================
-
-Previously added features by Dennis
-
-- "Clear non-copper" feature, supporting multi-tool work.
-- Groups in Project view.
-- Pan view by dragging in visualizer window with pressed MMB.
-- OpenGL-based visualizer.
-
+You can download all the required wheels files into a folder (e.g D:\my_folder) and install them from Command Prompt like this:
+
+```
+cd D:\my_folder
+```
+
+and for each wheel file (*.whl) run:
+```
+D:\my_folder\> pip install --upgrade package_from_requirements.whl
+```
+
+Run FlatCAM beta from the installation folder (e.g D:\FlatCAM_beta) in the Command Prompt with the following command:
+cd D:\FlatCAM_beta
+python FlatCAM.py
+
+2.Linux
+
+- make sure that Python 3.8 is installed on your OS and that the command: python3 -V confirm it
+- verify that the pip package is installed for your Python installation (e.g 3.8) by running the command:
+```
+pip3 -V
+``` 
+
+If it is not installed, install it. In Ubuntu-like OS's it is done like this: 
+```
+sudo apt-get install python3-pip 
+```
+or:
+```
+sudo apt-get install python3.8-pip
+```
+- verify that the file setup_ubuntu.sh has Linux line-endings (LF) and that it is executable (chmod +x setup_ubuntu.sh)
+- run the file setup_ubuntu.sh and install all the dependencies with the command:
+```
+./setup_ubuntu.sh
+```
+- if the previous command is successful and has no errors, run FlatCAM with the command: python3 FlatCAM.py
+
+- Alternatively you can install it on Ubuntu with:
+```
+# Optional if depencencies are missing
+make install_dependencies
+
+# Install for the current user only (using the folder in its place)
+make install
+
+# System-wide instalation
+sudo make install
+```
+
+3.MacOS
+
+Instructions from here: https://gist.github.com/natevw/3e6fc929aff358b38c0a#gistcomment-3111878
+
+- create a folder to hold the sources somewhere on your HDD:
+mkdir FlatCAM
+
+- unzip in this folder the sources downloaded from https://bitbucket.org/jpcgt/flatcam/downloads/
+Using commands (e.g using the sources for FlatCAM beta 8.991):
+cd ~/FlatCAM
+wget https://bitbucket.org/jpcgt/flatcam/downloads/FlatCAM_beta_8.991_sources.zip
+unzip FlatCAM_beta_8.991_sources.zip
+cd FlatCAM_beta_8.991_sources
+
+- check if Homebrew is installed:
+xcode-select --install
+ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
+
+- install dependencies:
+brew install pyqt
+python3 -m ensurepip
+python3 -m pip install -r requirements.txt
+
+- run FlatCAM
+python3 FlatCAM.py

+ 18 - 0
Utils/remove_bad_profiles_from_pictures.py

@@ -0,0 +1,18 @@
+import os
+import subprocess
+
+
+def system_call(args, cwd="."):
+    print("Running '{}' in '{}'".format(str(args), cwd))
+    subprocess.call(args, cwd=cwd)
+    pass
+
+
+def fix_image_files(root=os.curdir):
+    for path, dirs, files in os.walk(os.path.abspath(root)):
+        # sys.stdout.write('.')
+        for dir in dirs:
+            system_call("mogrify *.png", "{}".format(os.path.join(path, dir)))
+
+
+fix_image_files(os.curdir)

+ 195 - 0
Utils/vispy_example.py

@@ -0,0 +1,195 @@
+from PyQt5.QtGui import QPalette
+from PyQt5 import QtCore, QtWidgets
+
+import vispy.scene as scene
+from vispy.scene.visuals import Rectangle, Text
+from vispy.color import Color
+
+import sys
+
+
+class VisPyCanvas(scene.SceneCanvas):
+
+    def __init__(self, config=None):
+        super().__init__(config=config, keys=None)
+
+        self.unfreeze()
+        
+        # Colors used by the Scene
+        theme_color = Color('#FFFFFF')
+        tick_color = Color('#000000')
+        back_color = str(QPalette().color(QPalette.Window).name())
+        
+        # Central Widget Colors
+        self.central_widget.bgcolor = back_color
+        self.central_widget.border_color = back_color
+
+        self.grid_widget = self.central_widget.add_grid(margin=10)
+        self.grid_widget.spacing = 0
+        
+        # TOP Padding
+        top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
+        top_padding.height_max = 0
+
+        # RIGHT Padding
+        right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2)
+        right_padding.width_max = 0
+
+        # X Axis
+        self.xaxis = scene.AxisWidget(
+            orientation='bottom', axis_color=tick_color, text_color=tick_color,
+            font_size=8, axis_width=1,
+            anchors=['center', 'bottom']
+        )
+        self.xaxis.height_max = 30
+        self.grid_widget.add_widget(self.xaxis, row=2, col=1)
+
+        # Y Axis
+        self.yaxis = scene.AxisWidget(
+            orientation='left', axis_color=tick_color, text_color=tick_color, 
+            font_size=8, axis_width=1
+        )
+        self.yaxis.width_max = 55
+        self.grid_widget.add_widget(self.yaxis, row=1, col=0)
+
+        # View & Camera
+        self.view = self.grid_widget.add_view(row=1, col=1, border_color=tick_color,
+                                              bgcolor=theme_color)
+        self.view.camera = scene.PanZoomCamera(aspect=1, rect=(-25, -25, 150, 150))
+
+        self.xaxis.link_view(self.view)
+        self.yaxis.link_view(self.view)
+
+        self.grid = scene.GridLines(parent=self.view.scene, color='dimgray')
+        self.grid.set_gl_state(depth_test=False)
+
+        self.rect = Rectangle(center=(65,30), color=Color('#0000FF10'), border_color=Color('#0000FF10'),
+                              width=120, height=50, radius=[5, 5, 5, 5], parent=self.view)
+        self.rect.set_gl_state(depth_test=False)
+
+        self.text = Text('', parent=self.view, color='black', pos=(5, 30), method='gpu', anchor_x='left')
+        self.text.font_size = 8
+        self.text.text = 'Coordinates:\nX: %s\nY: %s' % ('0.0000', '0.0000')
+
+        self.freeze()
+
+        # self.measure_fps()
+
+
+class PlotCanvas(QtCore.QObject):
+
+    def __init__(self, container, my_app):
+        """
+        The constructor configures the VisPy figure that
+        will contain all plots, creates the base axes and connects
+        events to the plotting area.
+
+        :param container: The parent container in which to draw plots.
+        :rtype: PlotCanvas
+        """
+
+        super().__init__()
+        
+        # VisPyCanvas instance
+        self.vispy_canvas = VisPyCanvas()
+        
+        self.vispy_canvas.unfreeze()
+        
+        self.my_app = my_app
+        
+        # Parent container
+        self.container = container
+        
+        # <VisPyCanvas>
+        self.vispy_canvas.create_native()
+        self.vispy_canvas.native.setParent(self.my_app.ui)
+
+        # <QtCore.QObject>
+        self.container.addWidget(self.vispy_canvas.native)
+        
+        # add two Infinite Lines to act as markers for the X,Y axis
+        self.v_line = scene.visuals.InfiniteLine(
+            pos=0, color=(0.0, 0.0, 1.0, 0.3), vertical=True, 
+            parent=self.vispy_canvas.view.scene)
+
+        self.h_line = scene.visuals.InfiniteLine(
+            pos=0, color=(0.00, 0.0, 1.0, 0.3), vertical=False, 
+            parent=self.vispy_canvas.view.scene)
+        
+        self.vispy_canvas.freeze()
+    
+    def event_connect(self, event, callback):
+        getattr(self.vispy_canvas.events, event).connect(callback)
+        
+    def event_disconnect(self, event, callback):
+        getattr(self.vispy_canvas.events, event).disconnect(callback)
+    
+    def translate_coords(self, pos):
+        """
+        Translate pixels to canvas units.
+        """
+        tr = self.vispy_canvas.grid.get_transform('canvas', 'visual')
+        return tr.map(pos)
+        
+
+class MyGui(QtWidgets.QMainWindow):
+
+    def __init__(self):
+        super().__init__()
+
+        self.setWindowTitle("VisPy Test")
+
+        # add Menubar
+        self.menu = self.menuBar()
+        self.menufile = self.menu.addMenu("File")
+        self.menuedit = self.menu.addMenu("Edit")
+        self.menufhelp = self.menu.addMenu("Help")
+
+        # add a Toolbar
+        self.file_toolbar = QtWidgets.QToolBar("File Toolbar")
+        self.addToolBar(self.file_toolbar)
+        self.button = self.file_toolbar.addAction("Open")
+
+        # add Central Widget
+        self.c_widget = QtWidgets.QWidget()
+        self.central_layout = QtWidgets.QVBoxLayout()
+        self.c_widget.setLayout(self.central_layout)
+        self.setCentralWidget(self.c_widget)
+
+        # add InfoBar
+        # self.infobar = self.statusBar()
+        # self.position_label = QtWidgets.QLabel("Position:  X: 0.0000\tY: 0.0000")
+        # self.infobar.addWidget(self.position_label)
+
+
+class MyApp(QtCore.QObject):
+
+    def __init__(self):
+        super().__init__()
+        
+        self.ui = MyGui()
+        self.plot = PlotCanvas(container=self.ui.central_layout, my_app=self)
+        
+        self.ui.show()
+        
+        self.plot.event_connect(event="mouse_move", callback=self.on_mouse_move)
+    
+    def on_mouse_move(self, event):
+        cursor_pos = event.pos
+        
+        pos_canvas = self.plot.translate_coords(cursor_pos)
+        
+        # we don't need all the info in the tuple returned by the translate_coords()
+        # only first 2 elements
+        pos_canvas = [pos_canvas[0], pos_canvas[1]]
+        self.ui.position_label.setText("Position:  X: %.4f\tY: %.4f" % (pos_canvas[0], pos_canvas[1]))
+        # pos_text = 'Coordinates:   \nX: {:<7.4f}\nY: {:<7.4f}'.format(pos_canvas[0], pos_canvas[1])
+        pos_text = 'Coordinates:   \nX: {:<.4f}\nY: {:<.4f}'.format(pos_canvas[0], pos_canvas[1])
+        self.plot.vispy_canvas.text.text = pos_text
+
+
+if __name__ == '__main__':
+    app = QtWidgets.QApplication(sys.argv)
+
+    m_app = MyApp()
+    sys.exit(app.exec_())

+ 981 - 0
appCommon/Common.py

@@ -0,0 +1,981 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+# ##########################################################
+
+# ##########################################################
+# File Modified (major mod): Marius Adrian Stanciu         #
+# Date: 11/4/2019                                          #
+# ##########################################################
+from PyQt5 import QtCore
+
+from shapely.geometry import Polygon, Point, LineString
+from shapely.ops import unary_union
+
+from appGUI.VisPyVisuals import ShapeCollection
+from appTool import AppTool
+
+from copy import deepcopy
+import collections
+
+import numpy as np
+# from voronoi import Voronoi
+# from voronoi import Polygon as voronoi_polygon
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class GracefulException(Exception):
+    """
+    Graceful Exception raised when the user is requesting to cancel the current threaded task
+    """
+
+    def __init__(self):
+        super().__init__()
+
+    def __str__(self):
+        return '\n\n%s' % _("The user requested a graceful exit of the current task.")
+
+
+class LoudDict(dict):
+    """
+    A Dictionary with a callback for item changes.
+    """
+
+    def __init__(self, *args, **kwargs):
+        dict.__init__(self, *args, **kwargs)
+        self.callback = lambda x: None
+
+    def __setitem__(self, key, value):
+        """
+        Overridden __setitem__ method. Will emit 'changed(QString)' if the item was changed, with key as parameter.
+        """
+        if key in self and self.__getitem__(key) == value:
+            return
+
+        dict.__setitem__(self, key, value)
+        self.callback(key)
+
+    def update(self, *args, **kwargs):
+        if len(args) > 1:
+            raise TypeError("update expected at most 1 arguments, got %d" % len(args))
+        other = dict(*args, **kwargs)
+        for key in other:
+            self[key] = other[key]
+
+    def set_change_callback(self, callback):
+        """
+        Assigns a function as callback on item change. The callback
+        will receive the key of the object that was changed.
+
+        :param callback: Function to call on item change.
+        :type callback: func
+        :return: None
+        """
+
+        self.callback = callback
+
+
+class LoudUniqueList(list, collections.MutableSequence):
+    """
+    A List with a callback for item changes, callback which returns the index where the items are added/modified.
+    A List that will allow adding only items that are not in the list.
+    """
+
+    def __init__(self, arg=None):
+        super().__init__()
+        self.callback = lambda x: None
+
+        if arg is not None:
+            if isinstance(arg, list):
+                self.extend(arg)
+            else:
+                self.extend([arg])
+
+    def insert(self, i, v):
+        if v in self:
+            raise ValueError("One of the added items is already in the list.")
+        self.callback(i)
+        return super().insert(i, v)
+
+    def append(self, v):
+        if v in self:
+            raise ValueError("One of the added items is already in the list.")
+        le = len(self)
+        self.callback(le)
+        return super().append(v)
+
+    def extend(self, t):
+        for v in t:
+            if v in self:
+                raise ValueError("One of the added items is already in the list.")
+        le = len(self)
+        self.callback(le)
+        return super().extend(t)
+
+    def __add__(self, t):  # This is for something like `LoudUniqueList([1, 2, 3]) + list([4, 5, 6])`...
+        for v in t:
+            if v in self:
+                raise ValueError("One of the added items is already in the list.")
+        le = len(self)
+        self.callback(le)
+        return super().__add__(t)
+
+    def __iadd__(self, t):  # This is for something like `l = LoudUniqueList(); l += [1, 2, 3]`
+        for v in t:
+            if v in self:
+                raise ValueError("One of the added items is already in the list.")
+        le = len(self)
+        self.callback(le)
+        return super().__iadd__(t)
+
+    def __setitem__(self, i, v):
+        try:
+            for v1 in v:
+                if v1 in self:
+                    raise ValueError("One of the modified items is already in the list.")
+        except TypeError:
+            if v in self:
+                raise ValueError("One of the modified items is already in the list.")
+        if v is not None:
+            self.callback(i)
+        return super().__setitem__(i, v)
+
+    def set_callback(self, callback):
+        """
+        Assigns a function as callback on item change. The callback
+        will receive the index of the object that was changed.
+
+        :param callback: Function to call on item change.
+        :type callback: func
+        :return: None
+        """
+
+        self.callback = callback
+
+
+class FCSignal:
+    """
+    Taken from here: https://blog.abstractfactory.io/dynamic-signals-in-pyqt/
+    """
+
+    def __init__(self):
+        self.__subscribers = []
+
+    def emit(self, *args, **kwargs):
+        for subs in self.__subscribers:
+            subs(*args, **kwargs)
+
+    def connect(self, func):
+        self.__subscribers.append(func)
+
+    def disconnect(self, func):
+        try:
+            self.__subscribers.remove(func)
+        except ValueError:
+            print('Warning: function %s not removed '
+                  'from signal %s' % (func, self))
+
+
+def color_variant(hex_color, bright_factor=1):
+    """
+    Takes a color in HEX format #FF00FF and produces a lighter or darker variant
+
+    :param hex_color:           color to change
+    :type hex_color:            str
+    :param bright_factor:       factor to change the color brightness [0 ... 1]
+    :type bright_factor:        float
+    :return:                    Modified color
+    :rtype:                     str
+    """
+
+    if len(hex_color) != 7:
+        print("Color is %s, but needs to be in #FF00FF format. Returning original color." % hex_color)
+        return hex_color
+
+    if bright_factor > 1.0:
+        bright_factor = 1.0
+    if bright_factor < 0.0:
+        bright_factor = 0.0
+
+    rgb_hex = [hex_color[x:x + 2] for x in [1, 3, 5]]
+    new_rgb = []
+    for hex_value in rgb_hex:
+        # adjust each color channel and turn it into a INT suitable as argument for hex()
+        mod_color = round(int(hex_value, 16) * bright_factor)
+        # make sure that each color channel has two digits without the 0x prefix
+        mod_color_hex = str(hex(mod_color)[2:]).zfill(2)
+        new_rgb.append(mod_color_hex)
+
+    return "#" + "".join([i for i in new_rgb])
+
+
+class ExclusionAreas(QtCore.QObject):
+    """
+    Functionality for adding Exclusion Areas for the Excellon and Geometry FlatCAM Objects
+    """
+    e_shape_modified = QtCore.pyqtSignal()
+
+    def __init__(self, app):
+        super().__init__()
+
+        self.app = app
+
+        self.app.log.debug("+ Adding Exclusion Areas")
+        # Storage for shapes, storage that can be used by FlatCAm tools for utility geometry
+        # VisPy visuals
+        if self.app.is_legacy is False:
+            try:
+                self.exclusion_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
+            except AttributeError:
+                self.exclusion_shapes = None
+        else:
+            from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+            self.exclusion_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name="exclusion")
+
+        # Event signals disconnect id holders
+        self.mr = None
+        self.mm = None
+        self.kp = None
+
+        # variables to be used in area exclusion
+        self.cursor_pos = (0, 0)
+        self.first_click = False
+        self.points = []
+        self.poly_drawn = False
+
+        '''
+        Here we store the exclusion shapes and some other information's
+        Each list element is a dictionary with the format:
+        
+        {
+            "obj_type":   string ("excellon" or "geometry")   <- self.obj_type
+            "shape":      Shapely polygon
+            "strategy":   string ("over" or "around")         <- self.strategy_button
+            "overz":      float                               <- self.over_z_button
+        }
+        '''
+        self.exclusion_areas_storage = []
+
+        self.mouse_is_dragging = False
+
+        self.solid_geometry = []
+        self.obj_type = None
+
+        self.shape_type_button = None
+        self.over_z_button = None
+        self.strategy_button = None
+        self.cnc_button = None
+
+    def on_add_area_click(self, shape_button, overz_button, strategy_radio, cnc_button, solid_geo, obj_type):
+        """
+
+        :param shape_button:    a FCButton that has the value for the shape
+        :param overz_button:    a FCDoubleSpinner that holds the Over Z value
+        :param strategy_radio:  a RadioSet button with the strategy_button value
+        :param cnc_button:      a FCButton in Object UI that when clicked the CNCJob is created
+                                We have a reference here so we can change the color signifying that exclusion areas are
+                                available.
+        :param solid_geo:       reference to the object solid geometry for which we add exclusion areas
+        :param obj_type:        Type of FlatCAM object that called this method. String: "excellon" or "geometry"
+        :type obj_type:         str
+        :return:                None
+        """
+        self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
+        self.app.call_source = 'geometry'
+
+        self.shape_type_button = shape_button
+
+        self.over_z_button = overz_button
+        self.strategy_button = strategy_radio
+        self.cnc_button = cnc_button
+
+        self.solid_geometry = solid_geo
+        self.obj_type = obj_type
+
+        if self.app.is_legacy is False:
+            self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        else:
+            self.app.plotcanvas.graph_event_disconnect(self.app.mp)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mm)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mr)
+
+        self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
+        self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
+        # self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
+
+    # To be called after clicking on the plot.
+    def on_mouse_release(self, event):
+        """
+        Called on mouse click release.
+
+        :param event:   Mouse event
+        :type event:
+        :return:        None
+        :rtype:
+        """
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
+
+        event_pos = self.app.plotcanvas.translate_coords(event_pos)
+        if self.app.grid_status():
+            curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+        else:
+            curr_pos = (event_pos[0], event_pos[1])
+
+        x1, y1 = curr_pos[0], curr_pos[1]
+
+        # shape_type_button = self.ui.area_shape_radio.get_value()
+
+        # do clear area only for left mouse clicks
+        if event.button == 1:
+            if self.shape_type_button.get_value() == "square":
+                if self.first_click is False:
+                    self.first_click = True
+                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the area."))
+
+                    self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
+                    if self.app.grid_status():
+                        self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
+                else:
+                    self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
+                    self.app.delete_selection_shape()
+
+                    x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
+
+                    pt1 = (x0, y0)
+                    pt2 = (x1, y0)
+                    pt3 = (x1, y1)
+                    pt4 = (x0, y1)
+
+                    new_rectangle = Polygon([pt1, pt2, pt3, pt4])
+
+                    # {
+                    #     "obj_type":   string("excellon" or "geometry") < - self.obj_type
+                    #     "shape":      Shapely polygon
+                    #     "strategy_button":   string("over" or "around") < - self.strategy_button
+                    #     "overz":      float < - self.over_z_button
+                    # }
+                    new_el = {
+                        "obj_type": self.obj_type,
+                        "shape": new_rectangle,
+                        "strategy": self.strategy_button.get_value(),
+                        "overz": self.over_z_button.get_value()
+                    }
+                    self.exclusion_areas_storage.append(new_el)
+
+                    if self.obj_type == 'excellon':
+                        color = "#FF7400"
+                        face_color = "#FF7400BF"
+                    else:
+                        color = "#098a8f"
+                        face_color = "#FF7400BF"
+
+                    # add a temporary shape on canvas
+                    AppTool.draw_tool_selection_shape(
+                        self, old_coords=(x0, y0), coords=(x1, y1),
+                        color=color,
+                        face_color=face_color,
+                        shapes_storage=self.exclusion_shapes)
+
+                    self.first_click = False
+                    return
+            else:
+                self.points.append((x1, y1))
+
+                if len(self.points) > 1:
+                    self.poly_drawn = True
+                    self.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
+
+                return ""
+        elif event.button == right_button and self.mouse_is_dragging is False:
+
+            shape_type = self.shape_type_button.get_value()
+
+            if shape_type == "square":
+                self.first_click = False
+            else:
+                # if we finish to add a polygon
+                if self.poly_drawn is True:
+                    try:
+                        # try to add the point where we last clicked if it is not already in the self.points
+                        last_pt = (x1, y1)
+                        if last_pt != self.points[-1]:
+                            self.points.append(last_pt)
+                    except IndexError:
+                        pass
+
+                    # we need to add a Polygon and a Polygon can be made only from at least 3 points
+                    if len(self.points) > 2:
+                        AppTool.delete_moving_selection_shape(self)
+                        pol = Polygon(self.points)
+                        # do not add invalid polygons even if they are drawn by utility geometry
+                        if pol.is_valid:
+                            """
+                            {
+                                "obj_type":   string("excellon" or "geometry") < - self.obj_type
+                                "shape":      Shapely polygon
+                                "strategy":   string("over" or "around") < - self.strategy_button
+                                "overz":      float < - self.over_z_button
+                            }
+                            """
+                            new_el = {
+                                "obj_type": self.obj_type,
+                                "shape": pol,
+                                "strategy": self.strategy_button.get_value(),
+                                "overz": self.over_z_button.get_value()
+                            }
+                            self.exclusion_areas_storage.append(new_el)
+
+                            if self.obj_type == 'excellon':
+                                color = "#FF7400"
+                                face_color = "#FF7400BF"
+                            else:
+                                color = "#098a8f"
+                                face_color = "#FF7400BF"
+
+                            AppTool.draw_selection_shape_polygon(
+                                self, points=self.points,
+                                color=color,
+                                face_color=face_color,
+                                shapes_storage=self.exclusion_shapes)
+                            self.app.inform.emit(
+                                _("Zone added. Click to start adding next zone or right click to finish."))
+
+                    self.points = []
+                    self.poly_drawn = False
+                    return
+
+            # AppTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
+
+            if self.app.is_legacy is False:
+                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+                self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+                # self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
+            else:
+                self.app.plotcanvas.graph_event_disconnect(self.mr)
+                self.app.plotcanvas.graph_event_disconnect(self.mm)
+                # self.app.plotcanvas.graph_event_disconnect(self.kp)
+
+            self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                                  self.app.on_mouse_click_over_plot)
+            self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                                  self.app.on_mouse_move_over_plot)
+            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                                  self.app.on_mouse_click_release_over_plot)
+
+            self.app.call_source = 'app'
+
+            if len(self.exclusion_areas_storage) == 0:
+                return
+
+            # since the exclusion areas should apply to all objects in the app collection, this check is limited to
+            # only the current object therefore it will not guarantee success
+            self.app.inform.emit("%s" % _("Exclusion areas added. Checking overlap with the object geometry ..."))
+
+            for el in self.exclusion_areas_storage:
+                if el["shape"].intersects(unary_union(self.solid_geometry)):
+                    self.on_clear_area_click()
+                    self.app.inform.emit(
+                        "[ERROR_NOTCL] %s" % _("Failed. Exclusion areas intersects the object geometry ..."))
+                    return
+
+            self.app.inform.emit("[success] %s" % _("Exclusion areas added."))
+            self.cnc_button.setStyleSheet("""
+                                    QPushButton
+                                    {
+                                        font-weight: bold;
+                                        color: orange;
+                                    }
+                                    """)
+            self.cnc_button.setToolTip(
+                '%s %s' % (_("Generate the CNC Job object."), _("With Exclusion areas."))
+            )
+
+            self.e_shape_modified.emit()
+
+    def area_disconnect(self):
+        """
+        Will do the cleanup. Will disconnect the mouse events for the custom handlers in this class and initialize
+        certain class attributes.
+
+        :return:    None
+        :rtype:
+        """
+        if self.app.is_legacy is False:
+            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
+            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
+        else:
+            self.app.plotcanvas.graph_event_disconnect(self.mr)
+            self.app.plotcanvas.graph_event_disconnect(self.mm)
+            self.app.plotcanvas.graph_event_disconnect(self.kp)
+
+        self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
+                                                              self.app.on_mouse_click_over_plot)
+        self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
+                                                              self.app.on_mouse_move_over_plot)
+        self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                              self.app.on_mouse_click_release_over_plot)
+        self.points = []
+        self.poly_drawn = False
+        self.exclusion_areas_storage = []
+
+        AppTool.delete_moving_selection_shape(self)
+        # AppTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
+
+        self.app.call_source = "app"
+        self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
+
+    def on_mouse_move(self, event):
+        """
+        Called on mouse move
+
+        :param event:   mouse event
+        :type event:
+        :return:        None
+        :rtype:
+        """
+        shape_type = self.shape_type_button.get_value()
+
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            event_is_dragging = event.is_dragging
+            # right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            event_is_dragging = self.app.plotcanvas.is_dragging
+            # right_button = 3
+
+        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
+
+        # detect mouse dragging motion
+        if event_is_dragging is True:
+            self.mouse_is_dragging = True
+        else:
+            self.mouse_is_dragging = False
+
+        # update the cursor position
+        if self.app.grid_status():
+            # Update cursor
+            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
+
+            self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
+                                         symbol='++', edge_color=self.app.cursor_color_3D,
+                                         edge_width=self.app.defaults["global_cursor_width"],
+                                         size=self.app.defaults["global_cursor_size"])
+
+        # update the positions on status bar
+        if self.cursor_pos is None:
+            self.cursor_pos = (0, 0)
+
+        self.app.dx = curr_pos[0] - float(self.cursor_pos[0])
+        self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
+        self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                           "<b>Y</b>: %.4f&nbsp;" % (curr_pos[0], curr_pos[1]))
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+
+        units = self.app.defaults["units"].lower()
+        self.app.plotcanvas.text_hud.text = \
+            'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX:  \t{:<.4f} [{:s}]\nY:  \t{:<.4f} [{:s}]'.format(
+                self.app.dx, units, self.app.dy, units, curr_pos[0], units, curr_pos[1], units)
+
+        if self.obj_type == 'excellon':
+            color = "#FF7400"
+            face_color = "#FF7400BF"
+        else:
+            color = "#098a8f"
+            face_color = "#FF7400BF"
+
+        # draw the utility geometry
+        if shape_type == "square":
+            if self.first_click:
+                self.app.delete_selection_shape()
+
+                self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
+                                                     color=color,
+                                                     face_color=face_color,
+                                                     coords=(curr_pos[0], curr_pos[1]))
+        else:
+            AppTool.delete_moving_selection_shape(self)
+            AppTool.draw_moving_selection_shape_poly(
+                self, points=self.points,
+                color=color,
+                face_color=face_color,
+                data=(curr_pos[0], curr_pos[1]))
+
+    def on_clear_area_click(self):
+        """
+        Slot for clicking the button for Deleting all the Exclusion areas.
+
+        :return:    None
+        :rtype:
+        """
+        self.clear_shapes()
+
+        # restore the default StyleSheet
+        self.cnc_button.setStyleSheet("")
+        # update the StyleSheet
+        self.cnc_button.setStyleSheet("""
+                                QPushButton
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+        self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
+
+    def clear_shapes(self):
+        """
+        Will delete all the Exclusion areas; will delete on canvas any possible selection box for the Exclusion areas.
+
+        :return:    None
+        :rtype:
+        """
+        if self.exclusion_areas_storage:
+            self.app.inform.emit('%s' % _("All exclusion zones deleted."))
+        self.exclusion_areas_storage.clear()
+        AppTool.delete_moving_selection_shape(self)
+        self.app.delete_selection_shape()
+        AppTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
+
+    def delete_sel_shapes(self, idxs):
+        """
+
+        :param idxs:    list of indexes in self.exclusion_areas_storage list to be deleted
+        :type idxs:     list
+        :return:        None
+        """
+
+        # delete all plotted shapes
+        AppTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
+
+        # delete shapes
+        for idx in sorted(idxs, reverse=True):
+            del self.exclusion_areas_storage[idx]
+
+        # re-add what's left after deletion in first step
+        if self.obj_type == 'excellon':
+            color = "#FF7400"
+            face_color = "#FF7400BF"
+        else:
+            color = "#098a8f"
+            face_color = "#FF7400BF"
+
+        face_alpha = 0.3
+        color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
+
+        for geo_el in self.exclusion_areas_storage:
+            if isinstance(geo_el['shape'], Polygon):
+                self.exclusion_shapes.add(
+                    geo_el['shape'], color=color, face_color=color_t, update=True, layer=0, tolerance=None)
+        if self.app.is_legacy is True:
+            self.exclusion_shapes.redraw()
+
+        # if there are still some exclusion areas in the storage
+        if self.exclusion_areas_storage:
+            self.app.inform.emit('[success] %s' % _("Selected exclusion zones deleted."))
+        else:
+            # restore the default StyleSheet
+            self.cnc_button.setStyleSheet("")
+            # update the StyleSheet
+            self.cnc_button.setStyleSheet("""
+                                            QPushButton
+                                            {
+                                                font-weight: bold;
+                                            }
+                                            """)
+            self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
+
+            # there are no more exclusion areas in the storage, all have been selected and deleted
+            self.app.inform.emit('%s' % _("All exclusion zones deleted."))
+
+    def travel_coordinates(self, start_point, end_point, tooldia):
+        """
+        WIll create a path the go around the exclusion areas on the shortest path when travelling (at a Z above the
+        material).
+
+        :param start_point:     X,Y coordinates for the start point of the travel line
+        :type start_point:      tuple
+        :param end_point:       X,Y coordinates for the destination point of the travel line
+        :type end_point:        tuple
+        :param tooldia:         THe tool diameter used and which generates the travel lines
+        :type tooldia           float
+        :return:                A list of x,y tuples that describe the avoiding path
+        :rtype:                 list
+        """
+
+        ret_list = []
+
+        # Travel lines: rapids. Should not pass through Exclusion areas
+        travel_line = LineString([start_point, end_point])
+        origin_point = Point(start_point)
+
+        buffered_storage = []
+        # add a little something to the half diameter, to make sure that we really don't enter in the exclusion zones
+        buffered_distance = (tooldia / 2.0) + (0.1 if self.app.defaults['units'] == 'MM' else 0.00393701)
+
+        for area in self.exclusion_areas_storage:
+            new_area = deepcopy(area)
+            new_area['shape'] = area['shape'].buffer(buffered_distance, join_style=2)
+            buffered_storage.append(new_area)
+
+        # sort the Exclusion areas from the closest to the start_point to the farthest
+        tmp = []
+        for area in buffered_storage:
+            dist = Point(start_point).distance(area['shape'])
+            tmp.append((dist, area))
+        tmp.sort(key=lambda k: k[0])
+
+        sorted_area_storage = [k[1] for k in tmp]
+
+        # process the ordered exclusion areas list
+        for area in sorted_area_storage:
+            outline = area['shape'].exterior
+            if travel_line.intersects(outline):
+                intersection_pts = travel_line.intersection(outline)
+
+                if isinstance(intersection_pts, Point):
+                    # it's just a touch, continue
+                    continue
+
+                entry_pt = nearest_point(origin_point, intersection_pts)
+                exit_pt = farthest_point(origin_point, intersection_pts)
+
+                if area['strategy'] == 'around':
+                    full_vertex_points = [Point(x) for x in list(outline.coords)]
+
+                    # the last coordinate in outline, a LinearRing, is the closing one
+                    # therefore a duplicate of the first one; discard it
+                    vertex_points = full_vertex_points[:-1]
+
+                    # dist_from_entry = [(entry_pt.distance(vt), vertex_points.index(vt)) for vt in vertex_points]
+                    # closest_point_entry = nsmallest(1, dist_from_entry, key=lambda x: x[0])
+                    # start_idx = closest_point_entry[0][1]
+                    #
+                    # dist_from_exit = [(exit_pt.distance(vt), vertex_points.index(vt)) for vt in vertex_points]
+                    # closest_point_exit = nsmallest(1, dist_from_exit, key=lambda x: x[0])
+                    # end_idx = closest_point_exit[0][1]
+
+                    # pts_line_entry = None
+                    # pts_line_exit = None
+                    # for i in range(len(full_vertex_points)):
+                    #     try:
+                    #         line = LineString(
+                    #             [
+                    #                 (full_vertex_points[i].x, full_vertex_points[i].y),
+                    #                 (full_vertex_points[i + 1].x, full_vertex_points[i + 1].y)
+                    #             ]
+                    #         )
+                    #     except IndexError:
+                    #         continue
+                    #
+                    #     if entry_pt.within(line) or entry_pt.equals(Point(line.coords[0])) or \
+                    #             entry_pt.equals(Point(line.coords[1])):
+                    #         pts_line_entry = [Point(x) for x in line.coords]
+                    #
+                    #     if exit_pt.within(line) or exit_pt.equals(Point(line.coords[0])) or \
+                    #             exit_pt.equals(Point(line.coords[1])):
+                    #         pts_line_exit = [Point(x) for x in line.coords]
+                    #
+                    # closest_point_entry = nearest_point(entry_pt, pts_line_entry)
+                    # start_idx = vertex_points.index(closest_point_entry)
+                    #
+                    # closest_point_exit = nearest_point(exit_pt, pts_line_exit)
+                    # end_idx = vertex_points.index(closest_point_exit)
+
+                    # find all vertexes for which a line from start_point does not cross the Exclusion area polygon
+                    # the same for end_point
+                    # we don't need closest points for which the path leads to crosses of the Exclusion area
+
+                    close_start_points = []
+                    close_end_points = []
+                    for i in range(len(vertex_points)):
+                        try:
+                            start_line = LineString(
+                                [
+                                    start_point,
+                                    (vertex_points[i].x, vertex_points[i].y)
+                                ]
+                            )
+                            end_line = LineString(
+                                [
+                                    end_point,
+                                    (vertex_points[i].x, vertex_points[i].y)
+                                ]
+                            )
+                        except IndexError:
+                            continue
+
+                        if not start_line.crosses(area['shape']):
+                            close_start_points.append(vertex_points[i])
+                        if not end_line.crosses(area['shape']):
+                            close_end_points.append(vertex_points[i])
+
+                    closest_point_entry = nearest_point(entry_pt, close_start_points)
+                    closest_point_exit = nearest_point(exit_pt, close_end_points)
+
+                    start_idx = vertex_points.index(closest_point_entry)
+                    end_idx = vertex_points.index(closest_point_exit)
+
+                    # calculate possible paths: one clockwise the other counterclockwise on the exterior of the
+                    # exclusion area outline (Polygon.exterior)
+                    vp_len = len(vertex_points)
+                    if end_idx > start_idx:
+                        path_1 = vertex_points[start_idx:(end_idx + 1)]
+                        path_2 = [vertex_points[start_idx]]
+                        idx = start_idx
+                        for __ in range(vp_len):
+                            idx = idx - 1 if idx > 0 else (vp_len - 1)
+                            path_2.append(vertex_points[idx])
+                            if idx == end_idx:
+                                break
+                    else:
+                        path_1 = vertex_points[end_idx:(start_idx + 1)]
+                        path_2 = [vertex_points[end_idx]]
+                        idx = end_idx
+                        for __ in range(vp_len):
+                            idx = idx - 1 if idx > 0 else (vp_len - 1)
+                            path_2.append(vertex_points[idx])
+                            if idx == start_idx:
+                                break
+                        path_1.reverse()
+                        path_2.reverse()
+
+                    # choose the one with the lesser length
+                    length_path_1 = 0
+                    for i in range(len(path_1)):
+                        try:
+                            length_path_1 += path_1[i].distance(path_1[i + 1])
+                        except IndexError:
+                            pass
+
+                    length_path_2 = 0
+                    for i in range(len(path_2)):
+                        try:
+                            length_path_2 += path_2[i].distance(path_2[i + 1])
+                        except IndexError:
+                            pass
+
+                    path = path_1 if length_path_1 < length_path_2 else path_2
+
+                    # transform the list of Points into a list of Points coordinates
+                    path_coords = [[None, (p.x, p.y)] for p in path]
+                    ret_list += path_coords
+
+                else:
+                    path_coords = [[float(area['overz']), (entry_pt.x, entry_pt.y)], [None, (exit_pt.x, exit_pt.y)]]
+                    ret_list += path_coords
+
+                # create a new LineString to test again for possible other Exclusion zones
+                last_pt_in_path = path_coords[-1][1]
+                travel_line = LineString([last_pt_in_path, end_point])
+
+        ret_list.append([None, end_point])
+        return ret_list
+
+
+def farthest_point(origin, points_list):
+    """
+    Calculate the farthest Point in a list from another Point
+
+    :param origin:      Reference Point
+    :type origin:       Point
+    :param points_list: List of Points or a MultiPoint
+    :type points_list:  list
+    :return:            Farthest Point
+    :rtype:             Point
+    """
+    old_dist = 0
+    fartherst_pt = None
+
+    for pt in points_list:
+        dist = abs(origin.distance(pt))
+        if dist >= old_dist:
+            fartherst_pt = pt
+            old_dist = dist
+
+    return fartherst_pt
+
+
+# def voronoi_diagram(geom, envelope, edges=False):
+#     """
+#
+#     :param geom:        a collection of Shapely Points from which to build the Voronoi diagram
+#     :type geom:          MultiPoint
+#     :param envelope:    a bounding box to constrain the diagram (Shapely Polygon)
+#     :type envelope:     Polygon
+#     :param edges:       If False, return regions as polygons. Else, return only
+#                         edges e.g. LineStrings.
+#     :type edges:        bool, False
+#     :return:
+#     :rtype:
+#     """
+#
+#     if not isinstance(geom, MultiPoint):
+#         return False
+#
+#     coords = list(envelope.exterior.coords)
+#     v_poly = voronoi_polygon(coords)
+#
+#     vp = Voronoi(v_poly)
+#
+#     points = []
+#     for pt in geom:
+#         points.append((pt.x, pt.y))
+#     vp.create_diagram(points=points, vis_steps=False, verbose=False, vis_result=False, vis_tree=False)
+#
+#     if edges is True:
+#         return vp.edges
+#     else:
+#         voronoi_polygons = []
+#         for pt in vp.points:
+#             try:
+#                 poly_coords = list(pt.get_coordinates())
+#                 new_poly_coords = []
+#                 for coord in poly_coords:
+#                     new_poly_coords.append((coord.x, coord.y))
+#
+#                 voronoi_polygons.append(Polygon(new_poly_coords))
+#             except Exception:
+#                 print(traceback.format_exc())
+#
+#         return voronoi_polygons
+
+def nearest_point(origin, points_list):
+    """
+    Calculate the nearest Point in a list from another Point
+
+    :param origin:      Reference Point
+    :type origin:       Point
+    :param points_list: List of Points or a MultiPoint
+    :type points_list:  list
+    :return:            Nearest Point
+    :rtype:             Point
+    """
+    old_dist = np.Inf
+    nearest_pt = None
+
+    for pt in points_list:
+        dist = abs(origin.distance(pt))
+        if dist <= old_dist:
+            nearest_pt = pt
+            old_dist = dist
+
+    return nearest_pt

+ 119 - 0
appCommon/bilinear.py

@@ -0,0 +1,119 @@
+#############################################################################
+# Copyright (c) 2013 by Panagiotis Mavrogiorgos
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+#   this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+#   this list of conditions and the following disclaimer in the documentation
+#   and/or other materials provided with the distribution.
+# * Neither the name(s) of the copyright holders nor the names of its
+#   contributors may be used to endorse or promote products derived from this
+#   software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AS IS AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+# EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#############################################################################
+#
+# @license: http://opensource.org/licenses/BSD-3-Clause
+
+from bisect import bisect_left
+import logging
+
+log = logging.getLogger('base')
+
+
+class BilinearInterpolation(object):
+    """
+    Bilinear interpolation with optional extrapolation.
+    Usage:
+    table = BilinearInterpolation(
+        x_index=(1, 2, 3),
+        y_index=(1, 2, 3),
+        values=((110, 120, 130),
+                (210, 220, 230),
+                (310, 320, 330)),
+        extrapolate=True)
+
+    assert table(1, 1) == 110
+    assert table(2.5, 2.5) == 275
+
+    """
+    def __init__(self, x_index, y_index, values):
+        # sanity check
+        x_length = len(x_index)
+        y_length = len(y_index)
+
+        if x_length < 2 or y_length < 2:
+            raise ValueError("Table must be at least 2x2.")
+        if y_length != len(values):
+            raise ValueError("Table must have equal number of rows to y_index.")
+        if any(x2 - x1 <= 0 for x1, x2 in zip(x_index, x_index[1:])):
+            raise ValueError("x_index must be in strictly ascending order!")
+        if any(y2 - y1 <= 0 for y1, y2 in zip(y_index, y_index[1:])):
+            raise ValueError("y_index must be in strictly ascending order!")
+
+        self.x_index = x_index
+        self.y_index = y_index
+        self.values = values
+        self.x_length = x_length
+        self.y_length = y_length
+        self.extrapolate = True
+
+        # slopes = self.slopes = []
+        # for j in range(y_length):
+        #     intervals = zip(x_index, x_index[1:], values[j], values[j][1:])
+        #     slopes.append([(y2 - y1) / (x2 - x1) for x1, x2, y1, y2 in intervals])
+
+    def __call__(self, x, y):
+        # local lookups
+        x_index, y_index, values = self.x_index, self.y_index, self.values
+
+        i = bisect_left(x_index, x) - 1
+        j = bisect_left(y_index, y) - 1
+
+        if self.extrapolate:
+            # fix x index
+            if i == -1:
+                x_slice = slice(None, 2)
+            elif i == self.x_length - 1:
+                x_slice = slice(-2, None)
+            else:
+                x_slice = slice(i, i + 2)
+
+            # fix y index
+            if j == -1:
+                j = 0
+                y_slice = slice(None, 2)
+            elif j == self.y_length - 1:
+                j = -2
+                y_slice = slice(-2, None)
+            else:
+                y_slice = slice(j, j + 2)
+        else:
+            if i == -1 or i == self.x_length - 1:
+                raise ValueError("Extrapolation not allowed!")
+            if j == -1 or j == self.y_length - 1:
+                raise ValueError("Extrapolation not allowed!")
+
+        # if the extrapolations is False this will fail
+        x1, x2 = x_index[x_slice]
+        y1, y2 = y_index[y_slice]
+        z11, z12 = values[j][x_slice]
+        z21, z22 = values[j + 1][x_slice]
+
+        return (z11 * (x2 - x) * (y2 - y) +
+                z21 * (x - x1) * (y2 - y) +
+                z12 * (x2 - x) * (y - y1) +
+                z22 * (x - x1) * (y - y1)) / ((x2 - x1) * (y2 - y1))

+ 126 - 0
appCommon/bilinearInterpolator.py

@@ -0,0 +1,126 @@
+# import csv
+import math
+import numpy as np
+
+
+class bilinearInterpolator:
+    """
+    This class takes a collection of 3-dimensional points from a .csv file.  
+    It contains a bilinear interpolator to find unknown points within the grid.
+    """
+    @property
+    def probedGrid(self):
+        return self._probedGrid
+
+    """
+    Constructor takes a file with a .csv extension and creates an evenly-spaced 'ideal' grid from the data points.
+    This is done to get around any floating point errors that may exist in the data
+    """
+    def __init__(self, pointsFile):
+        
+        self.pointsFile = pointsFile
+        self.points = np.loadtxt(self.pointsFile, delimiter=',')
+
+        self.xMin, self.xMax, self.xSpacing, self.xCount = self._axisParams(0)
+        self.yMin, self.yMax, self.ySpacing, self.yCount = self._axisParams(1)
+
+        # generate ideal grid to match actually probed points -- this is due to floating-point error issues
+        idealGrid = ([
+            [(x, y) for x in np.linspace(self.xMin, self.xMax, self.xCount, True)]
+            for y in np.linspace(self.yMin, self.yMax, self.yCount, True)
+            ])
+
+        self._probedGrid = [[0] * self.yCount for i in range(0, self.xCount)]
+
+        # align ideal grid indices with probed data points
+        for rowIndex, row in enumerate(idealGrid):
+            for colIndex, idealPoint in enumerate(row):
+                minSqDist = math.inf
+                for probed in self.points:
+                    # find closest point in ideal grid that corresponds to actual tested point
+                    # put z value in correct index
+                    sqDist = pow(probed[0] - idealPoint[0], 2) + pow(probed[1] - idealPoint[1], 2)
+                    if sqDist <= minSqDist:
+                        minSqDist = sqDist
+                        indexX = rowIndex
+                        indexY = colIndex
+                        closestProbed = probed
+                self.probedGrid[indexY][indexX] = closestProbed
+
+    def Interpolate(self, point):
+        """
+        Bilinear interpolation method to determine unknown z-values within grid of known z-values.
+
+        NOTE: If one axis is outside the grid, linear interpolation is used instead.
+        If both axes are outside of the grid, the z-value of the closest corner of the grid is returned.
+        """
+        lin = False
+
+        if point[0] < self.xMin:
+            ix1 = 0
+            lin = True
+        elif point[0] > self.xMax:
+            ix1 = self.xCount-1
+            lin = True
+        else:
+            ix1 = math.floor((point[0] - self.xMin)/self.xSpacing)
+            ix2 = math.ceil((point[0] - self.xMin)/self.xSpacing)
+
+        def interpolatePoint(p1, p2, pt, axis):
+            return (p2[2]*(pt[axis] - p1[axis]) + p1[2]*(p2[axis] - pt[axis]))/(p2[axis] - p1[axis])
+
+        if point[1] < self.yMin:
+            if lin:
+                return self.probedGrid[ix1][0][2]
+            return interpolatePoint(self.probedGrid[ix1][0], self.probedGrid[ix2][0], point, 0)
+        elif point[1] > self.yMax:           
+            if lin:
+                return self.probedGrid[ix1][self.yCount - 1][2]
+            return interpolatePoint(
+                self.probedGrid[ix1][self.yCount - 1], self.probedGrid[ix2][self.yCount - 1], point, 0)
+        else:
+            iy1 = math.floor((point[1] - self.yMin)/self.ySpacing)
+            iy2 = math.ceil((point[1] - self.yMin)/self.ySpacing)
+            # if x was at an extrema, but y was not, perform linear interpolation on x axis
+            if lin:
+                return interpolatePoint(self.probedGrid[ix1][iy1], self.probedGrid[ix1][iy2], point, 1)
+
+        def specialDiv(a, b):
+            if b == 0:
+                return 0.5
+            else:
+                return a/b      
+
+        x1 = self.probedGrid[ix1][iy1][0]
+        x2 = self.probedGrid[ix2][iy1][0]
+        y1 = self.probedGrid[ix2][iy1][1]
+        y2 = self.probedGrid[ix2][iy2][1]
+
+        Q11 = self.probedGrid[ix1][iy1][2]
+        Q12 = self.probedGrid[ix1][iy2][2]
+        Q21 = self.probedGrid[ix2][iy1][2]
+        Q22 = self.probedGrid[ix2][iy2][2]
+
+        r1 = specialDiv(point[0]-x1, x2-x1)*Q21 + specialDiv(x2-point[0], x2-x1)*Q11
+        r2 = specialDiv(point[0]-x1, x2-x1)*Q22 + specialDiv(x2-point[0], x2-x1)*Q12
+        p = specialDiv(point[1]-y1, y2-y1)*r2 + specialDiv(y2-point[1], y2-y1)*r1
+            
+        return p
+
+    # Returns the min, max, spacing and size of one axis of the 2D grid
+    def _axisParams(self, sortAxis):
+        # sort the set and eliminate the previous, unsorted set
+        srtSet = sorted(self.points, key=lambda x: x[sortAxis])
+
+        dists = []
+        for item0, item1 in zip(srtSet[:(len(srtSet)-2)], srtSet[1:]):
+            dists.append(float(item1[sortAxis]) - float(item0[sortAxis]))
+        axisSpacing = max(dists)
+
+        # add an extra one for axisCount to account for the starting point
+        axisMin = float(min(srtSet, key=lambda x: x[sortAxis])[sortAxis])
+        axisMax = float(max(srtSet, key=lambda x: x[sortAxis])[sortAxis])
+        axisRange = axisMax - axisMin
+        axisCount = round((axisRange/axisSpacing) + 1)
+
+        return axisMin, axisMax, axisSpacing, axisCount

+ 3554 - 0
appDatabase.py

@@ -0,0 +1,3554 @@
+from PyQt5 import QtGui, QtCore, QtWidgets
+from appGUI.GUIElements import FCEntry, FCButton, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, \
+    FCTree, RadioSet, FCFileSaveDialog, FCLabel, FCComboBox2
+from camlib import to_dict
+
+import sys
+import json
+
+from copy import deepcopy
+from datetime import datetime
+import math
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class ToolsDB2UI:
+    
+    def __init__(self, app, grid_layout):
+        self.app = app
+        self.decimals = self.app.decimals
+
+        self.offset_item_options = ["Path", "In", "Out", "Custom"]
+        self.type_item_options = ['Iso', 'Rough', 'Finish']
+        self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
+
+        settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if settings.contains("machinist"):
+            self.machinist_setting = settings.value('machinist', type=int)
+        else:
+            self.machinist_setting = 0
+
+        self.g_lay = grid_layout
+
+        tree_layout = QtWidgets.QVBoxLayout()
+        self.g_lay.addLayout(tree_layout, 0, 0)
+
+        self.tree_widget = FCTree(columns=2, header_hidden=False, protected_column=[0])
+        self.tree_widget.setHeaderLabels([_("ID"), _("Tool Name")])
+        self.tree_widget.setIndentation(0)
+        self.tree_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+        self.tree_widget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+
+        # set alternating colors
+        # self.tree_widget.setAlternatingRowColors(True)
+        # p = QtGui.QPalette()
+        # p.setColor(QtGui.QPalette.AlternateBase, QtGui.QColor(226, 237, 253) )
+        # self.tree_widget.setPalette(p)
+
+        self.tree_widget.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
+        tree_layout.addWidget(self.tree_widget)
+
+        param_hlay = QtWidgets.QHBoxLayout()
+        param_area = QtWidgets.QScrollArea()
+        param_widget = QtWidgets.QWidget()
+        param_widget.setLayout(param_hlay)
+
+        param_area.setWidget(param_widget)
+        param_area.setWidgetResizable(True)
+
+        self.g_lay.addWidget(param_area, 0, 1)
+
+        # ###########################################################################
+        # ############## The UI form ################################################
+        # ###########################################################################
+
+        # Tool description box
+        self.tool_description_box = QtWidgets.QGroupBox()
+        self.tool_description_box.setStyleSheet("""
+        QGroupBox
+        {
+            font-size: 16px;
+            font-weight: bold;
+        }
+        """)
+        self.description_vlay = QtWidgets.QVBoxLayout()
+        self.tool_description_box.setTitle(_("Tool Description"))
+        self.tool_description_box.setMinimumWidth(250)
+
+        # Milling box
+        self.milling_box = QtWidgets.QGroupBox()
+        self.milling_box.setStyleSheet("""
+        QGroupBox
+        {
+            font-size: 16px;
+            font-weight: bold;
+        }
+        """)
+        self.milling_vlay = QtWidgets.QVBoxLayout()
+        self.milling_box.setTitle(_("Milling Parameters"))
+        self.milling_box.setMinimumWidth(250)
+
+        # NCC TOOL BOX
+        self.ncc_box = QtWidgets.QGroupBox()
+        self.ncc_box.setStyleSheet("""
+                        QGroupBox
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.ncc_vlay = QtWidgets.QVBoxLayout()
+        self.ncc_box.setTitle(_("NCC Parameters"))
+        self.ncc_box.setMinimumWidth(250)
+
+        # PAINT TOOL BOX
+        self.paint_box = QtWidgets.QGroupBox()
+        self.paint_box.setStyleSheet("""
+                        QGroupBox
+                        {
+                            font-size: 16px;
+                            font-weight: bold;
+                        }
+                        """)
+        self.paint_vlay = QtWidgets.QVBoxLayout()
+        self.paint_box.setTitle(_("Paint Parameters"))
+        self.paint_box.setMinimumWidth(250)
+
+        # ISOLATION TOOL BOX
+        self.iso_box = QtWidgets.QGroupBox()
+        self.iso_box.setStyleSheet("""
+                     QGroupBox
+                     {
+                         font-size: 16px;
+                         font-weight: bold;
+                     }
+                     """)
+        self.iso_vlay = QtWidgets.QVBoxLayout()
+        self.iso_box.setTitle(_("Isolation Parameters"))
+        self.iso_box.setMinimumWidth(250)
+
+        # DRILLING TOOL BOX
+        self.drill_box = QtWidgets.QGroupBox()
+        self.drill_box.setStyleSheet("""
+                     QGroupBox
+                     {
+                         font-size: 16px;
+                         font-weight: bold;
+                     }
+                     """)
+        self.drill_vlay = QtWidgets.QVBoxLayout()
+        self.drill_box.setTitle(_("Drilling Parameters"))
+        self.drill_box.setMinimumWidth(250)
+
+        # CUTOUT TOOL BOX
+        self.cutout_box = QtWidgets.QGroupBox()
+        self.cutout_box.setStyleSheet("""
+                     QGroupBox
+                     {
+                         font-size: 16px;
+                         font-weight: bold;
+                     }
+                     """)
+        self.cutout_vlay = QtWidgets.QVBoxLayout()
+        self.cutout_box.setTitle(_("Cutout Parameters"))
+        self.cutout_box.setMinimumWidth(250)
+
+        # Layout Constructor
+        self.tool_description_box.setLayout(self.description_vlay)
+        self.milling_box.setLayout(self.milling_vlay)
+        self.ncc_box.setLayout(self.ncc_vlay)
+        self.paint_box.setLayout(self.paint_vlay)
+        self.iso_box.setLayout(self.iso_vlay)
+        self.drill_box.setLayout(self.drill_vlay)
+        self.cutout_box.setLayout(self.cutout_vlay)
+
+        tools_vlay = QtWidgets.QVBoxLayout()
+        tools_vlay.addWidget(self.iso_box)
+        tools_vlay.addWidget(self.paint_box)
+        tools_vlay.addWidget(self.ncc_box)
+        tools_vlay.addWidget(self.cutout_box)
+        tools_vlay.addStretch()
+
+        descript_vlay = QtWidgets.QVBoxLayout()
+        descript_vlay.addWidget(self.tool_description_box)
+        descript_vlay.addLayout(tools_vlay)
+        descript_vlay.addStretch()
+
+        mill_vlay = QtWidgets.QVBoxLayout()
+        mill_vlay.addWidget(self.milling_box)
+        mill_vlay.addStretch()
+
+        drilling_vlay = QtWidgets.QVBoxLayout()
+        drilling_vlay.addWidget(self.drill_box)
+
+        param_hlay.addLayout(descript_vlay)
+        param_hlay.addLayout(drilling_vlay)
+        param_hlay.addLayout(tools_vlay)
+
+        # always visible, always to be included last
+        param_hlay.addLayout(mill_vlay)
+
+        param_hlay.addStretch()
+
+        # ###########################################################################
+        # ################ Tool UI form #############################################
+        # ###########################################################################
+        self.grid_tool = QtWidgets.QGridLayout()
+        self.description_vlay.addLayout(self.grid_tool)
+        self.grid_tool.setColumnStretch(0, 0)
+        self.grid_tool.setColumnStretch(1, 1)
+        self.description_vlay.addStretch()
+
+        # Tool Name
+        self.name_label = FCLabel('<span style="color:red;"><b>%s:</b></span>' % _('Name'))
+        self.name_label.setToolTip(
+            _("Tool name.\n"
+              "This is not used in the app, it's function\n"
+              "is to serve as a note for the user."))
+
+        self.name_entry = FCEntry()
+        self.name_entry.setObjectName('gdb_name')
+
+        self.grid_tool.addWidget(self.name_label, 0, 0)
+        self.grid_tool.addWidget(self.name_entry, 0, 1)
+
+        # Tool Dia
+        self.dia_label = FCLabel('%s:' % _('Diameter'))
+        self.dia_label.setToolTip(
+            '%s.' % _("Tool Diameter"))
+
+        self.dia_entry = FCDoubleSpinner()
+        self.dia_entry.set_range(-10000.0000, 10000.0000)
+        self.dia_entry.set_precision(self.decimals)
+        self.dia_entry.setObjectName('gdb_dia')
+
+        self.grid_tool.addWidget(self.dia_label, 1, 0)
+        self.grid_tool.addWidget(self.dia_entry, 1, 1)
+
+        # Tool Tolerance
+        self.tol_label = FCLabel("<b>%s:</b>" % _("Diameter Tolerance"))
+        self.tol_label.setToolTip(
+            _("Tool tolerance. This tool will be used if the desired tool diameter\n"
+              "is within the tolerance specified here.")
+        )
+        self.grid_tool.addWidget(self.tol_label, 2, 0, 1, 2)
+
+        # Tolerance Min Limit
+        self.min_limit_label = FCLabel('%s:' % _("Min"))
+        self.min_limit_label.setToolTip(
+            _("Set the tool tolerance minimum.")
+        )
+        self.tol_min_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.tol_min_entry.set_precision(self.decimals)
+        self.tol_min_entry.set_range(0, 10000.0000)
+        self.tol_min_entry.setSingleStep(0.1)
+        self.tol_min_entry.setObjectName("gdb_tol_min")
+
+        self.grid_tool.addWidget(self.min_limit_label, 4, 0)
+        self.grid_tool.addWidget(self.tol_min_entry, 4, 1)
+
+        # Tolerance Min Limit
+        self.max_limit_label = FCLabel('%s:' % _("Max"))
+        self.max_limit_label.setToolTip(
+            _("Set the tool tolerance maximum.")
+        )
+        self.tol_max_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.tol_max_entry.set_precision(self.decimals)
+        self.tol_max_entry.set_range(0, 10000.0000)
+        self.tol_max_entry.setSingleStep(0.1)
+        self.tol_max_entry.setObjectName("gdb_tol_max")
+
+        self.grid_tool.addWidget(self.max_limit_label, 6, 0)
+        self.grid_tool.addWidget(self.tol_max_entry, 6, 1)
+
+        # Tool Object Type
+        self.tool_op_label = FCLabel('<b>%s:</b>' % _('Operation'))
+        self.tool_op_label.setToolTip(
+            _("The kind of Application Tool where this tool is to be used."))
+
+        self.tool_op_combo = FCComboBox2()
+        self.tool_op_combo.addItems(
+            [_("General"), _("Milling"), _("Drilling"), _('Isolation'), _('Paint'), _('NCC'), _('Cutout')])
+        self.tool_op_combo.setObjectName('gdb_tool_target')
+
+        self.grid_tool.addWidget(self.tool_op_label, 8, 0)
+        self.grid_tool.addWidget(self.tool_op_combo, 8, 1)
+
+        # ###########################################################################
+        # ############### MILLING UI form ###########################################
+        # ###########################################################################
+        self.grid0 = QtWidgets.QGridLayout()
+        self.milling_vlay.addLayout(self.grid0)
+        self.grid0.setColumnStretch(0, 0)
+        self.grid0.setColumnStretch(1, 1)
+        self.milling_vlay.addStretch()
+
+        # Tool Shape
+        self.shape_label = FCLabel('%s:' % _('Shape'))
+        self.shape_label.setToolTip(
+            _("Tool Shape. \n"
+              "Can be:\n"
+              "C1 ... C4 = circular tool with x flutes\n"
+              "B = ball tip milling tool\n"
+              "V = v-shape milling tool"))
+
+        self.mill_shape_combo = FCComboBox()
+        self.mill_shape_combo.addItems(self.tool_type_item_options)
+        self.mill_shape_combo.setObjectName('gdb_shape')
+
+        self.grid0.addWidget(self.shape_label, 2, 0)
+        self.grid0.addWidget(self.mill_shape_combo, 2, 1)
+
+        # V-Dia
+        self.vdia_label = FCLabel('%s:' % _("V-Dia"))
+        self.vdia_label.setToolTip(
+            _("V-Dia.\n"
+              "Diameter of the tip for V-Shape Tools."))
+
+        self.mill_vdia_entry = FCDoubleSpinner()
+        self.mill_vdia_entry.set_range(0.0000, 10000.0000)
+        self.mill_vdia_entry.set_precision(self.decimals)
+        self.mill_vdia_entry.setObjectName('gdb_vdia')
+
+        self.grid0.addWidget(self.vdia_label, 4, 0)
+        self.grid0.addWidget(self.mill_vdia_entry, 4, 1)
+
+        # V-Angle
+        self.vangle_label = FCLabel('%s:' % _("V-Angle"))
+        self.vangle_label.setToolTip(
+            _("V-Agle.\n"
+              "Angle at the tip for the V-Shape Tools."))
+
+        self.mill_vangle_entry = FCDoubleSpinner()
+        self.mill_vangle_entry.set_range(-360.0, 360.0)
+        self.mill_vangle_entry.set_precision(self.decimals)
+        self.mill_vangle_entry.setObjectName('gdb_vangle')
+
+        self.grid0.addWidget(self.vangle_label, 6, 0)
+        self.grid0.addWidget(self.mill_vangle_entry, 6, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid0.addWidget(separator_line, 8, 0, 1, 2)
+
+        # Tool Type
+        self.type_label = FCLabel('%s:' % _("Tool Type"))
+        self.type_label.setToolTip(
+            _("Tool Type.\n"
+              "Can be:\n"
+              "Iso = isolation cut\n"
+              "Rough = rough cut, low feedrate, multiple passes\n"
+              "Finish = finishing cut, high feedrate"))
+
+        self.mill_type_combo = FCComboBox()
+        self.mill_type_combo.addItems(self.type_item_options)
+        self.mill_type_combo.setObjectName('gdb_type')
+
+        self.grid0.addWidget(self.type_label, 10, 0)
+        self.grid0.addWidget(self.mill_type_combo, 10, 1)
+
+        # Tool Offset
+        self.tooloffset_label = FCLabel('%s:' % _('Tool Offset'))
+        self.tooloffset_label.setToolTip(
+            _("Tool Offset.\n"
+              "Can be of a few types:\n"
+              "Path = zero offset\n"
+              "In = offset inside by half of tool diameter\n"
+              "Out = offset outside by half of tool diameter\n"
+              "Custom = custom offset using the Custom Offset value"))
+
+        self.mill_tooloffset_combo = FCComboBox()
+        self.mill_tooloffset_combo.addItems(self.offset_item_options)
+        self.mill_tooloffset_combo.setObjectName('gdb_tool_offset')
+
+        self.grid0.addWidget(self.tooloffset_label, 12, 0)
+        self.grid0.addWidget(self.mill_tooloffset_combo, 12, 1)
+
+        # Custom Offset
+        self.custom_offset_label = FCLabel('%s:' % _("Custom Offset"))
+        self.custom_offset_label.setToolTip(
+            _("Custom Offset.\n"
+              "A value to be used as offset from the current path."))
+
+        self.mill_custom_offset_entry = FCDoubleSpinner()
+        self.mill_custom_offset_entry.set_range(-10000.0000, 10000.0000)
+        self.mill_custom_offset_entry.set_precision(self.decimals)
+        self.mill_custom_offset_entry.setObjectName('gdb_custom_offset')
+
+        self.grid0.addWidget(self.custom_offset_label, 14, 0)
+        self.grid0.addWidget(self.mill_custom_offset_entry, 14, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid0.addWidget(separator_line, 16, 0, 1, 2)
+
+        # Cut Z
+        self.cutz_label = FCLabel('%s:' % _("Cut Z"))
+        self.cutz_label.setToolTip(
+            _("Cutting Depth.\n"
+              "The depth at which to cut into material."))
+
+        self.mill_cutz_entry = FCDoubleSpinner()
+        self.mill_cutz_entry.set_range(-10000.0000, 10000.0000)
+        self.mill_cutz_entry.set_precision(self.decimals)
+        self.mill_cutz_entry.setObjectName('gdb_cutz')
+
+        self.grid0.addWidget(self.cutz_label, 18, 0)
+        self.grid0.addWidget(self.mill_cutz_entry, 18, 1)
+
+        # Multi Depth
+        self.multidepth_label = FCLabel('%s:' % _("MultiDepth"))
+        self.multidepth_label.setToolTip(
+            _("Multi Depth.\n"
+              "Selecting this will allow cutting in multiple passes,\n"
+              "each pass adding a DPP parameter depth."))
+
+        self.mill_multidepth_cb = FCCheckBox()
+        self.mill_multidepth_cb.setObjectName('gdb_multidepth')
+
+        self.grid0.addWidget(self.multidepth_label, 20, 0)
+        self.grid0.addWidget(self.mill_multidepth_cb, 20, 1)
+
+        # Depth Per Pass
+        self.dpp_label = FCLabel('%s:' % _("DPP"))
+        self.dpp_label.setToolTip(
+            _("DPP. Depth per Pass.\n"
+              "The value used to cut into material on each pass."))
+
+        self.mill_multidepth_entry = FCDoubleSpinner()
+        self.mill_multidepth_entry.set_range(-10000.0000, 10000.0000)
+        self.mill_multidepth_entry.set_precision(self.decimals)
+        self.mill_multidepth_entry.setObjectName('gdb_multidepth_entry')
+
+        self.grid0.addWidget(self.dpp_label, 22, 0)
+        self.grid0.addWidget(self.mill_multidepth_entry, 22, 1)
+
+        # Travel Z
+        self.travelz_label = FCLabel('%s:' % _("Travel Z"))
+        self.travelz_label.setToolTip(
+            _("Clearance Height.\n"
+              "Height at which the milling bit will travel between cuts,\n"
+              "above the surface of the material, avoiding all fixtures."))
+
+        self.mill_travelz_entry = FCDoubleSpinner()
+        self.mill_travelz_entry.set_range(-10000.0000, 10000.0000)
+        self.mill_travelz_entry.set_precision(self.decimals)
+        self.mill_travelz_entry.setObjectName('gdb_travelz')
+
+        self.grid0.addWidget(self.travelz_label, 24, 0)
+        self.grid0.addWidget(self.mill_travelz_entry, 24, 1)
+
+        # Extra Cut
+        self.ecut_label = FCLabel('%s:' % _("ExtraCut"))
+        self.ecut_label.setToolTip(
+            _("Extra Cut.\n"
+              "If checked, after a isolation is finished an extra cut\n"
+              "will be added where the start and end of isolation meet\n"
+              "such as that this point is covered by this extra cut to\n"
+              "ensure a complete isolation."))
+
+        self.mill_ecut_cb = FCCheckBox()
+        self.mill_ecut_cb.setObjectName('gdb_ecut')
+
+        self.grid0.addWidget(self.ecut_label, 26, 0)
+        self.grid0.addWidget(self.mill_ecut_cb, 26, 1)
+
+        # Extra Cut Length
+        self.ecut_length_label = FCLabel('%s:' % _("E-Cut Length"))
+        self.ecut_length_label.setToolTip(
+            _("Extra Cut length.\n"
+              "If checked, after a isolation is finished an extra cut\n"
+              "will be added where the start and end of isolation meet\n"
+              "such as that this point is covered by this extra cut to\n"
+              "ensure a complete isolation. This is the length of\n"
+              "the extra cut."))
+
+        self.mill_ecut_length_entry = FCDoubleSpinner()
+        self.mill_ecut_length_entry.set_range(0.0000, 10000.0000)
+        self.mill_ecut_length_entry.set_precision(self.decimals)
+        self.mill_ecut_length_entry.setObjectName('gdb_ecut_length')
+
+        self.grid0.addWidget(self.ecut_length_label, 28, 0)
+        self.grid0.addWidget(self.mill_ecut_length_entry, 28, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid0.addWidget(separator_line, 30, 0, 1, 2)
+
+        # Feedrate X-Y
+        self.frxy_label = FCLabel('%s:' % _("Feedrate X-Y"))
+        self.frxy_label.setToolTip(
+            _("Feedrate X-Y. Feedrate\n"
+              "The speed on XY plane used while cutting into material."))
+
+        self.mill_frxy_entry = FCDoubleSpinner()
+        self.mill_frxy_entry.set_range(-9910000.0000, 9910000.0000)
+        self.mill_frxy_entry.set_precision(self.decimals)
+        self.mill_frxy_entry.setObjectName('gdb_frxy')
+
+        self.grid0.addWidget(self.frxy_label, 32, 0)
+        self.grid0.addWidget(self.mill_frxy_entry, 32, 1)
+
+        # Feedrate Z
+        self.frz_label = FCLabel('%s:' % _("Feedrate Z"))
+        self.frz_label.setToolTip(
+            _("Feedrate Z\n"
+              "The speed on Z plane."))
+
+        self.mill_frz_entry = FCDoubleSpinner()
+        self.mill_frz_entry.set_range(-9910000.0000, 9910000.0000)
+        self.mill_frz_entry.set_precision(self.decimals)
+        self.mill_frz_entry.setObjectName('gdb_frz')
+
+        self.grid0.addWidget(self.frz_label, 34, 0)
+        self.grid0.addWidget(self.mill_frz_entry, 34, 1)
+
+        # Feedrate Rapids
+        self.frapids_label = FCLabel('%s:' % _("FR Rapids"))
+        self.frapids_label.setToolTip(
+            _("FR Rapids. Feedrate Rapids\n"
+              "Speed used while moving as fast as possible.\n"
+              "This is used only by some devices that can't use\n"
+              "the G0 g-code command. Mostly 3D printers."))
+
+        self.mill_frapids_entry = FCDoubleSpinner()
+        self.mill_frapids_entry.set_range(0.0000, 10000.0000)
+        self.mill_frapids_entry.set_precision(self.decimals)
+        self.mill_frapids_entry.setObjectName('gdb_frapids')
+
+        self.grid0.addWidget(self.frapids_label, 36, 0)
+        self.grid0.addWidget(self.mill_frapids_entry, 36, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid0.addWidget(separator_line, 38, 0, 1, 2)
+
+        # Spindle Spped
+        self.spindle_label = FCLabel('%s:' % _("Spindle Speed"))
+        self.spindle_label.setToolTip(
+            _("Spindle Speed.\n"
+              "If it's left empty it will not be used.\n"
+              "The speed of the spindle in RPM."))
+
+        self.mill_spindle_entry = FCDoubleSpinner()
+        self.mill_spindle_entry.set_range(-9910000.0000, 9910000.0000)
+        self.mill_spindle_entry.set_precision(self.decimals)
+        self.mill_spindle_entry.setObjectName('gdb_spindle')
+
+        self.grid0.addWidget(self.spindle_label, 40, 0)
+        self.grid0.addWidget(self.mill_spindle_entry, 40, 1)
+
+        # Dwell
+        self.dwell_label = FCLabel('%s:' % _("Dwell"))
+        self.dwell_label.setToolTip(
+            _("Dwell.\n"
+              "Check this if a delay is needed to allow\n"
+              "the spindle motor to reach its set speed."))
+
+        self.mill_dwell_cb = FCCheckBox()
+        self.mill_dwell_cb.setObjectName('gdb_dwell')
+
+        self.grid0.addWidget(self.dwell_label, 42, 0)
+        self.grid0.addWidget(self.mill_dwell_cb, 42, 1)
+
+        # Dwell Time
+        self.dwelltime_label = FCLabel('%s:' % _("Dwelltime"))
+        self.dwelltime_label.setToolTip(
+            _("Dwell Time.\n"
+              "A delay used to allow the motor spindle reach its set speed."))
+
+        self.mill_dwelltime_entry = FCDoubleSpinner()
+        self.mill_dwelltime_entry.set_range(0.0000, 10000.0000)
+        self.mill_dwelltime_entry.set_precision(self.decimals)
+        self.mill_dwelltime_entry.setObjectName('gdb_dwelltime')
+
+        self.grid0.addWidget(self.dwelltime_label, 44, 0)
+        self.grid0.addWidget(self.mill_dwelltime_entry, 44, 1)
+
+        # ###########################################################################
+        # ############### NCC UI form ###############################################
+        # ###########################################################################
+
+        self.grid2 = QtWidgets.QGridLayout()
+        self.ncc_vlay.addLayout(self.grid2)
+        self.grid2.setColumnStretch(0, 0)
+        self.grid2.setColumnStretch(1, 1)
+        self.ncc_vlay.addStretch()
+
+        # Operation
+        op_label = FCLabel('%s:' % _('Operation'))
+        op_label.setToolTip(
+            _("The 'Operation' can be:\n"
+              "- Isolation -> will ensure that the non-copper clearing is always complete.\n"
+              "If it's not successful then the non-copper clearing will fail, too.\n"
+              "- Clear -> the regular non-copper clearing.")
+        )
+
+        self.ncc_op_radio = RadioSet([
+            {"label": _("Clear"), "value": "clear"},
+            {"label": _("Isolation"), "value": "iso"}
+        ], orientation='horizontal', stretch=False)
+        self.ncc_op_radio.setObjectName("gdb_n_operation")
+
+        self.grid2.addWidget(op_label, 13, 0)
+        self.grid2.addWidget(self.ncc_op_radio, 13, 1)
+
+        # Milling Type Radio Button
+        self.milling_type_label = FCLabel('%s:' % _('Milling Type'))
+        self.milling_type_label.setToolTip(
+            _("Milling type:\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+
+        self.ncc_milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
+                                                {'label': _('Conventional'), 'value': 'cv'}])
+        self.ncc_milling_type_radio.setToolTip(
+            _("Milling type:\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+        self.ncc_milling_type_radio.setObjectName("gdb_n_milling_type")
+
+        self.grid2.addWidget(self.milling_type_label, 14, 0)
+        self.grid2.addWidget(self.ncc_milling_type_radio, 14, 1)
+
+        # Overlap Entry
+        nccoverlabel = FCLabel('%s:' % _('Overlap'))
+        nccoverlabel.setToolTip(
+            _("How much (percentage) of the tool width to overlap each tool pass.\n"
+              "Adjust the value starting with lower values\n"
+              "and increasing it if areas that should be processed are still \n"
+              "not processed.\n"
+              "Lower values = faster processing, faster execution on CNC.\n"
+              "Higher values = slow processing and slow execution on CNC\n"
+              "due of too many paths.")
+        )
+        self.ncc_overlap_entry = FCDoubleSpinner(suffix='%')
+        self.ncc_overlap_entry.set_precision(self.decimals)
+        self.ncc_overlap_entry.setWrapping(True)
+        self.ncc_overlap_entry.setRange(0.000, 99.9999)
+        self.ncc_overlap_entry.setSingleStep(0.1)
+        self.ncc_overlap_entry.setObjectName("gdb_n_overlap")
+
+        self.grid2.addWidget(nccoverlabel, 15, 0)
+        self.grid2.addWidget(self.ncc_overlap_entry, 15, 1)
+
+        # Margin
+        nccmarginlabel = FCLabel('%s:' % _('Margin'))
+        nccmarginlabel.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.ncc_margin_entry = FCDoubleSpinner()
+        self.ncc_margin_entry.set_precision(self.decimals)
+        self.ncc_margin_entry.set_range(-10000.0000, 10000.0000)
+        self.ncc_margin_entry.setObjectName("gdb_n_margin")
+
+        self.grid2.addWidget(nccmarginlabel, 16, 0)
+        self.grid2.addWidget(self.ncc_margin_entry, 16, 1)
+
+        # Method
+        methodlabel = FCLabel('%s:' % _('Method'))
+        methodlabel.setToolTip(
+            _("Algorithm for copper clearing:\n"
+              "- Standard: Fixed step inwards.\n"
+              "- Seed-based: Outwards from seed.\n"
+              "- Line-based: Parallel lines.")
+        )
+
+        self.ncc_method_combo = FCComboBox2()
+        self.ncc_method_combo.addItems(
+            [_("Standard"), _("Seed"), _("Lines"), _("Combo")]
+        )
+        self.ncc_method_combo.setObjectName("gdb_n_method")
+
+        self.grid2.addWidget(methodlabel, 17, 0)
+        self.grid2.addWidget(self.ncc_method_combo, 17, 1)
+
+        # Connect lines
+        self.ncc_connect_cb = FCCheckBox('%s' % _("Connect"))
+        self.ncc_connect_cb.setObjectName("gdb_n_connect")
+
+        self.ncc_connect_cb.setToolTip(
+            _("Draw lines between resulting\n"
+              "segments to minimize tool lifts.")
+        )
+        self.grid2.addWidget(self.ncc_connect_cb, 18, 0)
+
+        # Contour
+        self.ncc_contour_cb = FCCheckBox('%s' % _("Contour"))
+        self.ncc_contour_cb.setObjectName("gdb_n_contour")
+
+        self.ncc_contour_cb.setToolTip(
+            _("Cut around the perimeter of the polygon\n"
+              "to trim rough edges.")
+        )
+        self.grid2.addWidget(self.ncc_contour_cb, 18, 1)
+
+        # ## NCC Offset choice
+        self.ncc_choice_offset_cb = FCCheckBox('%s' % _("Offset"))
+        self.ncc_choice_offset_cb.setObjectName("gdb_n_offset")
+
+        self.ncc_choice_offset_cb.setToolTip(
+            _("If used, it will add an offset to the copper features.\n"
+              "The copper clearing will finish to a distance\n"
+              "from the copper features.")
+        )
+        self.grid2.addWidget(self.ncc_choice_offset_cb, 19, 0)
+
+        # ## NCC Offset Entry
+        self.ncc_offset_spinner = FCDoubleSpinner()
+        self.ncc_offset_spinner.set_range(0.00, 10.00)
+        self.ncc_offset_spinner.set_precision(4)
+        self.ncc_offset_spinner.setWrapping(True)
+        self.ncc_offset_spinner.setObjectName("gdb_n_offset_value")
+
+        units = self.app.defaults['units'].upper()
+        if units == 'MM':
+            self.ncc_offset_spinner.setSingleStep(0.1)
+        else:
+            self.ncc_offset_spinner.setSingleStep(0.01)
+
+        self.grid2.addWidget(self.ncc_offset_spinner, 19, 1)
+
+        # ###########################################################################
+        # ############### Paint UI form #############################################
+        # ###########################################################################
+
+        self.grid3 = QtWidgets.QGridLayout()
+        self.paint_vlay.addLayout(self.grid3)
+        self.grid3.setColumnStretch(0, 0)
+        self.grid3.setColumnStretch(1, 1)
+        self.paint_vlay.addStretch()
+
+        # Overlap
+        ovlabel = FCLabel('%s:' % _('Overlap'))
+        ovlabel.setToolTip(
+            _("How much (percentage) of the tool width to overlap each tool pass.\n"
+              "Adjust the value starting with lower values\n"
+              "and increasing it if areas that should be processed are still \n"
+              "not processed.\n"
+              "Lower values = faster processing, faster execution on CNC.\n"
+              "Higher values = slow processing and slow execution on CNC\n"
+              "due of too many paths.")
+        )
+        self.paint_overlap_entry = FCDoubleSpinner(suffix='%')
+        self.paint_overlap_entry.set_precision(3)
+        self.paint_overlap_entry.setWrapping(True)
+        self.paint_overlap_entry.setRange(0.0000, 99.9999)
+        self.paint_overlap_entry.setSingleStep(0.1)
+        self.paint_overlap_entry.setObjectName('gdb_p_overlap')
+
+        self.grid3.addWidget(ovlabel, 1, 0)
+        self.grid3.addWidget(self.paint_overlap_entry, 1, 1)
+
+        # Margin
+        marginlabel = FCLabel('%s:' % _('Offset'))
+        marginlabel.setToolTip(
+            _("Distance by which to avoid\n"
+              "the edges of the polygon to\n"
+              "be painted.")
+        )
+        self.paint_offset_entry = FCDoubleSpinner()
+        self.paint_offset_entry.set_precision(self.decimals)
+        self.paint_offset_entry.set_range(-10000.0000, 10000.0000)
+        self.paint_offset_entry.setObjectName('gdb_p_offset')
+
+        self.grid3.addWidget(marginlabel, 2, 0)
+        self.grid3.addWidget(self.paint_offset_entry, 2, 1)
+
+        # Method
+        methodlabel = FCLabel('%s:' % _('Method'))
+        methodlabel.setToolTip(
+            _("Algorithm for painting:\n"
+              "- Standard: Fixed step inwards.\n"
+              "- Seed-based: Outwards from seed.\n"
+              "- Line-based: Parallel lines.\n"
+              "- Laser-lines: Active only for Gerber objects.\n"
+              "Will create lines that follow the traces.\n"
+              "- Combo: In case of failure a new method will be picked from the above\n"
+              "in the order specified.")
+        )
+
+        self.paint_method_combo = FCComboBox2()
+        self.paint_method_combo.addItems(
+            [_("Standard"), _("Seed"), _("Lines"), _("Laser_lines"), _("Combo")]
+        )
+        idx = self.paint_method_combo.findText(_("Laser_lines"))
+        self.paint_method_combo.model().item(idx).setEnabled(False)
+
+        self.paint_method_combo.setObjectName('gdb_p_method')
+
+        self.grid3.addWidget(methodlabel, 7, 0)
+        self.grid3.addWidget(self.paint_method_combo, 7, 1)
+
+        # Connect lines
+        self.path_connect_cb = FCCheckBox('%s' % _("Connect"))
+        self.path_connect_cb.setObjectName('gdb_p_connect')
+        self.path_connect_cb.setToolTip(
+            _("Draw lines between resulting\n"
+              "segments to minimize tool lifts.")
+        )
+
+        self.paint_contour_cb = FCCheckBox('%s' % _("Contour"))
+        self.paint_contour_cb.setObjectName('gdb_p_contour')
+        self.paint_contour_cb.setToolTip(
+            _("Cut around the perimeter of the polygon\n"
+              "to trim rough edges.")
+        )
+
+        self.grid3.addWidget(self.path_connect_cb, 10, 0)
+        self.grid3.addWidget(self.paint_contour_cb, 10, 1)
+
+        # ###########################################################################
+        # ############### Isolation UI form #########################################
+        # ###########################################################################
+
+        self.grid4 = QtWidgets.QGridLayout()
+        self.iso_vlay.addLayout(self.grid4)
+        self.grid4.setColumnStretch(0, 0)
+        self.grid4.setColumnStretch(1, 1)
+        self.iso_vlay.addStretch()
+
+        # Passes
+        passlabel = FCLabel('%s:' % _('Passes'))
+        passlabel.setToolTip(
+            _("Width of the isolation gap in\n"
+              "number (integer) of tool widths.")
+        )
+        self.iso_passes_entry = FCSpinner()
+        self.iso_passes_entry.set_range(1, 999)
+        self.iso_passes_entry.setObjectName("gdb_i_passes")
+
+        self.grid4.addWidget(passlabel, 0, 0)
+        self.grid4.addWidget(self.iso_passes_entry, 0, 1)
+
+        # Overlap Entry
+        overlabel = FCLabel('%s:' % _('Overlap'))
+        overlabel.setToolTip(
+            _("How much (percentage) of the tool width to overlap each tool pass.")
+        )
+        self.iso_overlap_entry = FCDoubleSpinner(suffix='%')
+        self.iso_overlap_entry.set_precision(self.decimals)
+        self.iso_overlap_entry.setWrapping(True)
+        self.iso_overlap_entry.set_range(0.0000, 99.9999)
+        self.iso_overlap_entry.setSingleStep(0.1)
+        self.iso_overlap_entry.setObjectName("gdb_i_overlap")
+
+        self.grid4.addWidget(overlabel, 2, 0)
+        self.grid4.addWidget(self.iso_overlap_entry, 2, 1)
+
+        # Milling Type Radio Button
+        self.iso_milling_type_label = FCLabel('%s:' % _('Milling Type'))
+        self.iso_milling_type_label.setToolTip(
+            _("Milling type:\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+
+        self.iso_milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
+                                                {'label': _('Conventional'), 'value': 'cv'}])
+        self.iso_milling_type_radio.setToolTip(
+            _("Milling type:\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+        self.iso_milling_type_radio.setObjectName("gdb_i_milling_type")
+
+        self.grid4.addWidget(self.iso_milling_type_label, 4, 0)
+        self.grid4.addWidget(self.iso_milling_type_radio, 4, 1)
+
+        # Follow
+        self.follow_label = FCLabel('%s:' % _('Follow'))
+        self.follow_label.setToolTip(
+            _("Generate a 'Follow' geometry.\n"
+              "This means that it will cut through\n"
+              "the middle of the trace.")
+        )
+
+        self.iso_follow_cb = FCCheckBox()
+        self.iso_follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
+                                        "This means that it will cut through\n"
+                                        "the middle of the trace."))
+        self.iso_follow_cb.setObjectName("gdb_i_follow")
+
+        self.grid4.addWidget(self.follow_label, 6, 0)
+        self.grid4.addWidget(self.iso_follow_cb, 6, 1)
+
+        # Isolation Type
+        self.iso_type_label = FCLabel('%s:' % _('Isolation Type'))
+        self.iso_type_label.setToolTip(
+            _("Choose how the isolation will be executed:\n"
+              "- 'Full' -> complete isolation of polygons\n"
+              "- 'Ext' -> will isolate only on the outside\n"
+              "- 'Int' -> will isolate only on the inside\n"
+              "'Exterior' isolation is almost always possible\n"
+              "(with the right tool) but 'Interior'\n"
+              "isolation can be done only when there is an opening\n"
+              "inside of the polygon (e.g polygon is a 'doughnut' shape).")
+        )
+        self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'},
+                                        {'label': _('Ext'), 'value': 'ext'},
+                                        {'label': _('Int'), 'value': 'int'}])
+        self.iso_type_radio.setObjectName("gdb_i_iso_type")
+
+        self.grid4.addWidget(self.iso_type_label, 8, 0)
+        self.grid4.addWidget(self.iso_type_radio, 8, 1)
+
+        # ###########################################################################
+        # ################ DRILLING UI form #########################################
+        # ###########################################################################
+        self.grid5 = QtWidgets.QGridLayout()
+        self.drill_vlay.addLayout(self.grid5)
+        self.grid5.setColumnStretch(0, 0)
+        self.grid5.setColumnStretch(1, 1)
+        self.drill_vlay.addStretch()
+
+        # Cut Z
+        self.cutzlabel = FCLabel('%s:' % _('Cut Z'))
+        self.cutzlabel.setToolTip(
+            _("Drill depth (negative)\n"
+              "below the copper surface.")
+        )
+
+        self.drill_cutz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.drill_cutz_entry.set_precision(self.decimals)
+
+        if self.machinist_setting == 0:
+            self.drill_cutz_entry.set_range(-10000.0000, 0.0000)
+        else:
+            self.drill_cutz_entry.set_range(-10000.0000, 10000.0000)
+
+        self.drill_cutz_entry.setSingleStep(0.1)
+        self.drill_cutz_entry.setObjectName("gdb_e_cutz")
+
+        self.grid5.addWidget(self.cutzlabel, 4, 0)
+        self.grid5.addWidget(self.drill_cutz_entry, 4, 1)
+
+        # Tool Offset
+        self.tool_offset_label = FCLabel('%s:' % _('Offset Z'))
+        self.tool_offset_label.setToolTip(
+            _("Some drill bits (the larger ones) need to drill deeper\n"
+              "to create the desired exit hole diameter due of the tip shape.\n"
+              "The value here can compensate the Cut Z parameter.")
+        )
+
+        self.drill_offset_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.drill_offset_entry.set_precision(self.decimals)
+        self.drill_offset_entry.set_range(-10000.0000, 10000.0000)
+        self.drill_offset_entry.setObjectName("gdb_e_offset")
+
+        self.grid5.addWidget(self.tool_offset_label, 6, 0)
+        self.grid5.addWidget(self.drill_offset_entry, 6, 1)
+
+        # Multi-Depth
+        self.multidepth_drill_label = FCLabel('%s:' % _("MultiDepth"))
+        self.multidepth_drill_label.setToolTip(
+            _(
+                "Use multiple passes to limit\n"
+                "the cut depth in each pass. Will\n"
+                "cut multiple times until Cut Z is\n"
+                "reached."
+            )
+        )
+        self.drill_mpass_cb = FCCheckBox()
+        self.drill_mpass_cb.setObjectName("gdb_e_multidepth")
+
+        self.grid5.addWidget(self.multidepth_drill_label, 7, 0)
+        self.grid5.addWidget(self.drill_mpass_cb, 7, 1)
+
+        # Depth Per Pass
+        self.dpp_drill_label = FCLabel('%s:' % _("DPP"))
+        self.dpp_drill_label.setToolTip(
+            _("DPP. Depth per Pass.\n"
+              "The value used to cut into material on each pass."))
+        self.drill_maxdepth_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.drill_maxdepth_entry.set_precision(self.decimals)
+        self.drill_maxdepth_entry.set_range(0, 10000.0000)
+        self.drill_maxdepth_entry.setSingleStep(0.1)
+
+        self.drill_maxdepth_entry.setToolTip(_("Depth of each pass (positive)."))
+        self.drill_maxdepth_entry.setObjectName("gdb_e_depthperpass")
+
+        self.grid5.addWidget(self.dpp_drill_label, 8, 0)
+        self.grid5.addWidget(self.drill_maxdepth_entry, 8, 1)
+
+        # Travel Z (z_move)
+        self.travelzlabel = FCLabel('%s:' % _('Travel Z'))
+        self.travelzlabel.setToolTip(
+            _("Tool height when travelling\n"
+              "across the XY plane.")
+        )
+
+        self.drill_travelz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.drill_travelz_entry.set_precision(self.decimals)
+
+        if self.machinist_setting == 0:
+            self.drill_travelz_entry.set_range(0.00001, 10000.0000)
+        else:
+            self.drill_travelz_entry.set_range(-10000.0000, 10000.0000)
+
+        self.drill_travelz_entry.setSingleStep(0.1)
+        self.drill_travelz_entry.setObjectName("gdb_e_travelz")
+
+        self.grid5.addWidget(self.travelzlabel, 10, 0)
+        self.grid5.addWidget(self.drill_travelz_entry, 10, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid5.addWidget(separator_line, 12, 0, 1, 2)
+
+        # Excellon Feedrate Z
+        self.frzlabel = FCLabel('%s:' % _('Feedrate Z'))
+        self.frzlabel.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "So called 'Plunge' feedrate.\n"
+              "This is for linear move G01.")
+        )
+        self.drill_feedrate_z_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.drill_feedrate_z_entry.set_precision(self.decimals)
+        self.drill_feedrate_z_entry.set_range(0.0, 910000.0000)
+        self.drill_feedrate_z_entry.setSingleStep(0.1)
+        self.drill_feedrate_z_entry.setObjectName("gdb_e_feedratez")
+
+        self.grid5.addWidget(self.frzlabel, 14, 0)
+        self.grid5.addWidget(self.drill_feedrate_z_entry, 14, 1)
+
+        # Excellon Rapid Feedrate
+        self.feedrate_rapid_label = FCLabel('%s:' % _('Feedrate Rapids'))
+        self.feedrate_rapid_label.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "This is for the rapid move G00.\n"
+              "It is useful only for Marlin,\n"
+              "ignore for any other cases.")
+        )
+        self.drill_feedrate_rapid_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.drill_feedrate_rapid_entry.set_precision(self.decimals)
+        self.drill_feedrate_rapid_entry.set_range(0.0, 910000.0000)
+        self.drill_feedrate_rapid_entry.setSingleStep(0.1)
+        self.drill_feedrate_rapid_entry.setObjectName("gdb_e_fr_rapid")
+
+        self.grid5.addWidget(self.feedrate_rapid_label, 16, 0)
+        self.grid5.addWidget(self.drill_feedrate_rapid_entry, 16, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid5.addWidget(separator_line, 18, 0, 1, 2)
+
+        # Spindlespeed
+        self.spindle_label = FCLabel('%s:' % _('Spindle speed'))
+        self.spindle_label.setToolTip(
+            _("Speed of the spindle\n"
+              "in RPM (optional)")
+        )
+
+        self.drill_spindlespeed_entry = FCSpinner(callback=self.confirmation_message_int)
+        self.drill_spindlespeed_entry.set_range(0, 1000000)
+        self.drill_spindlespeed_entry.set_step(100)
+        self.drill_spindlespeed_entry.setObjectName("gdb_e_spindlespeed")
+
+        self.grid5.addWidget(self.spindle_label, 20, 0)
+        self.grid5.addWidget(self.drill_spindlespeed_entry, 20, 1)
+
+        # Dwell
+        self.dwell_drill_label = FCLabel('%s:' % _("Dwell"))
+        self.dwell_drill_label.setToolTip(
+            _("Dwell.\n"
+              "Check this if a delay is needed to allow\n"
+              "the spindle motor to reach its set speed."))
+
+        self.drill_dwell_cb = FCCheckBox()
+        self.drill_dwell_cb.setObjectName("gdb_e_dwell")
+
+        self.grid5.addWidget(self.dwell_drill_label, 21, 0)
+        self.grid5.addWidget(self.drill_dwell_cb, 21, 1)
+
+        # Dwelltime
+        self.dwelltime_drill_lbl = FCLabel('%s:' % _('Dwelltime'))
+        self.dwelltime_drill_lbl.setToolTip(
+            _("Dwell Time.\n"
+              "A delay used to allow the motor spindle reach its set speed."))
+        self.drill_dwelltime_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.drill_dwelltime_entry.set_precision(self.decimals)
+        self.drill_dwelltime_entry.set_range(0.0, 10000.0000)
+        self.drill_dwelltime_entry.setSingleStep(0.1)
+        self.drill_dwelltime_entry.setObjectName("gdb_e_dwelltime")
+
+        self.grid5.addWidget(self.dwelltime_drill_lbl, 22, 0)
+        self.grid5.addWidget(self.drill_dwelltime_entry, 22, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid5.addWidget(separator_line, 24, 0, 1, 2)
+
+        # Drill slots
+        self.drill_slots_drill_lbl = FCLabel('%s:' % _('Drill slots'))
+        self.drill_slots_drill_lbl.setToolTip(
+            _("If the selected tool has slots then they will be drilled.")
+        )
+        self.drill_slots_cb = FCCheckBox()
+        self.drill_slots_cb.setObjectName("gdb_e_drill_slots")
+
+        self.grid5.addWidget(self.drill_slots_drill_lbl, 26, 0,)
+        self.grid5.addWidget(self.drill_slots_cb, 26, 1)
+
+        # Drill Overlap
+        self.drill_overlap_label = FCLabel('%s:' % _('Overlap'))
+        self.drill_overlap_label.setToolTip(
+            _("How much (percentage) of the tool diameter to overlap previous drill hole.")
+        )
+
+        self.drill_overlap_entry = FCDoubleSpinner(suffix='%', callback=self.confirmation_message)
+        self.drill_overlap_entry.set_precision(self.decimals)
+        self.drill_overlap_entry.set_range(0.0, 100.0000)
+        self.drill_overlap_entry.setSingleStep(0.1)
+
+        self.drill_overlap_entry.setObjectName("gdb_e_drill_slots_over")
+
+        self.grid5.addWidget(self.drill_overlap_label, 28, 0)
+        self.grid5.addWidget(self.drill_overlap_entry, 28, 1)
+
+        # Last drill in slot
+        self.last_drill_drill_lbl = FCLabel('%s:' % _('Last drill'))
+        self.last_drill_drill_lbl.setToolTip(
+            _("If the slot length is not completely covered by drill holes,\n"
+              "add a drill hole on the slot end point.")
+        )
+
+        self.drill_last_drill_cb = FCCheckBox()
+        self.drill_last_drill_cb.setObjectName("gdb_e_drill_last_drill")
+
+        self.grid5.addWidget(self.last_drill_drill_lbl, 30, 0, 1, 2)
+        self.grid5.addWidget(self.drill_last_drill_cb, 30, 1)
+
+        # ###########################################################################
+        # ################### Cutout UI form ########################################
+        # ###########################################################################
+        self.grid6 = QtWidgets.QGridLayout()
+        self.cutout_vlay.addLayout(self.grid6)
+        self.grid6.setColumnStretch(0, 0)
+        self.grid6.setColumnStretch(1, 1)
+        self.cutout_vlay.addStretch()
+
+        # Margin
+        self.cutout_margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.cutout_margin_entry.set_range(-10000.0000, 10000.0000)
+        self.cutout_margin_entry.setSingleStep(0.1)
+        self.cutout_margin_entry.set_precision(self.decimals)
+        self.cutout_margin_entry.setObjectName('gdb_ct_margin')
+
+        self.cutout_margin_label = FCLabel('%s:' % _("Margin"))
+        self.cutout_margin_label.setToolTip(
+            _("Margin over bounds. A positive value here\n"
+              "will make the cutout of the PCB further from\n"
+              "the actual PCB border")
+        )
+        self.grid6.addWidget(self.cutout_margin_label, 11, 0)
+        self.grid6.addWidget(self.cutout_margin_entry, 11, 1)
+
+        # Gapsize
+        self.cutout_gapsize = FCDoubleSpinner(callback=self.confirmation_message)
+        self.cutout_gapsize.set_precision(self.decimals)
+        self.cutout_gapsize.setObjectName('gdb_ct_gapsize')
+
+        self.cutout_gapsize_label = FCLabel('%s:' % _("Gap size"))
+        self.cutout_gapsize_label.setToolTip(
+            _("The size of the bridge gaps in the cutout\n"
+              "used to keep the board connected to\n"
+              "the surrounding material (the one \n"
+              "from which the PCB is cutout).")
+        )
+        self.grid6.addWidget(self.cutout_gapsize_label, 13, 0)
+        self.grid6.addWidget(self.cutout_gapsize, 13, 1)
+
+        # Gap Type
+        self.gaptype_label = FCLabel('%s:' % _("Gap type"))
+        self.gaptype_label.setToolTip(
+            _("The type of gap:\n"
+              "- Bridge -> the cutout will be interrupted by bridges\n"
+              "- Thin -> same as 'bridge' but it will be thinner by partially milling the gap\n"
+              "- M-Bites -> 'Mouse Bites' - same as 'bridge' but covered with drill holes")
+        )
+
+        self.cutout_gaptype_radio = RadioSet(
+            [
+                {'label': _('Bridge'), 'value': 'b'},
+                {'label': _('Thin'), 'value': 'bt'},
+                {'label': "M-Bites", 'value': 'mb'}
+            ],
+            stretch=True
+        )
+        self.cutout_gaptype_radio.setObjectName('gdb_ct_gap_type')
+
+        self.grid6.addWidget(self.gaptype_label, 15, 0)
+        self.grid6.addWidget(self.cutout_gaptype_radio, 15, 1)
+
+        # Thin gaps Depth
+        self.thin_depth_label = FCLabel('%s:' % _("Depth"))
+        self.thin_depth_label.setToolTip(
+            _("The depth until the milling is done\n"
+              "in order to thin the gaps.")
+        )
+        self.cutout_thin_depth_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.cutout_thin_depth_entry.set_precision(self.decimals)
+        self.cutout_thin_depth_entry.setObjectName('gdb_ct_gap_depth')
+
+        if self.machinist_setting == 0:
+            self.cutout_thin_depth_entry.setRange(-10000.0000, -0.00001)
+        else:
+            self.cutout_thin_depth_entry.setRange(-10000.0000, 10000.0000)
+        self.cutout_thin_depth_entry.setSingleStep(0.1)
+
+        self.grid6.addWidget(self.thin_depth_label, 17, 0)
+        self.grid6.addWidget(self.cutout_thin_depth_entry, 17, 1)
+
+        # Mouse Bites Tool Diameter
+        self.mb_dia_label = FCLabel('%s:' % _("Tool Diameter"))
+        self.mb_dia_label.setToolTip(
+            _("The drill hole diameter when doing mouse bites.")
+        )
+        self.cutout_mb_dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.cutout_mb_dia_entry.set_precision(self.decimals)
+        self.cutout_mb_dia_entry.setRange(0, 100.0000)
+        self.cutout_mb_dia_entry.setObjectName('gdb_ct_mb_dia')
+
+        self.grid6.addWidget(self.mb_dia_label, 19, 0)
+        self.grid6.addWidget(self.cutout_mb_dia_entry, 19, 1)
+
+        # Mouse Bites Holes Spacing
+        self.mb_spacing_label = FCLabel('%s:' % _("Spacing"))
+        self.mb_spacing_label.setToolTip(
+            _("The spacing between drill holes when doing mouse bites.")
+        )
+        self.cutout_mb_spacing_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.cutout_mb_spacing_entry.set_precision(self.decimals)
+        self.cutout_mb_spacing_entry.setRange(0, 100.0000)
+        self.cutout_mb_spacing_entry.setObjectName('gdb_ct_mb_spacing')
+
+        self.grid6.addWidget(self.mb_spacing_label, 21, 0)
+        self.grid6.addWidget(self.cutout_mb_spacing_entry, 21, 1)
+        
+        # How gaps wil be rendered:
+        # lr    - left + right
+        # tb    - top + bottom
+        # 4     - left + right +top + bottom
+        # 2lr   - 2*left + 2*right
+        # 2tb   - 2*top + 2*bottom
+        # 8     - 2*left + 2*right +2*top + 2*bottom
+
+        # Surrounding convex box shape
+        self.cutout_convex_box = FCCheckBox('%s' % _("Convex Shape"))
+        # self.convex_box_label = FCLabel('%s' % _("Convex Sh."))
+        self.cutout_convex_box.setToolTip(
+            _("Create a convex shape surrounding the entire PCB.\n"
+              "Used only if the source object type is Gerber.")
+        )
+        self.cutout_convex_box.setObjectName('gdb_ct_convex')
+
+        self.grid6.addWidget(self.cutout_convex_box, 23, 0, 1, 2)
+
+        # Gaps
+        self.cutout_gaps_label = FCLabel('%s:' % _('Gaps'))
+        self.cutout_gaps_label.setToolTip(
+            _("Number of gaps used for the Automatic cutout.\n"
+              "There can be maximum 8 bridges/gaps.\n"
+              "The choices are:\n"
+              "- None  - no gaps\n"
+              "- lr    - left + right\n"
+              "- tb    - top + bottom\n"
+              "- 4     - left + right +top + bottom\n"
+              "- 2lr   - 2*left + 2*right\n"
+              "- 2tb  - 2*top + 2*bottom\n"
+              "- 8     - 2*left + 2*right +2*top + 2*bottom")
+        )
+
+        self.cutout_gaps = FCComboBox()
+        gaps_items = ['None', 'LR', 'TB', '4', '2LR', '2TB', '8']
+        self.cutout_gaps.addItems(gaps_items)
+        self.cutout_gaps.setObjectName('gdb_ct_gaps')
+
+        self.grid6.addWidget(self.cutout_gaps_label, 25, 0)
+        self.grid6.addWidget(self.cutout_gaps, 25, 1)
+
+        # ####################################################################
+        # ####################################################################
+        # GUI for the lower part of the window
+        # ####################################################################
+        # ####################################################################
+
+        new_vlay = QtWidgets.QVBoxLayout()
+        self.g_lay.addLayout(new_vlay, 1, 0, 1, 2)
+
+        self.buttons_frame = QtWidgets.QFrame()
+        self.buttons_frame.setContentsMargins(0, 0, 0, 0)
+        new_vlay.addWidget(self.buttons_frame)
+        self.buttons_box = QtWidgets.QHBoxLayout()
+        self.buttons_box.setContentsMargins(0, 0, 0, 0)
+        self.buttons_frame.setLayout(self.buttons_box)
+        self.buttons_frame.show()
+
+        self.add_entry_btn = FCButton(_("Add Tool in DB"))
+        self.add_entry_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png'))
+        self.add_entry_btn.setToolTip(
+            _("Add a new tool in the Tools Database.\n"
+              "It will be used in the Geometry UI.\n"
+              "You can edit it after it is added.")
+        )
+        self.buttons_box.addWidget(self.add_entry_btn)
+
+        # add_fct_entry_btn = FCButton(_("Add Paint/NCC Tool in DB"))
+        # add_fct_entry_btn.setToolTip(
+        #     _("Add a new tool in the Tools Database.\n"
+        #       "It will be used in the Paint/NCC Tools UI.\n"
+        #       "You can edit it after it is added.")
+        # )
+        # self.buttons_box.addWidget(add_fct_entry_btn)
+
+        self.remove_entry_btn = FCButton(_("Delete Tool from DB"))
+        self.remove_entry_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/trash16.png'))
+        self.remove_entry_btn.setToolTip(
+            _("Remove a selection of tools in the Tools Database.")
+        )
+        self.buttons_box.addWidget(self.remove_entry_btn)
+
+        self.export_db_btn = FCButton(_("Export DB"))
+        self.export_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/export.png'))
+        self.export_db_btn.setToolTip(
+            _("Save the Tools Database to a custom text file.")
+        )
+        self.buttons_box.addWidget(self.export_db_btn)
+
+        self.import_db_btn = FCButton(_("Import DB"))
+        self.import_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/import.png'))
+        self.import_db_btn.setToolTip(
+            _("Load the Tools Database information's from a custom text file.")
+        )
+        self.buttons_box.addWidget(self.import_db_btn)
+
+        self.save_db_btn = FCButton(_("Save DB"))
+        self.save_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.save_db_btn.setToolTip(
+            _("Save the Tools Database information's.")
+        )
+        self.buttons_box.addWidget(self.save_db_btn)
+
+        self.add_tool_from_db = FCButton(_("Transfer the Tool"))
+        self.add_tool_from_db.setToolTip(
+            _("Insert a new tool in the Tools Table of the\n"
+              "object/application tool after selecting a tool\n"
+              "in the Tools Database.")
+        )
+        self.add_tool_from_db.setStyleSheet("""
+                                            QPushButton
+                                            {
+                                                font-weight: bold;
+                                                color: green;
+                                            }
+                                            """)
+        self.add_tool_from_db.hide()
+
+        self.cancel_tool_from_db = FCButton(_("Cancel"))
+        self.cancel_tool_from_db.hide()
+
+        hlay = QtWidgets.QHBoxLayout()
+        tree_layout.addLayout(hlay)
+        hlay.addWidget(self.add_tool_from_db)
+        hlay.addWidget(self.cancel_tool_from_db)
+        # hlay.addStretch()
+        # ############################ FINSIHED GUI ###################################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+        
+
+class ToolsDB2(QtWidgets.QWidget):
+
+    mark_tools_rows = QtCore.pyqtSignal()
+
+    def __init__(self, app, callback_on_tool_request, parent=None):
+        super(ToolsDB2, self).__init__(parent)
+
+        self.app = app
+        self.app_ui = self.app.ui
+        self.decimals = self.app.decimals
+
+        self.on_tool_request = callback_on_tool_request
+
+        self.tools_db_changed_flag = False
+
+        '''
+        dict to hold all the tools in the Tools DB
+        format:
+        {
+            tool_id: {
+                'name': 'new_tool'
+                'tooldia': self.app.defaults["geometry_cnctooldia"]
+                'offset': 'Path'
+                'offset_value': 0.0
+                'type':  'Rough',
+                'tool_type': 'C1'
+                'data': dict()
+            }
+        }
+        '''
+        self.db_tool_dict = {}
+
+        self.old_color = QtGui.QColor('black')
+
+        # ##############################################################################
+        # ##############################################################################
+        # TOOLS DATABASE UI
+        # ##############################################################################
+        # ##############################################################################
+        layout = QtWidgets.QGridLayout()
+        layout.setColumnStretch(0, 0)
+        layout.setColumnStretch(1, 1)
+        self.setLayout(layout)
+        self.ui = ToolsDB2UI(app=self.app, grid_layout=layout)
+
+        # ##############################################################################
+        # ##############################################################################
+        # ########## SETUP THE DICTIONARIES THAT HOLD THE WIDGETS #####################
+        # ##############################################################################
+        # ##############################################################################
+
+        self.form_fields = {
+            "tool_target":      self.ui.tool_op_combo,
+            "tol_min":          self.ui.tol_min_entry,
+            "tol_max":          self.ui.tol_max_entry,
+            "name":             self.ui.name_entry,
+            "tooldia":          self.ui.dia_entry,
+
+            # Milling
+            "tool_type":        self.ui.mill_shape_combo,
+            "cutz":             self.ui.mill_cutz_entry,
+            "multidepth":       self.ui.mill_multidepth_cb,
+            "depthperpass":     self.ui.mill_multidepth_entry,
+            "travelz":          self.ui.mill_travelz_entry,
+            "feedrate":         self.ui.mill_frxy_entry,
+            "feedrate_z":       self.ui.mill_frz_entry,
+            "spindlespeed":     self.ui.mill_spindle_entry,
+            "dwell":            self.ui.mill_dwell_cb,
+            "dwelltime":        self.ui.mill_dwelltime_entry,
+
+            "type":             self.ui.mill_type_combo,
+            "offset":           self.ui.mill_tooloffset_combo,
+            "offset_value":     self.ui.mill_custom_offset_entry,
+            "vtipdia":          self.ui.mill_vdia_entry,
+            "vtipangle":        self.ui.mill_vangle_entry,
+            "feedrate_rapid":   self.ui.mill_frapids_entry,
+            "extracut":         self.ui.mill_ecut_cb,
+            "extracut_length":  self.ui.mill_ecut_length_entry,
+
+            # NCC
+            "tools_ncc_operation":      self.ui.ncc_op_radio,
+            "tools_ncc_milling_type":   self.ui.ncc_milling_type_radio,
+            "tools_ncc_overlap":        self.ui.ncc_overlap_entry,
+            "tools_ncc_margin":         self.ui.ncc_margin_entry,
+            "tools_ncc_method":         self.ui.ncc_method_combo,
+            "tools_ncc_connect":        self.ui.ncc_connect_cb,
+            "tools_ncc_contour":        self.ui.ncc_contour_cb,
+            "tools_ncc_offset_choice":  self.ui.ncc_choice_offset_cb,
+            "tools_ncc_offset_value":   self.ui.ncc_offset_spinner,
+
+            # Paint
+            "tools_paint_overlap":      self.ui.paint_overlap_entry,
+            "tools_paint_offset":       self.ui.paint_offset_entry,
+            "tools_paint_method":       self.ui.paint_method_combo,
+            "tools_paint_connect":      self.ui.path_connect_cb,
+            "tools_paint_contour":      self.ui.paint_contour_cb,
+
+            # Isolation
+            "tools_iso_passes":         self.ui.iso_passes_entry,
+            "tools_iso_overlap":        self.ui.iso_overlap_entry,
+            "tools_iso_milling_type":   self.ui.iso_milling_type_radio,
+            "tools_iso_follow":         self.ui.iso_follow_cb,
+            "tools_iso_isotype":        self.ui.iso_type_radio,
+
+            # Drilling
+            "tools_drill_cutz":             self.ui.drill_cutz_entry,
+            "tools_drill_multidepth":       self.ui.drill_mpass_cb,
+            "tools_drill_depthperpass":     self.ui.drill_maxdepth_entry,
+            "tools_drill_travelz":          self.ui.drill_travelz_entry,
+            "tools_drill_feedrate_z":       self.ui.drill_feedrate_z_entry,
+
+            "tools_drill_feedrate_rapid":   self.ui.drill_feedrate_rapid_entry,
+            "tools_drill_spindlespeed":     self.ui.drill_spindlespeed_entry,
+            "tools_drill_dwell":            self.ui.drill_dwell_cb,
+            "tools_drill_dwelltime":        self.ui.drill_dwelltime_entry,
+
+            "tools_drill_offset":           self.ui.drill_offset_entry,
+            "tools_drill_drill_slots":      self.ui.drill_slots_cb,
+            "tools_drill_drill_overlap":    self.ui.drill_overlap_entry,
+            "tools_drill_last_drill":       self.ui.drill_last_drill_cb,
+
+            # Cutout
+            "tools_cutout_margin":          self.ui.cutout_margin_entry,
+            "tools_cutout_gapsize":         self.ui.cutout_gapsize,
+            "tools_cutout_gaps_ff":         self.ui.cutout_gaps,
+            "tools_cutout_convexshape":     self.ui.cutout_convex_box,
+
+            "tools_cutout_gap_type":        self.ui.cutout_gaptype_radio,
+            "tools_cutout_gap_depth":       self.ui.cutout_thin_depth_entry,
+            "tools_cutout_mb_dia":          self.ui.cutout_mb_dia_entry,
+            "tools_cutout_mb_spacing":      self.ui.cutout_mb_spacing_entry,
+
+        }
+
+        self.name2option = {
+            "gdb_tool_target":      "tool_target",
+            "gdb_tol_min":          "tol_min",
+            "gdb_tol_max":          "tol_max",
+
+            "gdb_name":             "name",
+            "gdb_dia":              "tooldia",
+
+            # Milling
+            "gdb_shape":            "tool_type",
+            "gdb_cutz":             "cutz",
+            "gdb_multidepth":       "multidepth",
+            "gdb_multidepth_entry": "depthperpass",
+            "gdb_travelz":           "travelz",
+            "gdb_frxy":             "feedrate",
+            "gdb_frz":              "feedrate_z",
+            "gdb_spindle":          "spindlespeed",
+            "gdb_dwell":            "dwell",
+            "gdb_dwelltime":        "dwelltime",
+
+            "gdb_type":             "type",
+            "gdb_tool_offset":      "offset",
+            "gdb_custom_offset":    "offset_value",
+            "gdb_vdia":             "vtipdia",
+            "gdb_vangle":           "vtipangle",
+            "gdb_frapids":          "feedrate_rapid",
+            "gdb_ecut":             "extracut",
+            "gdb_ecut_length":      "extracut_length",
+
+            # NCC
+            "gdb_n_operation":      "tools_ncc_operation",
+            "gdb_n_overlap":        "tools_ncc_overlap",
+            "gdb_n_margin":         "tools_ncc_margin",
+            "gdb_n_method":         "tools_ncc_method",
+            "gdb_n_connect":        "tools_ncc_connect",
+            "gdb_n_contour":        "tools_ncc_contour",
+            "gdb_n_offset":         "tools_ncc_offset_choice",
+            "gdb_n_offset_value":   "tools_ncc_offset_value",
+            "gdb_n_milling_type":   "tools_ncc_milling_type",
+
+            # Paint
+            'gdb_p_overlap':        "tools_paint_overlap",
+            'gdb_p_offset':         "tools_paint_offset",
+            'gdb_p_method':         "tools_paint_method",
+            'gdb_p_connect':        "tools_paint_connect",
+            'gdb_p_contour':        "tools_paint_contour",
+
+            # Isolation
+            "gdb_i_passes":         "tools_iso_passes",
+            "gdb_i_overlap":        "tools_iso_overlap",
+            "gdb_i_milling_type":   "tools_iso_milling_type",
+            "gdb_i_follow":         "tools_iso_follow",
+            "gdb_i_iso_type":       "tools_iso_isotype",
+
+            # Drilling
+            "gdb_e_cutz":               "tools_drill_cutz",
+            "gdb_e_multidepth":         "tools_drill_multidepth",
+            "gdb_e_depthperpass":       "tools_drill_depthperpass",
+            "gdb_e_travelz":            "tools_drill_travelz",
+
+            "gdb_e_feedratez":          "tools_drill_feedrate_z",
+            "gdb_e_fr_rapid":           "tools_drill_feedrate_rapid",
+            "gdb_e_spindlespeed":       "tools_drill_spindlespeed",
+            "gdb_e_dwell":              "tools_drill_dwell",
+            "gdb_e_dwelltime":          "tools_drill_dwelltime",
+
+            "gdb_e_offset":             "tools_drill_offset",
+            "gdb_e_drill_slots":        "tools_drill_drill_slots",
+            "gdb_e_drill_slots_over":   "tools_drill_drill_overlap",
+            "gdb_e_drill_last_drill":   "tools_drill_last_drill",
+
+            # Cutout
+            "gdb_ct_margin":            "tools_cutout_margin",
+            "gdb_ct_gapsize":           "tools_cutout_gapsize",
+            "gdb_ct_gaps":              "tools_cutout_gaps_ff",
+            "gdb_ct_convex":            "tools_cutout_convexshape",
+
+            "gdb_ct_gap_type":          "tools_cutout_gap_type",
+            "gdb_ct_gap_depth":         "tools_cutout_gap_depth",
+            "gdb_ct_mb_dia":            "tools_cutout_mb_dia",
+            "gdb_ct_mb_spacing":        "tools_cutout_mb_spacing"
+
+        }
+
+        self.current_toolid = None
+
+        # variable to show if double clicking and item will trigger adding a tool from DB
+        self.ok_to_add = False
+
+        # ##############################################################################
+        # ######################## SIGNALS #############################################
+        # ##############################################################################
+
+        self.ui.add_entry_btn.clicked.connect(self.on_tool_add)
+        self.ui.remove_entry_btn.clicked.connect(self.on_tool_delete)
+        self.ui.export_db_btn.clicked.connect(self.on_export_tools_db_file)
+        self.ui.import_db_btn.clicked.connect(self.on_import_tools_db_file)
+        self.ui.save_db_btn.clicked.connect(self.on_save_db_btn_click)
+        # closebtn.clicked.connect(self.accept)
+
+        self.ui.add_tool_from_db.clicked.connect(self.on_tool_requested_from_app)
+        self.ui.cancel_tool_from_db.clicked.connect(self.on_cancel_tool)
+
+        # self.ui.tree_widget.selectionModel().selectionChanged.connect(self.on_list_selection_change)
+        self.ui.tree_widget.currentItemChanged.connect(self.on_list_selection_change)
+        self.ui.tree_widget.itemChanged.connect(self.on_list_item_edited)
+        self.ui.tree_widget.customContextMenuRequested.connect(self.on_menu_request)
+
+        self.ui.tree_widget.itemDoubleClicked.connect(self.on_item_double_clicked)
+
+        self.ui.tool_op_combo.currentIndexChanged.connect(self.on_tool_target_changed)
+
+        self.setup_db_ui()
+
+    def on_menu_request(self, pos):
+
+        menu = QtWidgets.QMenu()
+        add_tool = menu.addAction(QtGui.QIcon(self.app.resource_location + '/plus16.png'), _("Add to DB"))
+        add_tool.triggered.connect(self.on_tool_add)
+
+        copy_tool = menu.addAction(QtGui.QIcon(self.app.resource_location + '/copy16.png'), _("Copy from DB"))
+        copy_tool.triggered.connect(self.on_tool_copy)
+
+        delete_tool = menu.addAction(QtGui.QIcon(self.app.resource_location + '/delete32.png'), _("Delete from DB"))
+        delete_tool.triggered.connect(self.on_tool_delete)
+
+        # sep = menu.addSeparator()
+
+        save_changes = menu.addAction(QtGui.QIcon(self.app.resource_location + '/save_as.png'), _("Save changes"))
+        save_changes.triggered.connect(self.on_save_changes)
+
+        # tree_item = self.ui.tree_widget.itemAt(pos)
+        menu.exec(self.ui.tree_widget.viewport().mapToGlobal(pos))
+
+    def on_save_changes(self):
+        widget_name = self.app_ui.plot_tab_area.currentWidget().objectName()
+        if widget_name == 'database_tab':
+            # Tools DB saved, update flag
+            self.app.tools_db_changed_flag = False
+            self.app.tools_db_tab.on_save_tools_db()
+
+    def on_item_double_clicked(self, item, column):
+        if column == 0 and self.ok_to_add is True:
+            self.ok_to_add = False
+            self.on_tool_requested_from_app()
+
+    def on_list_selection_change(self, current, previous):
+        self.ui_disconnect()
+        self.current_toolid = int(current.text(0))
+        self.storage_to_form(self.db_tool_dict[current.text(0)])
+        self.ui_connect()
+
+    def on_list_item_edited(self, item, column):
+        if column == 0:
+            return
+
+        self.ui.name_entry.set_value(item.text(1))
+
+    def storage_to_form(self, dict_storage):
+        self.ui_disconnect()
+        for form_key in self.form_fields:
+            for storage_key in dict_storage:
+                if form_key == storage_key:
+                    try:
+                        self.form_fields[form_key].set_value(dict_storage[form_key])
+                    except Exception as e:
+                        print(str(e))
+                if storage_key == 'data':
+                    for data_key in dict_storage[storage_key]:
+                        if form_key == data_key:
+                            try:
+                                self.form_fields[form_key].set_value(dict_storage['data'][data_key])
+                            except Exception as e:
+                                print(str(e))
+        self.ui_connect()
+
+    def form_to_storage(self, tool):
+        self.ui_disconnect()
+
+        widget_changed = self.sender()
+        wdg_objname = widget_changed.objectName()
+        option_changed = self.name2option[wdg_objname]
+        tooluid_item = int(tool)
+
+        for tooluid_key, tooluid_val in self.db_tool_dict.items():
+            if int(tooluid_key) == tooluid_item:
+                new_option_value = self.form_fields[option_changed].get_value()
+                if option_changed in tooluid_val:
+                    tooluid_val[option_changed] = new_option_value
+                if option_changed in tooluid_val['data']:
+                    tooluid_val['data'][option_changed] = new_option_value
+        self.ui_connect()
+
+    def setup_db_ui(self):
+
+        # set the old color for the Tools Database Tab
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+                self.old_color = self.app.ui.plot_tab_area.tabBar.tabTextColor(idx)
+
+        filename = self.app.tools_database_path()
+
+        # load the database tools from the file
+        try:
+            with open(filename) as f:
+                tools = f.read()
+        except IOError:
+            self.app.log.error("Could not load tools DB file.")
+            self.app.inform.emit('[ERROR] %s' % _("Could not load the file."))
+            return
+
+        try:
+            self.db_tool_dict = json.loads(tools)
+        except Exception:
+            e = sys.exc_info()[0]
+            self.app.log.error(str(e))
+            self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
+            return
+
+        self.app.inform.emit('[success] %s: %s' % (_("Loaded Tools DB from"), filename))
+
+        self.build_db_ui()
+
+    def build_db_ui(self):
+        self.ui_disconnect()
+        nr_crt = 0
+
+        parent = self.ui.tree_widget
+        self.ui.tree_widget.blockSignals(True)
+        self.ui.tree_widget.clear()
+        self.ui.tree_widget.blockSignals(False)
+
+        for toolid, dict_val in self.db_tool_dict.items():
+            row = nr_crt
+            nr_crt += 1
+
+            t_name = dict_val['name']
+            try:
+                # self.add_tool_table_line(row, name=t_name, tooldict=dict_val)
+                self.ui.tree_widget.blockSignals(True)
+                try:
+                    self.ui.tree_widget.addParentEditable(parent=parent, title=[str(row+1), t_name], editable=True)
+                except Exception as e:
+                    print('FlatCAMCoomn.ToolDB2.build_db_ui() -> ', str(e))
+                self.ui.tree_widget.blockSignals(False)
+            except Exception as e:
+                self.app.log.debug("ToolDB.build_db_ui.add_tool_table_line() --> %s" % str(e))
+
+        if self.current_toolid is None or self.current_toolid < 1:
+            if self.db_tool_dict:
+                self.storage_to_form(self.db_tool_dict['1'])
+
+                # Enable appGUI
+                try:
+                    self.on_tool_target_changed(val=self.db_tool_dict['1']['data']['tool_target'])
+                except KeyError:
+                    self.on_tool_target_changed(val=_("General"))
+
+                self.ui.tree_widget.setCurrentItem(self.ui.tree_widget.topLevelItem(0))
+                # self.ui.tree_widget.setFocus()
+
+            else:
+                # Disable appGUI
+                self.ui.tool_description_box.show()
+                self.ui.milling_box.show()
+                self.ui.ncc_box.show()
+                self.ui.paint_box.show()
+                self.ui.iso_box.show()
+                self.ui.drill_box.show()
+                self.ui.cutout_box.show()
+
+                self.ui.tool_description_box.setEnabled(False)
+                self.ui.milling_box.setEnabled(False)
+                self.ui.ncc_box.setEnabled(False)
+                self.ui.paint_box.setEnabled(False)
+                self.ui.iso_box.setEnabled(False)
+                self.ui.drill_box.setEnabled(False)
+                self.ui.cutout_box.setEnabled(False)
+        else:
+            self.storage_to_form(self.db_tool_dict[str(self.current_toolid)])
+
+        self.ui_connect()
+
+    def on_tool_target_changed(self, index=None, val=None):
+
+        if val is None:
+            tool_target = self.ui.tool_op_combo.get_value()
+        else:
+            tool_target = val
+
+        self.ui.tool_description_box.setEnabled(True)
+        if self.db_tool_dict:
+            if tool_target == 0:    # _("General")
+                self.ui.milling_box.setEnabled(True)
+                self.ui.ncc_box.setEnabled(True)
+                self.ui.paint_box.setEnabled(True)
+                self.ui.iso_box.setEnabled(True)
+                self.ui.drill_box.setEnabled(True)
+                self.ui.cutout_box.setEnabled(True)
+
+                self.ui.milling_box.show()
+                self.ui.ncc_box.show()
+                self.ui.paint_box.show()
+                self.ui.iso_box.show()
+                self.ui.drill_box.show()
+                self.ui.cutout_box.show()
+            else:
+                self.ui.milling_box.hide()
+                self.ui.ncc_box.hide()
+                self.ui.paint_box.hide()
+                self.ui.iso_box.hide()
+                self.ui.drill_box.hide()
+                self.ui.cutout_box.hide()
+
+                if tool_target == 1:    # _("Milling")
+                    self.ui.milling_box.setEnabled(True)
+                    self.ui.milling_box.show()
+
+                if tool_target == 2:    # _("Drilling")
+                    self.ui.drill_box.setEnabled(True)
+                    self.ui.drill_box.show()
+
+                if tool_target == 3:    # _("Isolation")
+                    self.ui.iso_box.setEnabled(True)
+                    self.ui.iso_box.show()
+                    self.ui.milling_box.setEnabled(True)
+                    self.ui.milling_box.show()
+
+                if tool_target == 4:    # _("Paint")
+                    self.ui.paint_box.setEnabled(True)
+                    self.ui.paint_box.show()
+                    self.ui.milling_box.setEnabled(True)
+                    self.ui.milling_box.show()
+
+                if tool_target == 5:    # _("NCC")
+                    self.ui.ncc_box.setEnabled(True)
+                    self.ui.ncc_box.show()
+                    self.ui.milling_box.setEnabled(True)
+                    self.ui.milling_box.show()
+
+                if tool_target == 6:    # _("Cutout")
+                    self.ui.cutout_box.setEnabled(True)
+                    self.ui.cutout_box.show()
+                    self.ui.milling_box.setEnabled(True)
+                    self.ui.milling_box.show()
+
+    def on_tool_add(self):
+        """
+        Add a tool in the DB Tool Table
+        :return: None
+        """
+
+        default_data = {}
+        default_data.update({
+            "plot":             True,
+            "tool_target": 0,   # _("General")
+            "tol_min": 0.0,
+            "tol_max": 0.0,
+
+            # Milling
+            "cutz":             float(self.app.defaults["geometry_cutz"]),
+            "multidepth":       self.app.defaults["geometry_multidepth"],
+            "depthperpass":     float(self.app.defaults["geometry_depthperpass"]),
+            "vtipdia":          float(self.app.defaults["geometry_vtipdia"]),
+            "vtipangle":        float(self.app.defaults["geometry_vtipangle"]),
+            "travelz":          float(self.app.defaults["geometry_travelz"]),
+            "feedrate":         float(self.app.defaults["geometry_feedrate"]),
+            "feedrate_z":       float(self.app.defaults["geometry_feedrate_z"]),
+            "feedrate_rapid":   float(self.app.defaults["geometry_feedrate_rapid"]),
+            "spindlespeed":     self.app.defaults["geometry_spindlespeed"],
+            "dwell":            self.app.defaults["geometry_dwell"],
+            "dwelltime":        float(self.app.defaults["geometry_dwelltime"]),
+            "ppname_g":         self.app.defaults["geometry_ppname_g"],
+            "extracut":         self.app.defaults["geometry_extracut"],
+            "extracut_length":  float(self.app.defaults["geometry_extracut_length"]),
+            "toolchange":       self.app.defaults["geometry_toolchange"],
+            "toolchangexy":     self.app.defaults["geometry_toolchangexy"],
+            "toolchangez":      float(self.app.defaults["geometry_toolchangez"]),
+            "startz":           self.app.defaults["geometry_startz"],
+            "endz":             float(self.app.defaults["geometry_endz"]),
+            "endxy":            self.app.defaults["geometry_endxy"],
+            "search_time":      int(self.app.defaults["geometry_search_time"]),
+            "z_pdepth":         float(self.app.defaults["geometry_z_pdepth"]),
+            "f_plunge":         float(self.app.defaults["geometry_f_plunge"]),
+
+            "spindledir":               self.app.defaults["geometry_spindledir"],
+            "optimization_type":        self.app.defaults["geometry_optimization_type"],
+            "feedrate_probe":           self.app.defaults["geometry_feedrate_probe"],
+
+            "segx":             self.app.defaults["geometry_segx"],
+            "segy":             self.app.defaults["geometry_segy"],
+            "area_exclusion":   self.app.defaults["geometry_area_exclusion"],
+            "area_shape":       self.app.defaults["geometry_area_shape"],
+            "area_strategy":    self.app.defaults["geometry_area_strategy"],
+            "area_overz":       self.app.defaults["geometry_area_overz"],
+            "polish":           self.app.defaults["geometry_polish"],
+            "polish_dia":       self.app.defaults["geometry_polish_dia"],
+            "polish_pressure":  self.app.defaults["geometry_polish_pressure"],
+            "polish_travelz":   self.app.defaults["geometry_polish_travelz"],
+            "polish_margin":    self.app.defaults["geometry_polish_margin"],
+            "polish_overlap":   self.app.defaults["geometry_polish_overlap"],
+            "polish_method":    self.app.defaults["geometry_polish_method"],
+
+            # NCC
+            "tools_ncc_operation":       self.app.defaults["tools_ncc_operation"],
+            "tools_ncc_milling_type":    self.app.defaults["tools_ncc_milling_type"],
+            "tools_ncc_overlap":         float(self.app.defaults["tools_ncc_overlap"]),
+            "tools_ncc_margin":          float(self.app.defaults["tools_ncc_margin"]),
+            "tools_ncc_method":          self.app.defaults["tools_ncc_method"],
+            "tools_ncc_connect":         self.app.defaults["tools_ncc_connect"],
+            "tools_ncc_contour":         self.app.defaults["tools_ncc_contour"],
+            "tools_ncc_offset_choice":  self.app.defaults["tools_ncc_offset_choice"],
+            "tools_ncc_offset_value":   float(self.app.defaults["tools_ncc_offset_value"]),
+
+            # Paint
+            "tools_paint_overlap":       float(self.app.defaults["tools_paint_overlap"]),
+            "tools_paint_offset":        float(self.app.defaults["tools_paint_offset"]),
+            "tools_paint_method":        self.app.defaults["tools_paint_method"],
+            "tools_paint_connect":        self.app.defaults["tools_paint_connect"],
+            "tools_paint_contour":       self.app.defaults["tools_paint_contour"],
+
+            # Isolation
+            "tools_iso_passes":         int(self.app.defaults["tools_iso_passes"]),
+            "tools_iso_overlap":        float(self.app.defaults["tools_iso_overlap"]),
+            "tools_iso_milling_type":   self.app.defaults["tools_iso_milling_type"],
+            "tools_iso_follow":         self.app.defaults["tools_iso_follow"],
+            "tools_iso_isotype":        self.app.defaults["tools_iso_isotype"],
+
+            # Drilling
+            "tools_drill_cutz":             float(self.app.defaults["tools_drill_cutz"]),
+            "tools_drill_multidepth":       self.app.defaults["tools_drill_multidepth"],
+            "tools_drill_depthperpass":     float(self.app.defaults["tools_drill_depthperpass"]),
+            "tools_drill_travelz":          float(self.app.defaults["tools_drill_travelz"]),
+
+            "tools_drill_feedrate_z":       float(self.app.defaults["tools_drill_feedrate_z"]),
+            "tools_drill_feedrate_rapid":   float(self.app.defaults["tools_drill_feedrate_rapid"]),
+            "tools_drill_spindlespeed":     float(self.app.defaults["tools_drill_spindlespeed"]),
+            "tools_drill_dwell":            self.app.defaults["tools_drill_dwell"],
+
+            "tools_drill_offset":           float(self.app.defaults["tools_drill_offset"]),
+            "tools_drill_drill_slots":      self.app.defaults["tools_drill_drill_slots"],
+            "tools_drill_drill_overlap":    float(self.app.defaults["tools_drill_drill_overlap"]),
+            "tools_drill_last_drill":       self.app.defaults["tools_drill_last_drill"],
+
+            # Cutout
+            "tools_cutout_margin":          float(self.app.defaults["tools_cutout_margin"]),
+            "tools_cutout_gapsize":         float(self.app.defaults["tools_cutout_gapsize"]),
+            "tools_cutout_gaps_ff":         self.app.defaults["tools_cutout_gaps_ff"],
+            "tools_cutout_convexshape":     self.app.defaults["tools_cutout_convexshape"],
+
+            "tools_cutout_gap_type":        self.app.defaults["tools_cutout_gap_type"],
+            "tools_cutout_gap_depth":       float(self.app.defaults["tools_cutout_gap_depth"]),
+            "tools_cutout_mb_dia":          float(self.app.defaults["tools_cutout_mb_dia"]),
+            "tools_cutout_mb_spacing":      float(self.app.defaults["tools_cutout_mb_spacing"])
+        })
+
+        temp = []
+        for k, v in self.db_tool_dict.items():
+            if "new_tool_" in v['name']:
+                temp.append(float(v['name'].rpartition('_')[2]))
+
+        if temp:
+            new_name = "new_tool_%d" % int(max(temp) + 1)
+        else:
+            new_name = "new_tool_1"
+
+        dict_elem = {'name': new_name}
+        if type(self.app.defaults["geometry_cnctooldia"]) == float:
+            dict_elem['tooldia'] = self.app.defaults["geometry_cnctooldia"]
+        else:
+            try:
+                tools_string = self.app.defaults["geometry_cnctooldia"].split(",")
+                tools_diameters = [eval(a) for a in tools_string if a != '']
+                dict_elem['tooldia'] = tools_diameters[0] if tools_diameters else 0.0
+            except Exception as e:
+                self.app.log.debug("ToolDB.on_tool_add() --> %s" % str(e))
+                return
+
+        dict_elem['offset'] = 'Path'
+        dict_elem['offset_value'] = 0.0
+        dict_elem['type'] = 'Rough'
+        dict_elem['tool_type'] = 'C1'
+        dict_elem['data'] = default_data
+
+        new_toolid = len(self.db_tool_dict) + 1
+        self.db_tool_dict[str(new_toolid)] = deepcopy(dict_elem)
+
+        # add the new entry to the Tools DB table
+        self.update_storage()
+        self.build_db_ui()
+
+        # select the last Tree item just added
+        nr_items = self.ui.tree_widget.topLevelItemCount()
+        if nr_items:
+            last_item = self.ui.tree_widget.topLevelItem(nr_items - 1)
+            self.ui.tree_widget.setCurrentItem(last_item)
+            last_item.setSelected(True)
+
+        self.on_tool_target_changed(val=dict_elem['data']['tool_target'])
+        self.app.inform.emit('[success] %s' % _("Tool added to DB."))
+
+    def on_tool_copy(self):
+        """
+        Copy a selection of Tools in the Tools DB table
+        :return:
+        """
+        new_tool_id = len(self.db_tool_dict)
+        for item in self.ui.tree_widget.selectedItems():
+            old_tool_id = item.data(0, QtCore.Qt.DisplayRole)
+
+            for toolid, dict_val in list(self.db_tool_dict.items()):
+                if int(old_tool_id) == int(toolid):
+                    new_tool_id += 1
+                    new_key = str(new_tool_id)
+
+                    self.db_tool_dict.update({
+                        new_key: deepcopy(dict_val)
+                    })
+
+        self.current_toolid = new_tool_id
+
+        self.update_storage()
+        self.build_db_ui()
+
+        # select the last Tree item just added
+        nr_items = self.ui.tree_widget.topLevelItemCount()
+        if nr_items:
+            last_item = self.ui.tree_widget.topLevelItem(nr_items - 1)
+            self.ui.tree_widget.setCurrentItem(last_item)
+            last_item.setSelected(True)
+
+        self.on_tools_db_edited()
+        self.app.inform.emit('[success] %s' % _("Tool copied from Tools DB."))
+
+    def on_tool_delete(self):
+        """
+        Delete a selection of Tools in the Tools DB table
+        :return:
+        """
+        for item in self.ui.tree_widget.selectedItems():
+            toolname_to_remove = item.data(0, QtCore.Qt.DisplayRole)
+
+            for toolid, dict_val in list(self.db_tool_dict.items()):
+                if int(toolname_to_remove) == int(toolid):
+                    # remove from the storage
+                    self.db_tool_dict.pop(toolid, None)
+
+        self.current_toolid -= 1
+
+        self.update_storage()
+        self.build_db_ui()
+
+        # select the first Tree item
+        nr_items = self.ui.tree_widget.topLevelItemCount()
+        if nr_items:
+            first_item = self.ui.tree_widget.topLevelItem(0)
+            self.ui.tree_widget.setCurrentItem(first_item)
+            first_item.setSelected(True)
+
+        self.app.inform.emit('[success] %s' % _("Tool removed from Tools DB."))
+
+    def on_export_tools_db_file(self):
+        self.app.defaults.report_usage("on_export_tools_db_file")
+        self.app.log.debug("on_export_tools_db_file()")
+
+        date = str(datetime.today()).rpartition('.')[0]
+        date = ''.join(c for c in date if c not in ':-')
+        date = date.replace(' ', '_')
+
+        filter__ = "Text File (*.TXT);;All Files (*.*)"
+
+        filename, _f = FCFileSaveDialog.get_saved_filename(
+            caption=_("Export Tools Database"),
+            directory='{l_save}/FlatCAM_{n}_{date}'.format(l_save=str(self.app.get_last_save_folder()),
+                                                           n=_("Tools_Database"),
+                                                           date=date),
+            ext_filter=filter__)
+
+        filename = str(filename)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+            return
+        else:
+            try:
+                f = open(filename, 'w')
+                f.close()
+            except PermissionError:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                return
+            except IOError:
+                self.app.log.debug('Creating a new Tools DB file ...')
+                f = open(filename, 'w')
+                f.close()
+            except Exception:
+                e = sys.exc_info()[0]
+                self.app.log.error("Could not load Tools DB file.")
+                self.app.log.error(str(e))
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load the file."))
+                return
+
+            # Save update options
+            try:
+                # Save Tools DB in a file
+                try:
+                    with open(filename, "w") as f:
+                        json.dump(self.db_tool_dict, f, default=to_dict, indent=2)
+                except Exception as e:
+                    self.app.log.debug("App.on_save_tools_db() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+                    return
+            except Exception:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+                return
+
+        self.app.inform.emit('[success] %s: %s' % (_("Exported Tools DB to"), filename))
+
+    def on_import_tools_db_file(self):
+        self.app.defaults.report_usage("on_import_tools_db_file")
+        self.app.log.debug("on_import_tools_db_file()")
+
+        filter__ = "Text File (*.TXT);;All Files (*.*)"
+        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Tools DB"), filter=filter__)
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+        else:
+            try:
+                with open(filename) as f:
+                    tools_in_db = f.read()
+            except IOError:
+                self.app.log.error("Could not load Tools DB file.")
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load the file."))
+                return
+
+            try:
+                self.db_tool_dict = json.loads(tools_in_db)
+            except Exception:
+                e = sys.exc_info()[0]
+                self.app.log.error(str(e))
+                self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
+                return
+
+            self.app.inform.emit('[success] %s: %s' % (_("Loaded Tools DB from"), filename))
+            self.build_db_ui()
+            self.update_storage()
+
+    def on_save_tools_db(self, silent=False):
+        self.app.log.debug("ToolsDB.on_save_button() --> Saving Tools Database to file.")
+
+        filename = self.app.tools_database_path()
+
+        # Preferences save, update the color of the Tools DB Tab text
+        for idx in range(self.app_ui.plot_tab_area.count()):
+            if self.app_ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+                self.app_ui.plot_tab_area.tabBar.setTabTextColor(idx, self.old_color)
+                self.ui.save_db_btn.setStyleSheet("")
+
+                # clean the dictionary and leave only keys of interest
+                for tool_id in self.db_tool_dict.keys():
+                    if self.db_tool_dict[tool_id]['data']['tool_target'] != _('General'):
+                        continue
+
+                    if self.db_tool_dict[tool_id]['data']['tool_target'] == _('Milling'):
+                        for k in list(self.db_tool_dict[tool_id]['data'].keys()):
+                            if str(k).startswith('tools_'):
+                                self.db_tool_dict[tool_id]['data'].pop(k, None)
+
+                    if self.db_tool_dict[tool_id]['data']['tool_target'] == _('Drilling'):
+                        for k in list(self.db_tool_dict[tool_id]['data'].keys()):
+                            if str(k).startswith('tools_'):
+                                if str(k).startswith('tools_drill') or str(k).startswith('tools_mill'):
+                                    pass
+                                else:
+                                    self.db_tool_dict[tool_id]['data'].pop(k, None)
+
+                    if self.db_tool_dict[tool_id]['data']['tool_target'] == _('Isolation'):
+                        for k in list(self.db_tool_dict[tool_id]['data'].keys()):
+                            if str(k).startswith('tools_'):
+                                if str(k).startswith('tools_iso') or str(k).startswith('tools_mill'):
+                                    pass
+                                else:
+                                    self.db_tool_dict[tool_id]['data'].pop(k, None)
+
+                    if self.db_tool_dict[tool_id]['data']['tool_target'] == _('Paint'):
+                        for k in list(self.db_tool_dict[tool_id]['data'].keys()):
+                            if str(k).startswith('tools_'):
+                                if str(k).startswith('tools_paint') or str(k).startswith('tools_mill'):
+                                    pass
+                                else:
+                                    self.db_tool_dict[tool_id]['data'].pop(k, None)
+                                    
+                    if self.db_tool_dict[tool_id]['data']['tool_target'] == _('NCC'):
+                        for k in list(self.db_tool_dict[tool_id]['data'].keys()):
+                            if str(k).startswith('tools_'):
+                                if str(k).startswith('tools_ncc') or str(k).startswith('tools_mill'):
+                                    pass
+                                else:
+                                    self.db_tool_dict[tool_id]['data'].pop(k, None)
+
+                    if self.db_tool_dict[tool_id]['data']['tool_target'] == _('Cutout'):
+                        for k in list(self.db_tool_dict[tool_id]['data'].keys()):
+                            if str(k).startswith('tools_'):
+                                if str(k).startswith('tools_cutout') or str(k).startswith('tools_mill'):
+                                    pass
+                                else:
+                                    self.db_tool_dict[tool_id]['data'].pop(k, None)
+
+                # Save Tools DB in a file
+                try:
+                    f = open(filename, "w")
+                    json.dump(self.db_tool_dict, f, default=to_dict, indent=2)
+                    f.close()
+                except Exception as e:
+                    self.app.log.debug("ToolsDB.on_save_tools_db() --> %s" % str(e))
+                    self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+                    return
+
+                if not silent:
+                    self.app.inform.emit('[success] %s' % _("Saved Tools DB."))
+
+    def on_save_db_btn_click(self):
+        self.app.tools_db_changed_flag = False
+        self.on_save_tools_db()
+
+    def on_calculate_tooldia(self):
+        if self.ui.mill_shape_combo.get_value() == 'V':
+            tip_dia = float(self.ui.mill_vdia_entry.get_value())
+            half_tip_angle = float(self.ui.mill_vangle_entry.get_value()) / 2.0
+            cut_z = float(self.ui.mill_cutz_entry.get_value())
+            cut_z = -cut_z if cut_z < 0 else cut_z
+
+            # calculated tool diameter so the cut_z parameter is obeyed
+            tool_dia = tip_dia + (2 * cut_z * math.tan(math.radians(half_tip_angle)))
+
+            self.ui.dia_entry.set_value(tool_dia)
+
+    def ui_connect(self):
+        # make sure that we don't make multiple connections to the widgets
+        self.ui_disconnect()
+
+        self.ui.name_entry.editingFinished.connect(self.update_tree_name)
+
+        for key in self.form_fields:
+            wdg = self.form_fields[key]
+
+            # FCEntry
+            if isinstance(wdg, FCEntry):
+                wdg.textChanged.connect(self.update_storage)
+
+            # ComboBox
+            if isinstance(wdg, FCComboBox):
+                wdg.currentIndexChanged.connect(self.update_storage)
+
+            # CheckBox
+            if isinstance(wdg, FCCheckBox):
+                wdg.toggled.connect(self.update_storage)
+
+            # FCRadio
+            if isinstance(wdg, RadioSet):
+                wdg.activated_custom.connect(self.update_storage)
+
+            # SpinBox, DoubleSpinBox
+            if isinstance(wdg, FCSpinner) or isinstance(wdg, FCDoubleSpinner):
+                wdg.valueChanged.connect(self.update_storage)
+
+        # connect the calculate tooldia method to the controls
+        # if the tool shape is 'V' the tool dia will be calculated to obey Cut Z parameter
+        self.ui.mill_shape_combo.currentIndexChanged.connect(self.on_calculate_tooldia)
+        self.ui.mill_cutz_entry.valueChanged.connect(self.on_calculate_tooldia)
+        self.ui.mill_vdia_entry.valueChanged.connect(self.on_calculate_tooldia)
+        self.ui.mill_vangle_entry.valueChanged.connect(self.on_calculate_tooldia)
+
+    def ui_disconnect(self):
+        try:
+            self.ui.name_entry.editingFinished.disconnect(self.update_tree_name)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.ui.mill_shape_combo.currentIndexChanged.disconnect(self.on_calculate_tooldia)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.ui.mill_cutz_entry.valueChanged.disconnect(self.on_calculate_tooldia)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.ui.mill_vdia_entry.valueChanged.disconnect(self.on_calculate_tooldia)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.ui.mill_vangle_entry.valueChanged.disconnect(self.on_calculate_tooldia)
+        except (TypeError, AttributeError):
+            pass
+
+        for key in self.form_fields:
+            wdg = self.form_fields[key]
+
+            # FCEntry
+            if isinstance(wdg, FCEntry):
+                try:
+                    wdg.textChanged.disconnect(self.update_storage)
+                except (TypeError, AttributeError):
+                    pass
+
+            # ComboBox
+            if isinstance(wdg, FCComboBox):
+                try:
+                    wdg.currentIndexChanged.disconnect(self.update_storage)
+                except (TypeError, AttributeError):
+                    pass
+
+            # CheckBox
+            if isinstance(wdg, FCCheckBox):
+                try:
+                    wdg.toggled.disconnect(self.update_storage)
+                except (TypeError, AttributeError):
+                    pass
+
+            # FCRadio
+            if isinstance(wdg, RadioSet):
+                try:
+                    wdg.activated_custom.disconnect(self.update_storage)
+                except (TypeError, AttributeError):
+                    pass
+
+            # SpinBox, DoubleSpinBox
+            if isinstance(wdg, FCSpinner) or isinstance(wdg, FCDoubleSpinner):
+                try:
+                    wdg.valueChanged.disconnect(self.update_storage)
+                except (TypeError, AttributeError):
+                    pass
+
+    def update_tree_name(self):
+        val = self.ui.name_entry.get_value()
+
+        item = self.ui.tree_widget.currentItem()
+        if item is None:
+            return
+        # I'm setting the value for the second column (designated by 1) because first column holds the ID
+        # and second column holds the Name (this behavior is set in the build_ui method)
+        item.setData(1, QtCore.Qt.DisplayRole, val)
+
+    def update_storage(self):
+        """
+        Update the dictionary that is the storage of the tools 'database'
+        :return:
+        """
+
+        tool_id = str(self.current_toolid)
+
+        try:
+            wdg = self.sender()
+
+            assert isinstance(wdg, QtWidgets.QWidget) or isinstance(wdg, QtWidgets.QAction), \
+                "Expected a QWidget got %s" % type(wdg)
+
+            if wdg is None:
+                return
+
+            if isinstance(wdg, FCButton) or isinstance(wdg, QtWidgets.QAction):
+                # this is called when adding a new tool; no need to run the update below since that section is for
+                # when editing a tool
+                self.on_tools_db_edited()
+                return
+
+            wdg_name = wdg.objectName()
+            val = wdg.get_value()
+        except AttributeError as err:
+            self.app.log.debug("ToolsDB2.update_storage() -> %s" % str(err))
+            return
+
+        # #############################################################################################################
+        # #############################################################################################################
+        # ################ EDITING PARAMETERS IN A TOOL SECTION
+        # #############################################################################################################
+        # #############################################################################################################
+
+        # #############################################################################################################
+        # this might change in the future; it makes sense to change values at once for all tools
+        # for now change values only for one tool at once
+        sel_rows = []
+        for item in self.ui.tree_widget.selectedItems():
+            sel_rows.append(item.data(0, QtCore.Qt.DisplayRole))
+
+        len_sel_rows = len(sel_rows)
+        if len_sel_rows > 1:
+            msg = '[ERROR_NOTCL] %s: %s' % \
+                  (_("To change tool properties select only one tool. Tools currently selected"), str(len_sel_rows))
+            self.app.inform.emit(msg)
+            old_value = self.db_tool_dict[tool_id]['data'][self.name2option[wdg_name]]
+            wdg.set_value(old_value)
+            wdg.clearFocus()
+            return
+        # #############################################################################################################
+
+        if wdg_name == "gdb_name":
+            self.db_tool_dict[tool_id]['name'] = val
+        elif wdg_name == "gdb_dia":
+            self.db_tool_dict[tool_id]['tooldia'] = val
+        elif wdg_name == "gdb_tool_offset":
+            self.db_tool_dict[tool_id]['offset'] = val
+        elif wdg_name == "gdb_custom_offset":
+            self.db_tool_dict[tool_id]['offset_value'] = val
+        elif wdg_name == "gdb_type":
+            self.db_tool_dict[tool_id]['type'] = val
+        elif wdg_name == "gdb_shape":
+            self.db_tool_dict[tool_id]['tool_type'] = val
+        else:
+            # Milling Tool
+            if wdg_name == "gdb_tool_target":
+                self.db_tool_dict[tool_id]['data']['tool_target'] = val
+            elif wdg_name == "gdb_tol_min":
+                self.db_tool_dict[tool_id]['data']['tol_min'] = val
+            elif wdg_name == "gdb_tol_max":
+                self.db_tool_dict[tool_id]['data']['tol_max'] = val
+
+            elif wdg_name == "gdb_cutz":
+                self.db_tool_dict[tool_id]['data']['cutz'] = val
+            elif wdg_name == "gdb_multidepth":
+                self.db_tool_dict[tool_id]['data']['multidepth'] = val
+            elif wdg_name == "gdb_multidepth_entry":
+                self.db_tool_dict[tool_id]['data']['depthperpass'] = val
+
+            elif wdg_name == "gdb_travelz":
+                self.db_tool_dict[tool_id]['data']['travelz'] = val
+            elif wdg_name == "gdb_frxy":
+                self.db_tool_dict[tool_id]['data']['feedrate'] = val
+            elif wdg_name == "gdb_frz":
+                self.db_tool_dict[tool_id]['data']['feedrate_z'] = val
+            elif wdg_name == "gdb_spindle":
+                self.db_tool_dict[tool_id]['data']['spindlespeed'] = val
+            elif wdg_name == "gdb_dwell":
+                self.db_tool_dict[tool_id]['data']['dwell'] = val
+            elif wdg_name == "gdb_dwelltime":
+                self.db_tool_dict[tool_id]['data']['dwelltime'] = val
+
+            elif wdg_name == "gdb_vdia":
+                self.db_tool_dict[tool_id]['data']['vtipdia'] = val
+            elif wdg_name == "gdb_vangle":
+                self.db_tool_dict[tool_id]['data']['vtipangle'] = val
+            elif wdg_name == "gdb_frapids":
+                self.db_tool_dict[tool_id]['data']['feedrate_rapid'] = val
+            elif wdg_name == "gdb_ecut":
+                self.db_tool_dict[tool_id]['data']['extracut'] = val
+            elif wdg_name == "gdb_ecut_length":
+                self.db_tool_dict[tool_id]['data']['extracut_length'] = val
+
+            # NCC Tool
+            elif wdg_name == "gdb_n_operation":
+                self.db_tool_dict[tool_id]['data']['tools_ncc_operation'] = val
+            elif wdg_name == "gdb_n_overlap":
+                self.db_tool_dict[tool_id]['data']['tools_ncc_overlap'] = val
+            elif wdg_name == "gdb_n_margin":
+                self.db_tool_dict[tool_id]['data']['tools_ncc_margin'] = val
+            elif wdg_name == "gdb_n_method":
+                self.db_tool_dict[tool_id]['data']['tools_ncc_method'] = val
+            elif wdg_name == "gdb_n_connect":
+                self.db_tool_dict[tool_id]['data']['tools_ncc_connect'] = val
+            elif wdg_name == "gdb_n_contour":
+                self.db_tool_dict[tool_id]['data']['tools_ncc_contour'] = val
+            elif wdg_name == "gdb_n_offset":
+                self.db_tool_dict[tool_id]['data']['tools_ncc_offset_choice'] = val
+            elif wdg_name == "gdb_n_offset_value":
+                self.db_tool_dict[tool_id]['data']['tools_ncc_offset_value'] = val
+            elif wdg_name == "gdb_n_milling_type":
+                self.db_tool_dict[tool_id]['data']['tools_ncc_milling_type'] = val
+
+            # Paint Tool
+            elif wdg_name == "gdb_p_overlap":
+                self.db_tool_dict[tool_id]['data']['tools_paint_overlap'] = val
+            elif wdg_name == "gdb_p_offset":
+                self.db_tool_dict[tool_id]['data']['tools_paint_offset'] = val
+            elif wdg_name == "gdb_p_method":
+                self.db_tool_dict[tool_id]['data']['tools_paint_method'] = val
+            elif wdg_name == "gdb_p_connect":
+                self.db_tool_dict[tool_id]['data']['tools_paint_connect'] = val
+            elif wdg_name == "gdb_p_contour":
+                self.db_tool_dict[tool_id]['data']['tools_paint_contour'] = val
+
+            # Isolation Tool
+            elif wdg_name == "gdb_i_passes":
+                self.db_tool_dict[tool_id]['data']['tools_iso_passes'] = val
+            elif wdg_name == "gdb_i_overlap":
+                self.db_tool_dict[tool_id]['data']['tools_iso_overlap'] = val
+            elif wdg_name == "gdb_i_milling_type":
+                self.db_tool_dict[tool_id]['data']['tools_iso_milling_type'] = val
+            elif wdg_name == "gdb_i_follow":
+                self.db_tool_dict[tool_id]['data']['tools_iso_follow'] = val
+            elif wdg_name == "gdb_i_iso_type":
+                self.db_tool_dict[tool_id]['data']['tools_iso_isotype'] = val
+
+            # Drilling Tool
+            elif wdg_name == "gdb_e_cutz":
+                self.db_tool_dict[tool_id]['data']['tools_drill_cutz'] = val
+            elif wdg_name == "gdb_e_multidepth":
+                self.db_tool_dict[tool_id]['data']['tools_drill_multidepth'] = val
+            elif wdg_name == "gdb_e_depthperpass":
+                self.db_tool_dict[tool_id]['data']['tools_drill_depthperpass'] = val
+            elif wdg_name == "gdb_e_travelz":
+                self.db_tool_dict[tool_id]['data']['tools_drill_travelz'] = val
+
+            elif wdg_name == "gdb_e_feedratez":
+                self.db_tool_dict[tool_id]['data']['tools_drill_feedrate_z'] = val
+            elif wdg_name == "gdb_e_fr_rapid":
+                self.db_tool_dict[tool_id]['data']['tools_drill_feedrate_rapid'] = val
+            elif wdg_name == "gdb_e_spindlespeed":
+                self.db_tool_dict[tool_id]['data']['tools_drill_spindlespeed'] = val
+            elif wdg_name == "gdb_e_dwell":
+                self.db_tool_dict[tool_id]['data']['tools_drill_dwell'] = val
+            elif wdg_name == "gdb_e_dwelltime":
+                self.db_tool_dict[tool_id]['data']['tools_drill_dwelltime'] = val
+
+            elif wdg_name == "gdb_e_offset":
+                self.db_tool_dict[tool_id]['data']['tools_drill_offset'] = val
+            elif wdg_name == "gdb_e_drill_slots":
+                self.db_tool_dict[tool_id]['data']['tools_drill_drill_slots'] = val
+            elif wdg_name == "gdb_e_drill_slots_over":
+                self.db_tool_dict[tool_id]['data']['tools_drill_drill_overlap'] = val
+            elif wdg_name == "gdb_e_drill_last_drill":
+                self.db_tool_dict[tool_id]['data']['tools_drill_last_drill'] = val
+
+            # Cutout Tool
+            elif wdg_name == "gdb_ct_margin":
+                self.db_tool_dict[tool_id]['data']['tools_cutout_margin'] = val
+            elif wdg_name == "gdb_ct_gapsize":
+                self.db_tool_dict[tool_id]['data']['tools_cutout_gapsize'] = val
+            elif wdg_name == "gdb_ct_gaps":
+                self.db_tool_dict[tool_id]['data']['tools_cutout_gaps_ff'] = val
+            elif wdg_name == "gdb_ct_convex":
+                self.db_tool_dict[tool_id]['data']['tools_cutout_convexshape'] = val
+
+            elif wdg_name == "gdb_ct_gap_type":
+                self.db_tool_dict[tool_id]['data']['tools_cutout_gap_type'] = val
+            elif wdg_name == "gdb_ct_gap_depth":
+                self.db_tool_dict[tool_id]['data']['tools_cutout_gap_depth'] = val
+            elif wdg_name == "gdb_ct_mb_dia":
+                self.db_tool_dict[tool_id]['data']['tools_cutout_mb_dia'] = val
+            elif wdg_name == "gdb_ct_mb_spacing":
+                self.db_tool_dict[tool_id]['data']['tools_cutout_mb_spacing'] = val
+
+        self.on_tools_db_edited()
+
+    def on_tool_requested_from_app(self):
+        if not self.ui.tree_widget.selectedItems():
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("No Tool/row selected in the Tools Database table"))
+            return
+
+        if not self.db_tool_dict:
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Tools DB empty."))
+            return
+
+        for item in self.ui.tree_widget.selectedItems():
+            tool_uid = item.data(0, QtCore.Qt.DisplayRole)
+
+            for key in self.db_tool_dict.keys():
+                if str(key) == str(tool_uid):
+                    selected_tool = self.db_tool_dict[key]
+                    self.on_tool_request(tool=selected_tool)
+
+    def on_tools_db_edited(self, silent=None):
+        """
+        Executed whenever a tool is edited in Tools Database.
+        Will color the text of the Tools Database tab to Red color.
+
+        :return:
+        """
+
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+                self.app.ui.plot_tab_area.tabBar.setTabTextColor(idx, QtGui.QColor('red'))
+
+        self.ui.save_db_btn.setStyleSheet("QPushButton {color: red;}")
+
+        self.tools_db_changed_flag = True
+        if silent is None:
+            msg = '[WARNING_NOTCL] %s' % _("Tools in Tools Database edited but not saved.")
+            self.app.inform[str, bool].emit(msg, False)
+
+    def on_cancel_tool(self):
+        for idx in range(self.app_ui.plot_tab_area.count()):
+            if self.app_ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+                wdg = self.app_ui.plot_tab_area.widget(idx)
+                wdg.deleteLater()
+                self.app_ui.plot_tab_area.removeTab(idx)
+        self.app.inform.emit('%s' % _("Cancelled adding tool from DB."))
+
+    # def resize_new_tool_table_widget(self, min_size, max_size):
+    #     """
+    #     Resize the table widget responsible for adding new tool in the Tool Database
+    #
+    #     :param min_size: passed by rangeChanged signal or the self.new_tool_table_widget.horizontalScrollBar()
+    #     :param max_size: passed by rangeChanged signal or the self.new_tool_table_widget.horizontalScrollBar()
+    #     :return:
+    #     """
+    #     t_height = self.t_height
+    #     if max_size > min_size:
+    #         t_height = self.t_height + self.new_tool_table_widget.verticalScrollBar().height()
+    #
+    #     self.new_tool_table_widget.setMaximumHeight(t_height)
+
+    def closeEvent(self, QCloseEvent):
+        super().closeEvent(QCloseEvent)
+
+
+# class ToolsDB(QtWidgets.QWidget):
+#
+#     mark_tools_rows = QtCore.pyqtSignal()
+#
+#     def __init__(self, app, callback_on_edited, callback_on_tool_request, parent=None):
+#         super(ToolsDB, self).__init__(parent)
+#
+#         self.app = app
+#         self.decimals = 4
+#         self.callback_app = callback_on_edited
+#
+#         self.on_tool_request = callback_on_tool_request
+#
+#         self.offset_item_options = ["Path", "In", "Out", "Custom"]
+#         self.type_item_options = ["Iso", "Rough", "Finish"]
+#         self.tool_type_item_options = ["C1", "C2", "C3", "C4", "B", "V"]
+#
+#         '''
+#         dict to hold all the tools in the Tools DB
+#         format:
+#         {
+#             tool_id: {
+#                 'name': 'new_tool'
+#                 'tooldia': self.app.defaults["geometry_cnctooldia"]
+#                 'offset': 'Path'
+#                 'offset_value': 0.0
+#                 'type':  'Rough',
+#                 'tool_type': 'C1'
+#                 'data': dict()
+#             }
+#         }
+#         '''
+#         self.db_tool_dict = {}
+#
+#         # layouts
+#         layout = QtWidgets.QVBoxLayout()
+#         self.setLayout(layout)
+#
+#         table_hlay = QtWidgets.QHBoxLayout()
+#         layout.addLayout(table_hlay)
+#
+#         self.table_widget = FCTable(drag_drop=True)
+#         self.table_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+#         table_hlay.addWidget(self.table_widget)
+#
+#         # set the number of columns and the headers tool tips
+#         self.configure_table()
+#
+#         # pal = QtGui.QPalette()
+#         # pal.setColor(QtGui.QPalette.Background, Qt.white)
+#
+#         # New Bookmark
+#         new_vlay = QtWidgets.QVBoxLayout()
+#         layout.addLayout(new_vlay)
+#
+#         # new_tool_lbl = FCLabel('<b>%s</b>' % _("New Tool"))
+#         # new_vlay.addWidget(new_tool_lbl, alignment=QtCore.Qt.AlignBottom)
+#
+#         self.buttons_frame = QtWidgets.QFrame()
+#         self.buttons_frame.setContentsMargins(0, 0, 0, 0)
+#         layout.addWidget(self.buttons_frame)
+#         self.buttons_box = QtWidgets.QHBoxLayout()
+#         self.buttons_box.setContentsMargins(0, 0, 0, 0)
+#         self.buttons_frame.setLayout(self.buttons_box)
+#         self.buttons_frame.show()
+#
+#         add_entry_btn = FCButton(_("Add Geometry Tool in DB"))
+#         add_entry_btn.setToolTip(
+#             _("Add a new tool in the Tools Database.\n"
+#               "It will be used in the Geometry UI.\n"
+#               "You can edit it after it is added.")
+#         )
+#         self.buttons_box.addWidget(add_entry_btn)
+#
+#         # add_fct_entry_btn = FCButton(_("Add Paint/NCC Tool in DB"))
+#         # add_fct_entry_btn.setToolTip(
+#         #     _("Add a new tool in the Tools Database.\n"
+#         #       "It will be used in the Paint/NCC Tools UI.\n"
+#         #       "You can edit it after it is added.")
+#         # )
+#         # self.buttons_box.addWidget(add_fct_entry_btn)
+#
+#         remove_entry_btn = FCButton(_("Delete Tool from DB"))
+#         remove_entry_btn.setToolTip(
+#             _("Remove a selection of tools in the Tools Database.")
+#         )
+#         self.buttons_box.addWidget(remove_entry_btn)
+#
+#         export_db_btn = FCButton(_("Export DB"))
+#         export_db_btn.setToolTip(
+#             _("Save the Tools Database to a custom text file.")
+#         )
+#         self.buttons_box.addWidget(export_db_btn)
+#
+#         import_db_btn = FCButton(_("Import DB"))
+#         import_db_btn.setToolTip(
+#             _("Load the Tools Database information's from a custom text file.")
+#         )
+#         self.buttons_box.addWidget(import_db_btn)
+#
+#         self.add_tool_from_db = FCButton(_("Transfer the Tool"))
+#         self.add_tool_from_db.setToolTip(
+#             _("Add a new tool in the Tools Table of the\n"
+#               "active Geometry object after selecting a tool\n"
+#               "in the Tools Database.")
+#         )
+#         self.add_tool_from_db.hide()
+#
+#         self.cancel_tool_from_db = FCButton(_("Cancel"))
+#         self.cancel_tool_from_db.hide()
+#
+#         hlay = QtWidgets.QHBoxLayout()
+#         layout.addLayout(hlay)
+#         hlay.addWidget(self.add_tool_from_db)
+#         hlay.addWidget(self.cancel_tool_from_db)
+#         hlay.addStretch()
+#
+#         # ##############################################################################
+#         # ######################## SIGNALS #############################################
+#         # ##############################################################################
+#
+#         add_entry_btn.clicked.connect(self.on_tool_add)
+#         remove_entry_btn.clicked.connect(self.on_tool_delete)
+#         export_db_btn.clicked.connect(self.on_export_tools_db_file)
+#         import_db_btn.clicked.connect(self.on_import_tools_db_file)
+#         # closebtn.clicked.connect(self.accept)
+#
+#         self.add_tool_from_db.clicked.connect(self.on_tool_requested_from_app)
+#         self.cancel_tool_from_db.clicked.connect(self.on_cancel_tool)
+#
+#         self.setup_db_ui()
+#
+#     def configure_table(self):
+#         self.table_widget.setColumnCount(27)
+#         # self.table_widget.setColumnWidth(0, 20)
+#         self.table_widget.setHorizontalHeaderLabels(
+#             [
+#                 '#',
+#                 _("Tool Name"),
+#                 _("Tool Dia"),
+#                 _("Tool Offset"),
+#                 _("Custom Offset"),
+#                 _("Tool Type"),
+#                 _("Tool Shape"),
+#                 _("Cut Z"),
+#                 _("MultiDepth"),
+#                 _("DPP"),
+#                 _("V-Dia"),
+#                 _("V-Angle"),
+#                 _("Travel Z"),
+#                 _("FR"),
+#                 _("FR Z"),
+#                 _("FR Rapids"),
+#                 _("Spindle Speed"),
+#                 _("Dwell"),
+#                 _("Dwelltime"),
+#                 _("Preprocessor"),
+#                 _("ExtraCut"),
+#                 _("E-Cut Length"),
+#                 _("Toolchange"),
+#                 _("Toolchange XY"),
+#                 _("Toolchange Z"),
+#                 _("Start Z"),
+#                 _("End Z"),
+#             ]
+#         )
+#         self.table_widget.horizontalHeaderItem(0).setToolTip(
+#             _("Tool Index."))
+#         self.table_widget.horizontalHeaderItem(1).setToolTip(
+#             _("Tool name.\n"
+#               "This is not used in the app, it's function\n"
+#               "is to serve as a note for the user."))
+#         self.table_widget.horizontalHeaderItem(2).setToolTip(
+#             _("Tool Diameter."))
+#         self.table_widget.horizontalHeaderItem(3).setToolTip(
+#             _("Tool Offset.\n"
+#               "Can be of a few types:\n"
+#               "Path = zero offset\n"
+#               "In = offset inside by half of tool diameter\n"
+#               "Out = offset outside by half of tool diameter\n"
+#               "Custom = custom offset using the Custom Offset value"))
+#         self.table_widget.horizontalHeaderItem(4).setToolTip(
+#             _("Custom Offset.\n"
+#               "A value to be used as offset from the current path."))
+#         self.table_widget.horizontalHeaderItem(5).setToolTip(
+#             _("Tool Type.\n"
+#               "Can be:\n"
+#               "Iso = isolation cut\n"
+#               "Rough = rough cut, low feedrate, multiple passes\n"
+#               "Finish = finishing cut, high feedrate"))
+#         self.table_widget.horizontalHeaderItem(6).setToolTip(
+#             _("Tool Shape. \n"
+#               "Can be:\n"
+#               "C1 ... C4 = circular tool with x flutes\n"
+#               "B = ball tip milling tool\n"
+#               "V = v-shape milling tool"))
+#         self.table_widget.horizontalHeaderItem(7).setToolTip(
+#             _("Cutting Depth.\n"
+#               "The depth at which to cut into material."))
+#         self.table_widget.horizontalHeaderItem(8).setToolTip(
+#             _("Multi Depth.\n"
+#               "Selecting this will allow cutting in multiple passes,\n"
+#               "each pass adding a DPP parameter depth."))
+#         self.table_widget.horizontalHeaderItem(9).setToolTip(
+#             _("DPP. Depth per Pass.\n"
+#               "The value used to cut into material on each pass."))
+#         self.table_widget.horizontalHeaderItem(10).setToolTip(
+#             _("V-Dia.\n"
+#               "Diameter of the tip for V-Shape Tools."))
+#         self.table_widget.horizontalHeaderItem(11).setToolTip(
+#             _("V-Agle.\n"
+#               "Angle at the tip for the V-Shape Tools."))
+#         self.table_widget.horizontalHeaderItem(12).setToolTip(
+#             _("Clearance Height.\n"
+#               "Height at which the milling bit will travel between cuts,\n"
+#               "above the surface of the material, avoiding all fixtures."))
+#         self.table_widget.horizontalHeaderItem(13).setToolTip(
+#             _("FR. Feedrate\n"
+#               "The speed on XY plane used while cutting into material."))
+#         self.table_widget.horizontalHeaderItem(14).setToolTip(
+#             _("FR Z. Feedrate Z\n"
+#               "The speed on Z plane."))
+#         self.table_widget.horizontalHeaderItem(15).setToolTip(
+#             _("FR Rapids. Feedrate Rapids\n"
+#               "Speed used while moving as fast as possible.\n"
+#               "This is used only by some devices that can't use\n"
+#               "the G0 g-code command. Mostly 3D printers."))
+#         self.table_widget.horizontalHeaderItem(16).setToolTip(
+#             _("Spindle Speed.\n"
+#               "If it's left empty it will not be used.\n"
+#               "The speed of the spindle in RPM."))
+#         self.table_widget.horizontalHeaderItem(17).setToolTip(
+#             _("Dwell.\n"
+#               "Check this if a delay is needed to allow\n"
+#               "the spindle motor to reach its set speed."))
+#         self.table_widget.horizontalHeaderItem(18).setToolTip(
+#             _("Dwell Time.\n"
+#               "A delay used to allow the motor spindle reach its set speed."))
+#         self.table_widget.horizontalHeaderItem(19).setToolTip(
+#             _("Preprocessor.\n"
+#               "A selection of files that will alter the generated G-code\n"
+#               "to fit for a number of use cases."))
+#         self.table_widget.horizontalHeaderItem(20).setToolTip(
+#             _("Extra Cut.\n"
+#               "If checked, after a isolation is finished an extra cut\n"
+#               "will be added where the start and end of isolation meet\n"
+#               "such as that this point is covered by this extra cut to\n"
+#               "ensure a complete isolation."))
+#         self.table_widget.horizontalHeaderItem(21).setToolTip(
+#             _("Extra Cut length.\n"
+#               "If checked, after a isolation is finished an extra cut\n"
+#               "will be added where the start and end of isolation meet\n"
+#               "such as that this point is covered by this extra cut to\n"
+#               "ensure a complete isolation. This is the length of\n"
+#               "the extra cut."))
+#         self.table_widget.horizontalHeaderItem(22).setToolTip(
+#             _("Toolchange.\n"
+#               "It will create a toolchange event.\n"
+#               "The kind of toolchange is determined by\n"
+#               "the preprocessor file."))
+#         self.table_widget.horizontalHeaderItem(23).setToolTip(
+#             _("Toolchange XY.\n"
+#               "A set of coordinates in the format (x, y).\n"
+#               "Will determine the cartesian position of the point\n"
+#               "where the tool change event take place."))
+#         self.table_widget.horizontalHeaderItem(24).setToolTip(
+#             _("Toolchange Z.\n"
+#               "The position on Z plane where the tool change event take place."))
+#         self.table_widget.horizontalHeaderItem(25).setToolTip(
+#             _("Start Z.\n"
+#               "If it's left empty it will not be used.\n"
+#               "A position on Z plane to move immediately after job start."))
+#         self.table_widget.horizontalHeaderItem(26).setToolTip(
+#             _("End Z.\n"
+#               "A position on Z plane to move immediately after job stop."))
+#
+#     def setup_db_ui(self):
+#         filename = self.app.tools_database_path()
+#
+#         # load the database tools from the file
+#         try:
+#             with open(filename) as f:
+#                 tools = f.read()
+#         except IOError:
+#             self.app.log.error("Could not load tools DB file.")
+#             self.app.inform.emit('[ERROR] %s' % _("Could not load Tools DB file."))
+#             return
+#
+#         try:
+#             self.db_tool_dict = json.loads(tools)
+#         except Exception:
+#             e = sys.exc_info()[0]
+#             self.app.log.error(str(e))
+#             self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
+#             return
+#
+#         self.app.inform.emit('[success] %s: %s' % (_("Loaded Tools DB from"), filename))
+#
+#         self.build_db_ui()
+#
+#         self.table_widget.setupContextMenu()
+#         self.table_widget.addContextMenu(
+#             _("Add to DB"), self.on_tool_add, icon=QtGui.QIcon(self.app.resource_location + "/plus16.png"))
+#         self.table_widget.addContextMenu(
+#             _("Copy from DB"), self.on_tool_copy, icon=QtGui.QIcon(self.app.resource_location + "/copy16.png"))
+#         self.table_widget.addContextMenu(
+#             _("Delete from DB"), self.on_tool_delete, icon=QtGui.QIcon(self.app.resource_location + "/delete32.png"))
+#
+#     def build_db_ui(self):
+#         self.ui_disconnect()
+#         self.table_widget.setRowCount(len(self.db_tool_dict))
+#
+#         nr_crt = 0
+#
+#         for toolid, dict_val in self.db_tool_dict.items():
+#             row = nr_crt
+#             nr_crt += 1
+#
+#             t_name = dict_val['name']
+#             try:
+#                 self.add_tool_table_line(row, name=t_name, widget=self.table_widget, tooldict=dict_val)
+#             except Exception as e:
+#                 self.app.log.debug("ToolDB.build_db_ui.add_tool_table_line() --> %s" % str(e))
+#             vertical_header = self.table_widget.verticalHeader()
+#             vertical_header.hide()
+#
+#             horizontal_header = self.table_widget.horizontalHeader()
+#             horizontal_header.setMinimumSectionSize(10)
+#             horizontal_header.setDefaultSectionSize(70)
+#
+#             self.table_widget.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+#             for x in range(27):
+#                 self.table_widget.resizeColumnToContents(x)
+#
+#             horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+#             # horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+#             # horizontal_header.setSectionResizeMode(13, QtWidgets.QHeaderView.Fixed)
+#
+#             horizontal_header.resizeSection(0, 20)
+#             # horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
+#             # horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
+#
+#         self.ui_connect()
+#
+#     def add_tool_table_line(self, row, name, widget, tooldict):
+#         data = tooldict['data']
+#
+#         nr_crt = row + 1
+#         id_item = QtWidgets.QTableWidgetItem('%d' % int(nr_crt))
+#         # id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+#         flags = id_item.flags() & ~QtCore.Qt.ItemIsEditable
+#         id_item.setFlags(flags)
+#         widget.setItem(row, 0, id_item)  # Tool name/id
+#
+#         tool_name_item = QtWidgets.QTableWidgetItem(name)
+#         widget.setItem(row, 1, tool_name_item)
+#
+#         dia_item = FCDoubleSpinner()
+#         dia_item.set_precision(self.decimals)
+#         dia_item.setSingleStep(0.1)
+#         dia_item.set_range(0.0, 10000.0000)
+#         dia_item.set_value(float(tooldict['tooldia']))
+#         widget.setCellWidget(row, 2, dia_item)
+#
+#         tool_offset_item = FCComboBox()
+#         for item in self.offset_item_options:
+#             tool_offset_item.addItem(item)
+#         tool_offset_item.set_value(tooldict['offset'])
+#         widget.setCellWidget(row, 3, tool_offset_item)
+#
+#         c_offset_item = FCDoubleSpinner()
+#         c_offset_item.set_precision(self.decimals)
+#         c_offset_item.setSingleStep(0.1)
+#         c_offset_item.set_range(-10000.0000, 10000.0000)
+#         c_offset_item.set_value(float(tooldict['offset_value']))
+#         widget.setCellWidget(row, 4, c_offset_item)
+#
+#         tt_item = FCComboBox()
+#         for item in self.type_item_options:
+#             tt_item.addItem(item)
+#         tt_item.set_value(tooldict['type'])
+#         widget.setCellWidget(row, 5, tt_item)
+#
+#         tshape_item = FCComboBox()
+#         for item in self.tool_type_item_options:
+#             tshape_item.addItem(item)
+#         tshape_item.set_value(tooldict['tool_type'])
+#         widget.setCellWidget(row, 6, tshape_item)
+#
+#         cutz_item = FCDoubleSpinner()
+#         cutz_item.set_precision(self.decimals)
+#         cutz_item.setSingleStep(0.1)
+#         if self.app.defaults['global_machinist_setting']:
+#             cutz_item.set_range(-10000.0000, 10000.0000)
+#         else:
+#             cutz_item.set_range(-10000.0000, -0.0000)
+#
+#         cutz_item.set_value(float(data['cutz']))
+#         widget.setCellWidget(row, 7, cutz_item)
+#
+#         multidepth_item = FCCheckBox()
+#         multidepth_item.set_value(data['multidepth'])
+#         widget.setCellWidget(row, 8, multidepth_item)
+#
+#         # to make the checkbox centered but it can no longer have it's value accessed - needs a fix using findchild()
+#         # multidepth_item = QtWidgets.QWidget()
+#         # cb = FCCheckBox()
+#         # cb.set_value(data['multidepth'])
+#         # qhboxlayout = QtWidgets.QHBoxLayout(multidepth_item)
+#         # qhboxlayout.addWidget(cb)
+#         # qhboxlayout.setAlignment(QtCore.Qt.AlignCenter)
+#         # qhboxlayout.setContentsMargins(0, 0, 0, 0)
+#         # widget.setCellWidget(row, 8, multidepth_item)
+#
+#         depth_per_pass_item = FCDoubleSpinner()
+#         depth_per_pass_item.set_precision(self.decimals)
+#         depth_per_pass_item.setSingleStep(0.1)
+#         depth_per_pass_item.set_range(0.0, 10000.0000)
+#         depth_per_pass_item.set_value(float(data['depthperpass']))
+#         widget.setCellWidget(row, 9, depth_per_pass_item)
+#
+#         vtip_dia_item = FCDoubleSpinner()
+#         vtip_dia_item.set_precision(self.decimals)
+#         vtip_dia_item.setSingleStep(0.1)
+#         vtip_dia_item.set_range(0.0, 10000.0000)
+#         vtip_dia_item.set_value(float(data['vtipdia']))
+#         widget.setCellWidget(row, 10, vtip_dia_item)
+#
+#         vtip_angle_item = FCDoubleSpinner()
+#         vtip_angle_item.set_precision(self.decimals)
+#         vtip_angle_item.setSingleStep(0.1)
+#         vtip_angle_item.set_range(-360.0, 360.0)
+#         vtip_angle_item.set_value(float(data['vtipangle']))
+#         widget.setCellWidget(row, 11, vtip_angle_item)
+#
+#         travelz_item = FCDoubleSpinner()
+#         travelz_item.set_precision(self.decimals)
+#         travelz_item.setSingleStep(0.1)
+#         if self.app.defaults['global_machinist_setting']:
+#             travelz_item.set_range(-10000.0000, 10000.0000)
+#         else:
+#             travelz_item.set_range(0.0000, 10000.0000)
+#
+#         travelz_item.set_value(float(data['travelz']))
+#         widget.setCellWidget(row, 12, travelz_item)
+#
+#         fr_item = FCDoubleSpinner()
+#         fr_item.set_precision(self.decimals)
+#         fr_item.set_range(0.0, 10000.0000)
+#         fr_item.set_value(float(data['feedrate']))
+#         widget.setCellWidget(row, 13, fr_item)
+#
+#         frz_item = FCDoubleSpinner()
+#         frz_item.set_precision(self.decimals)
+#         frz_item.set_range(0.0, 10000.0000)
+#         frz_item.set_value(float(data['feedrate_z']))
+#         widget.setCellWidget(row, 14, frz_item)
+#
+#         frrapids_item = FCDoubleSpinner()
+#         frrapids_item.set_precision(self.decimals)
+#         frrapids_item.set_range(0.0, 10000.0000)
+#         frrapids_item.set_value(float(data['feedrate_rapid']))
+#         widget.setCellWidget(row, 15, frrapids_item)
+#
+#         spindlespeed_item = FCSpinner()
+#         spindlespeed_item.set_range(0, 1000000)
+#         spindlespeed_item.set_value(int(data['spindlespeed']))
+#         spindlespeed_item.set_step(100)
+#         widget.setCellWidget(row, 16, spindlespeed_item)
+#
+#         dwell_item = FCCheckBox()
+#         dwell_item.set_value(data['dwell'])
+#         widget.setCellWidget(row, 17, dwell_item)
+#
+#         dwelltime_item = FCDoubleSpinner()
+#         dwelltime_item.set_precision(self.decimals)
+#         dwelltime_item.set_range(0.0000, 10000.0000)
+#         dwelltime_item.set_value(float(data['dwelltime']))
+#         widget.setCellWidget(row, 18, dwelltime_item)
+#
+#         pp_item = FCComboBox()
+#         for item in self.app.preprocessors:
+#             pp_item.addItem(item)
+#         pp_item.set_value(data['ppname_g'])
+#         widget.setCellWidget(row, 19, pp_item)
+#
+#         ecut_item = FCCheckBox()
+#         ecut_item.set_value(data['extracut'])
+#         widget.setCellWidget(row, 20, ecut_item)
+#
+#         ecut_length_item = FCDoubleSpinner()
+#         ecut_length_item.set_precision(self.decimals)
+#         ecut_length_item.set_range(0.0000, 10000.0000)
+#         ecut_length_item.set_value(data['extracut_length'])
+#         widget.setCellWidget(row, 21, ecut_length_item)
+#
+#         toolchange_item = FCCheckBox()
+#         toolchange_item.set_value(data['toolchange'])
+#         widget.setCellWidget(row, 22, toolchange_item)
+#
+#         toolchangexy_item = QtWidgets.QTableWidgetItem(str(data['toolchangexy']) if data['toolchangexy'] else '')
+#         widget.setItem(row, 23, toolchangexy_item)
+#
+#         toolchangez_item = FCDoubleSpinner()
+#         toolchangez_item.set_precision(self.decimals)
+#         toolchangez_item.setSingleStep(0.1)
+#         if self.app.defaults['global_machinist_setting']:
+#             toolchangez_item.set_range(-10000.0000, 10000.0000)
+#         else:
+#             toolchangez_item.set_range(0.0000, 10000.0000)
+#
+#         toolchangez_item.set_value(float(data['toolchangez']))
+#         widget.setCellWidget(row, 24, toolchangez_item)
+#
+#         startz_item = QtWidgets.QTableWidgetItem(str(data['startz']) if data['startz'] else '')
+#         widget.setItem(row, 25, startz_item)
+#
+#         endz_item = FCDoubleSpinner()
+#         endz_item.set_precision(self.decimals)
+#         endz_item.setSingleStep(0.1)
+#         if self.app.defaults['global_machinist_setting']:
+#             endz_item.set_range(-10000.0000, 10000.0000)
+#         else:
+#             endz_item.set_range(0.0000, 10000.0000)
+#
+#         endz_item.set_value(float(data['endz']))
+#         widget.setCellWidget(row, 26, endz_item)
+#
+#     def on_tool_add(self):
+#         """
+#         Add a tool in the DB Tool Table
+#         :return: None
+#         """
+#
+#         default_data = {}
+#         default_data.update({
+#             "cutz": float(self.app.defaults["geometry_cutz"]),
+#             "multidepth": self.app.defaults["geometry_multidepth"],
+#             "depthperpass": float(self.app.defaults["geometry_depthperpass"]),
+#             "vtipdia": float(self.app.defaults["geometry_vtipdia"]),
+#             "vtipangle": float(self.app.defaults["geometry_vtipangle"]),
+#             "travelz": float(self.app.defaults["geometry_travelz"]),
+#             "feedrate": float(self.app.defaults["geometry_feedrate"]),
+#             "feedrate_z": float(self.app.defaults["geometry_feedrate_z"]),
+#             "feedrate_rapid": float(self.app.defaults["geometry_feedrate_rapid"]),
+#             "spindlespeed": self.app.defaults["geometry_spindlespeed"],
+#             "dwell": self.app.defaults["geometry_dwell"],
+#             "dwelltime": float(self.app.defaults["geometry_dwelltime"]),
+#             "ppname_g": self.app.defaults["geometry_ppname_g"],
+#             "extracut": self.app.defaults["geometry_extracut"],
+#             "extracut_length": float(self.app.defaults["geometry_extracut_length"]),
+#             "toolchange": self.app.defaults["geometry_toolchange"],
+#             "toolchangexy": self.app.defaults["geometry_toolchangexy"],
+#             "toolchangez": float(self.app.defaults["geometry_toolchangez"]),
+#             "startz": self.app.defaults["geometry_startz"],
+#             "endz": float(self.app.defaults["geometry_endz"])
+#         })
+#
+#         dict_elem = {}
+#         dict_elem['name'] = 'new_tool'
+#         if type(self.app.defaults["geometry_cnctooldia"]) == float:
+#             dict_elem['tooldia'] = self.app.defaults["geometry_cnctooldia"]
+#         else:
+#             try:
+#                 tools_string = self.app.defaults["geometry_cnctooldia"].split(",")
+#                 tools_diameters = [eval(a) for a in tools_string if a != '']
+#                 dict_elem['tooldia'] = tools_diameters[0] if tools_diameters else 0.0
+#             except Exception as e:
+#                 self.app.log.debug("ToolDB.on_tool_add() --> %s" % str(e))
+#                 return
+#
+#         dict_elem['offset'] = 'Path'
+#         dict_elem['offset_value'] = 0.0
+#         dict_elem['type'] = 'Rough'
+#         dict_elem['tool_type'] = 'C1'
+#         dict_elem['data'] = default_data
+#
+#         new_toolid = len(self.db_tool_dict) + 1
+#         self.db_tool_dict[new_toolid] = deepcopy(dict_elem)
+#
+#         # add the new entry to the Tools DB table
+#         self.build_db_ui()
+#         self.callback_on_edited()
+#         self.app.inform.emit('[success] %s' % _("Tool added to DB."))
+#
+#     def on_tool_copy(self):
+#         """
+#         Copy a selection of Tools in the Tools DB table
+#         :return:
+#         """
+#         new_tool_id = self.table_widget.rowCount() + 1
+#         for model_index in self.table_widget.selectionModel().selectedRows():
+#             # index = QtCore.QPersistentModelIndex(model_index)
+#             old_tool_id = self.table_widget.item(model_index.row(), 0).text()
+#             new_tool_id += 1
+#
+#             for toolid, dict_val in list(self.db_tool_dict.items()):
+#                 if int(old_tool_id) == int(toolid):
+#                     self.db_tool_dict.update({
+#                         new_tool_id: deepcopy(dict_val)
+#                     })
+#
+#         self.build_db_ui()
+#         self.callback_on_edited()
+#         self.app.inform.emit('[success] %s' % _("Tool copied from Tools DB."))
+#
+#     def on_tool_delete(self):
+#         """
+#         Delete a selection of Tools in the Tools DB table
+#         :return:
+#         """
+#         for model_index in self.table_widget.selectionModel().selectedRows():
+#             # index = QtCore.QPersistentModelIndex(model_index)
+#             toolname_to_remove = self.table_widget.item(model_index.row(), 0).text()
+#
+#             for toolid, dict_val in list(self.db_tool_dict.items()):
+#                 if int(toolname_to_remove) == int(toolid):
+#                     # remove from the storage
+#                     self.db_tool_dict.pop(toolid, None)
+#
+#         self.build_db_ui()
+#         self.callback_on_edited()
+#         self.app.inform.emit('[success] %s' % _("Tool removed from Tools DB."))
+#
+#     def on_export_tools_db_file(self):
+#         self.app.defaults.report_usage("on_export_tools_db_file")
+#         self.app.log.debug("on_export_tools_db_file()")
+#
+#         date = str(datetime.today()).rpartition('.')[0]
+#         date = ''.join(c for c in date if c not in ':-')
+#         date = date.replace(' ', '_')
+#
+#         filter__ = "Text File (*.TXT);;All Files (*.*)"
+#         filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Tools Database"),
+#                                                            directory='{l_save}/FlatCAM_{n}_{date}'.format(
+#                                                                l_save=str(self.app.get_last_save_folder()),
+#                                                                n=_("Tools_Database"),
+#                                                                date=date),
+#                                                            ext_filter=filter__)
+#
+#         filename = str(filename)
+#
+#         if filename == "":
+#             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+#             return
+#         else:
+#             try:
+#                 f = open(filename, 'w')
+#                 f.close()
+#             except PermissionError:
+#                 self.app.inform.emit('[WARNING] %s' %
+#                                      _("Permission denied, saving not possible.\n"
+#                                        "Most likely another app is holding the file open and not accessible."))
+#                 return
+#             except IOError:
+#                 self.app.log.debug('Creating a new Tools DB file ...')
+#                 f = open(filename, 'w')
+#                 f.close()
+#             except Exception:
+#                 e = sys.exc_info()[0]
+#                 self.app.log.error("Could not load Tools DB file.")
+#                 self.app.log.error(str(e))
+#                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load Tools DB file."))
+#                 return
+#
+#             # Save update options
+#             try:
+#                 # Save Tools DB in a file
+#                 try:
+#                     with open(filename, "w") as f:
+#                         json.dump(self.db_tool_dict, f, default=to_dict, indent=2)
+#                 except Exception as e:
+#                     self.app.log.debug("App.on_save_tools_db() --> %s" % str(e))
+#                     self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+#                     return
+#             except Exception:
+#                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+#                 return
+#
+#         self.app.inform.emit('[success] %s: %s' % (_("Exported Tools DB to"), filename))
+#
+#     def on_import_tools_db_file(self):
+#         self.app.defaults.report_usage("on_import_tools_db_file")
+#         self.app.log.debug("on_import_tools_db_file()")
+#
+#         filter__ = "Text File (*.TXT);;All Files (*.*)"
+#         filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Tools DB"), filter=filter__)
+#
+#         if filename == "":
+#             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+#         else:
+#             try:
+#                 with open(filename) as f:
+#                     tools_in_db = f.read()
+#             except IOError:
+#                 self.app.log.error("Could not load Tools DB file.")
+#                 self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not load Tools DB file."))
+#                 return
+#
+#             try:
+#                 self.db_tool_dict = json.loads(tools_in_db)
+#             except Exception:
+#                 e = sys.exc_info()[0]
+#                 self.app.log.error(str(e))
+#                 self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
+#                 return
+#
+#             self.app.inform.emit('[success] %s: %s' % (_("Loaded Tools DB from"), filename))
+#             self.build_db_ui()
+#             self.callback_on_edited()
+#
+#     def on_save_tools_db(self, silent=False):
+#         self.app.log.debug("ToolsDB.on_save_button() --> Saving Tools Database to file.")
+#
+#         filename = self.app.tools_database_path()
+#
+#         # Preferences save, update the color of the Tools DB Tab text
+#         for idx in range(self.app_ui.plot_tab_area.count()):
+#             if self.app_ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+#                 self.app_ui.plot_tab_area.tabBar.setTabTextColor(idx, QtGui.QColor('black'))
+#
+#                 # Save Tools DB in a file
+#                 try:
+#                     f = open(filename, "w")
+#                     json.dump(self.db_tool_dict, f, default=to_dict, indent=2)
+#                     f.close()
+#                 except Exception as e:
+#                     self.app.log.debug("ToolsDB.on_save_tools_db() --> %s" % str(e))
+#                     self.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed to write Tools DB to file."))
+#                     return
+#
+#                 if not silent:
+#                     self.app.inform.emit('[success] %s' % _("Saved Tools DB."))
+#
+#     def ui_connect(self):
+#         try:
+#             try:
+#                 self.table_widget.itemChanged.disconnect(self.callback_on_edited)
+#             except (TypeError, AttributeError):
+#                 pass
+#             self.table_widget.itemChanged.connect(self.callback_on_edited)
+#         except AttributeError:
+#             pass
+#
+#         for row in range(self.table_widget.rowCount()):
+#             for col in range(self.table_widget.columnCount()):
+#                 # ComboBox
+#                 try:
+#                     try:
+#                         self.table_widget.cellWidget(row, col).currentIndexChanged.disconnect(self.callback_on_edited)
+#                     except (TypeError, AttributeError):
+#                         pass
+#                     self.table_widget.cellWidget(row, col).currentIndexChanged.connect(self.callback_on_edited)
+#                 except AttributeError:
+#                     pass
+#
+#                 # CheckBox
+#                 try:
+#                     try:
+#                         self.table_widget.cellWidget(row, col).toggled.disconnect(self.callback_on_edited)
+#                     except (TypeError, AttributeError):
+#                         pass
+#                     self.table_widget.cellWidget(row, col).toggled.connect(self.callback_on_edited)
+#                 except AttributeError:
+#                     pass
+#
+#                 # SpinBox, DoubleSpinBox
+#                 try:
+#                     try:
+#                         self.table_widget.cellWidget(row, col).valueChanged.disconnect(self.callback_on_edited)
+#                     except (TypeError, AttributeError):
+#                         pass
+#                     self.table_widget.cellWidget(row, col).valueChanged.connect(self.callback_on_edited)
+#                 except AttributeError:
+#                     pass
+#
+#     def ui_disconnect(self):
+#         try:
+#             self.table_widget.itemChanged.disconnect(self.callback_on_edited)
+#         except (TypeError, AttributeError):
+#             pass
+#
+#         for row in range(self.table_widget.rowCount()):
+#             for col in range(self.table_widget.columnCount()):
+#                 # ComboBox
+#                 try:
+#                     self.table_widget.cellWidget(row, col).currentIndexChanged.disconnect(self.callback_on_edited)
+#                 except (TypeError, AttributeError):
+#                     pass
+#
+#                 # CheckBox
+#                 try:
+#                     self.table_widget.cellWidget(row, col).toggled.disconnect(self.callback_on_edited)
+#                 except (TypeError, AttributeError):
+#                     pass
+#
+#                 # SpinBox, DoubleSpinBox
+#                 try:
+#                     self.table_widget.cellWidget(row, col).valueChanged.disconnect(self.callback_on_edited)
+#                 except (TypeError, AttributeError):
+#                     pass
+#
+#     def callback_on_edited(self):
+#
+#         # update the dictionary storage self.db_tool_dict
+#         self.db_tool_dict.clear()
+#         dict_elem = {}
+#         default_data = {}
+#
+#         for row in range(self.table_widget.rowCount()):
+#             new_toolid = row + 1
+#             for col in range(self.table_widget.columnCount()):
+#                 column_header_text = self.table_widget.horizontalHeaderItem(col).text()
+#                 if column_header_text == _('Tool Name'):
+#                     dict_elem['name'] = self.table_widget.item(row, col).text()
+#                 elif column_header_text == _('Tool Dia'):
+#                     dict_elem['tooldia'] = self.table_widget.cellWidget(row, col).get_value()
+#                 elif column_header_text == _('Tool Offset'):
+#                     dict_elem['offset'] = self.table_widget.cellWidget(row, col).get_value()
+#                 elif column_header_text == _('Custom Offset'):
+#                     dict_elem['offset_value'] = self.table_widget.cellWidget(row, col).get_value()
+#                 elif column_header_text == _('Tool Type'):
+#                     dict_elem['type'] = self.table_widget.cellWidget(row, col).get_value()
+#                 elif column_header_text == _('Tool Shape'):
+#                     dict_elem['tool_type'] = self.table_widget.cellWidget(row, col).get_value()
+#                 else:
+#                     if column_header_text == _('Cut Z'):
+#                         default_data['cutz'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('MultiDepth'):
+#                         default_data['multidepth'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('DPP'):
+#                         default_data['depthperpass'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('V-Dia'):
+#                         default_data['vtipdia'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('V-Angle'):
+#                         default_data['vtipangle'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('Travel Z'):
+#                         default_data['travelz'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('FR'):
+#                         default_data['feedrate'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('FR Z'):
+#                         default_data['feedrate_z'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('FR Rapids'):
+#                         default_data['feedrate_rapid'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('Spindle Speed'):
+#                         default_data['spindlespeed'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('Dwell'):
+#                         default_data['dwell'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('Dwelltime'):
+#                         default_data['dwelltime'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('Preprocessor'):
+#                         default_data['ppname_g'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('ExtraCut'):
+#                         default_data['extracut'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _("E-Cut Length"):
+#                         default_data['extracut_length'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('Toolchange'):
+#                         default_data['toolchange'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('Toolchange XY'):
+#                         default_data['toolchangexy'] = self.table_widget.item(row, col).text()
+#                     elif column_header_text == _('Toolchange Z'):
+#                         default_data['toolchangez'] = self.table_widget.cellWidget(row, col).get_value()
+#                     elif column_header_text == _('Start Z'):
+#                         default_data['startz'] = float(self.table_widget.item(row, col).text()) \
+#                             if self.table_widget.item(row, col).text() != '' else None
+#                     elif column_header_text == _('End Z'):
+#                         default_data['endz'] = self.table_widget.cellWidget(row, col).get_value()
+#
+#             dict_elem['data'] = default_data
+#             self.db_tool_dict.update(
+#                 {
+#                     new_toolid: deepcopy(dict_elem)
+#                 }
+#             )
+#
+#         self.callback_app()
+#
+#     def on_tool_requested_from_app(self):
+#         if not self.table_widget.selectionModel().selectedRows():
+#             self.app.inform.emit('[WARNING_NOTCL] %s...' % _("No Tool/row selected in the Tools Database table"))
+#             return
+#
+#         model_index_list = self.table_widget.selectionModel().selectedRows()
+#         for model_index in model_index_list:
+#             selected_row = model_index.row()
+#             tool_uid = selected_row + 1
+#             for key in self.db_tool_dict.keys():
+#                 if str(key) == str(tool_uid):
+#                     selected_tool = self.db_tool_dict[key]
+#                     self.on_tool_request(tool=selected_tool)
+#
+#     def on_cancel_tool(self):
+#         for idx in range(self.app_ui.plot_tab_area.count()):
+#             if self.app_ui.plot_tab_area.tabText(idx) == _("Tools Database"):
+#                 wdg = self.app_ui.plot_tab_area.widget(idx)
+#                 wdg.deleteLater()
+#                 self.app_ui.plot_tab_area.removeTab(idx)
+#         self.app.inform.emit('%s' % _("Cancelled adding tool from DB."))
+#
+#     # def resize_new_tool_table_widget(self, min_size, max_size):
+#     #     """
+#     #     Resize the table widget responsible for adding new tool in the Tool Database
+#     #
+#     #     :param min_size: passed by rangeChanged signal or the self.new_tool_table_widget.horizontalScrollBar()
+#     #     :param max_size: passed by rangeChanged signal or the self.new_tool_table_widget.horizontalScrollBar()
+#     #     :return:
+#     #     """
+#     #     t_height = self.t_height
+#     #     if max_size > min_size:
+#     #         t_height = self.t_height + self.new_tool_table_widget.verticalScrollBar().height()
+#     #
+#     #     self.new_tool_table_widget.setMaximumHeight(t_height)
+#
+#     def closeEvent(self, QCloseEvent):
+#         super().closeEvent(QCloseEvent)

+ 4526 - 0
appEditors/AppExcEditor.py

@@ -0,0 +1,4526 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 8/17/2019                                          #
+# MIT Licence                                              #
+# ##########################################################
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import Qt
+
+from camlib import distance, arc, FlatCAMRTreeStorage
+from appGUI.GUIElements import FCEntry, FCComboBox2, FCTable, FCDoubleSpinner, RadioSet, FCSpinner, FCButton, FCLabel
+from appEditors.AppGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, AppGeoEditor
+
+from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon, Point
+import shapely.affinity as affinity
+
+import numpy as np
+
+from rtree import index as rtindex
+
+import traceback
+import math
+import logging
+from copy import deepcopy
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class SelectEditorExc(FCShapeTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'drill_select'
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+
+        self.draw_app = draw_app
+        self.storage = self.draw_app.storage_dict
+        # self.selected = self.draw_app.selected
+
+        # here we store the selected tools
+        self.sel_tools = set()
+
+        # here we store all shapes that were selected so we can search for the nearest to our click location
+        self.sel_storage = AppExcEditor.make_storage()
+
+        self.draw_app.ui.resize_frame.hide()
+        self.draw_app.ui.array_frame.hide()
+        self.draw_app.ui.slot_frame.hide()
+        self.draw_app.ui.slot_array_frame.hide()
+
+    def click(self, point):
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if key_modifier == QtCore.Qt.ShiftModifier:
+            mod_key = 'Shift'
+        elif key_modifier == QtCore.Qt.ControlModifier:
+            mod_key = 'Control'
+        else:
+            mod_key = None
+
+        if mod_key == self.draw_app.app.defaults["global_mselect_key"]:
+            pass
+        else:
+            self.draw_app.selected = []
+
+    def click_release(self, pos):
+        self.draw_app.ui.tools_table_exc.clearSelection()
+        xmin, ymin, xmax, ymax = 0, 0, 0, 0
+
+        try:
+            for storage in self.draw_app.storage_dict:
+                # for sh in self.draw_app.storage_dict[storage].get_objects():
+                #     self.sel_storage.insert(sh)
+                _, st_closest_shape = self.draw_app.storage_dict[storage].nearest(pos)
+                self.sel_storage.insert(st_closest_shape)
+
+            _, closest_shape = self.sel_storage.nearest(pos)
+
+            # constrain selection to happen only within a certain bounding box; it works only for MultiLineStrings
+            if isinstance(closest_shape.geo, MultiLineString):
+                x_coord, y_coord = closest_shape.geo[0].xy
+                delta = (x_coord[1] - x_coord[0])
+                # closest_shape_coords = (((x_coord[0] + delta / 2)), y_coord[0])
+                xmin = x_coord[0] - (0.7 * delta)
+                xmax = x_coord[0] + (1.7 * delta)
+                ymin = y_coord[0] - (0.7 * delta)
+                ymax = y_coord[0] + (1.7 * delta)
+            elif isinstance(closest_shape.geo, Polygon):
+                xmin, ymin, xmax, ymax = closest_shape.geo.bounds
+                dx = xmax - xmin
+                dy = ymax - ymin
+                delta = dx if dx > dy else dy
+                xmin -= 0.7 * delta
+                xmax += 0.7 * delta
+                ymin -= 0.7 * delta
+                ymax += 0.7 * delta
+        except StopIteration:
+            return ""
+
+        if pos[0] < xmin or pos[0] > xmax or pos[1] < ymin or pos[1] > ymax:
+            self.draw_app.selected = []
+        else:
+            modifiers = QtWidgets.QApplication.keyboardModifiers()
+
+            if modifiers == QtCore.Qt.ShiftModifier:
+                mod_key = 'Shift'
+            elif modifiers == QtCore.Qt.ControlModifier:
+                mod_key = 'Control'
+            else:
+                mod_key = None
+
+            if mod_key == self.draw_app.app.defaults["global_mselect_key"]:
+                if closest_shape in self.draw_app.selected:
+                    self.draw_app.selected.remove(closest_shape)
+                else:
+                    self.draw_app.selected.append(closest_shape)
+            else:
+                self.draw_app.selected = []
+                self.draw_app.selected.append(closest_shape)
+
+            # select the diameter of the selected shape in the tool table
+            try:
+                self.draw_app.ui.tools_table_exc.cellPressed.disconnect()
+            except (TypeError, AttributeError):
+                pass
+
+            # if mod_key == self.draw_app.app.defaults["global_mselect_key"]:
+            #     self.draw_app.ui.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
+            self.sel_tools.clear()
+
+            for shape_s in self.draw_app.selected:
+                for storage in self.draw_app.storage_dict:
+                    if shape_s in self.draw_app.storage_dict[storage].get_objects():
+                        self.sel_tools.add(storage)
+
+            self.draw_app.ui.tools_table_exc.clearSelection()
+            for storage in self.sel_tools:
+                for k, v in self.draw_app.tool2tooldia.items():
+                    if v == storage:
+                        self.draw_app.ui.tools_table_exc.selectRow(int(k) - 1)
+                        self.draw_app.last_tool_selected = int(k)
+                        break
+
+            # self.draw_app.ui.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+
+            self.draw_app.ui.tools_table_exc.cellPressed.connect(self.draw_app.on_row_selected)
+
+        # delete whatever is in selection storage, there is no longer need for those shapes
+        self.sel_storage = AppExcEditor.make_storage()
+
+        return ""
+
+        # pos[0] and pos[1] are the mouse click coordinates (x, y)
+        # for storage in self.draw_app.storage_dict:
+        #     for obj_shape in self.draw_app.storage_dict[storage].get_objects():
+        #         minx, miny, maxx, maxy = obj_shape.geo.bounds
+        #         if (minx <= pos[0] <= maxx) and (miny <= pos[1] <= maxy):
+        #             over_shape_list.append(obj_shape)
+        #
+        # try:
+        #     # if there is no shape under our click then deselect all shapes
+        #     if not over_shape_list:
+        #         self.draw_app.selected = []
+        #         AppExcEditor.draw_shape_idx = -1
+        #         self.draw_app.ui.tools_table_exc.clearSelection()
+        #     else:
+        #         # if there are shapes under our click then advance through the list of them, one at the time in a
+        #         # circular way
+        #         AppExcEditor.draw_shape_idx = (AppExcEditor.draw_shape_idx + 1) % len(over_shape_list)
+        #         obj_to_add = over_shape_list[int(AppExcEditor.draw_shape_idx)]
+        #
+        #         if self.draw_app.app.defaults["global_mselect_key"] == 'Shift':
+        #             if self.draw_app.modifiers == Qt.ShiftModifier:
+        #                 if obj_to_add in self.draw_app.selected:
+        #                     self.draw_app.selected.remove(obj_to_add)
+        #                 else:
+        #                     self.draw_app.selected.append(obj_to_add)
+        #             else:
+        #                 self.draw_app.selected = []
+        #                 self.draw_app.selected.append(obj_to_add)
+        #         else:
+        #             # if CONTROL key is pressed then we add to the selected list the current shape but if it's already
+        #             # in the selected list, we removed it. Therefore first click selects, second deselects.
+        #             if self.draw_app.modifiers == Qt.ControlModifier:
+        #                 if obj_to_add in self.draw_app.selected:
+        #                     self.draw_app.selected.remove(obj_to_add)
+        #                 else:
+        #                     self.draw_app.selected.append(obj_to_add)
+        #             else:
+        #                 self.draw_app.selected = []
+        #                 self.draw_app.selected.append(obj_to_add)
+        #
+        #     for storage in self.draw_app.storage_dict:
+        #         for shape in self.draw_app.selected:
+        #             if shape in self.draw_app.storage_dict[storage].get_objects():
+        #                 for key in self.draw_app.tool2tooldia:
+        #                     if self.draw_app.tool2tooldia[key] == storage:
+        #                         item = self.draw_app.ui.tools_table_exc.item((key - 1), 1)
+        #                         item.setSelected(True)
+        #                         # self.draw_app.ui.tools_table_exc.selectItem(key - 1)
+        #
+        # except Exception as e:
+        #     log.error("[ERROR] Something went bad. %s" % str(e))
+        #     raise
+
+    def clean_up(self):
+        pass
+
+
+class DrillAdd(FCShapeTool):
+    """
+    Resulting type: MultiLineString
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'drill_add'
+        self.draw_app = draw_app
+
+        self.selected_dia = None
+        try:
+            self.draw_app.app.inform.emit(_("Click to place ..."))
+            self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
+
+            # as a visual marker, select again in tooltable the actual tool that we are using
+            # remember that it was deselected when clicking on canvas
+            item = self.draw_app.ui.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.ui.tools_table_exc.setCurrentItem(item)
+        except KeyError:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' % _("To add a drill first select a tool"))
+            self.draw_app.select_tool("drill_select")
+            return
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_drill.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo=geo)
+
+        self.draw_app.app.inform.emit(_("Click to place ..."))
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+    def click(self, point):
+        self.make()
+        return "Done."
+
+    def utility_geometry(self, data=None):
+        self.points = data
+        return DrawToolUtilityShape(self.util_shape(data))
+
+    def util_shape(self, point):
+        if point[0] is None and point[1] is None:
+            point_x = self.draw_app.x
+            point_y = self.draw_app.y
+        else:
+            point_x = point[0]
+            point_y = point[1]
+
+        start_hor_line = ((point_x - (self.selected_dia / 2)), point_y)
+        stop_hor_line = ((point_x + (self.selected_dia / 2)), point_y)
+        start_vert_line = (point_x, (point_y - (self.selected_dia / 2)))
+        stop_vert_line = (point_x, (point_y + (self.selected_dia / 2)))
+
+        return MultiLineString([(start_hor_line, stop_hor_line), (start_vert_line, stop_vert_line)])
+
+    def make(self):
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+
+        # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
+        # to the value, as a list of itself
+        if self.selected_dia in self.draw_app.points_edit:
+            self.draw_app.points_edit[self.selected_dia].append(self.points)
+        else:
+            self.draw_app.points_edit[self.selected_dia] = [self.points]
+
+        self.draw_app.current_storage = self.draw_app.storage_dict[self.selected_dia]
+        self.geometry = DrawToolShape(self.util_shape(self.points))
+        self.draw_app.in_action = False
+        self.complete = True
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        self.draw_app.app.jump_signal.disconnect()
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.tools_table_exc.clearSelection()
+        self.draw_app.plot_all()
+
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class DrillArray(FCShapeTool):
+    """
+    Resulting type: MultiLineString
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'drill_array'
+
+        self.draw_app.ui.array_frame.show()
+
+        self.selected_dia = None
+        self.drill_axis = 'X'
+        self.drill_array = 'linear'    # 'linear'
+        self.drill_array_size = None
+        self.drill_pitch = None
+        self.drill_linear_angle = None
+
+        self.drill_angle = None
+        self.drill_direction = None
+        self.drill_radius = None
+
+        self.origin = None
+        self.destination = None
+        self.flag_for_circ_array = None
+
+        self.last_dx = 0
+        self.last_dy = 0
+
+        self.pt = []
+
+        try:
+            self.draw_app.app.inform.emit(_("Click to place ..."))
+            self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
+            # as a visual marker, select again in tooltable the actual tool that we are using
+            # remember that it was deselected when clicking on canvas
+            item = self.draw_app.ui.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.ui.tools_table_exc.setCurrentItem(item)
+        except KeyError:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                          _("To add an Drill Array first select a tool in Tool Table"))
+            return
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_drill_array.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True)
+
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo=geo)
+
+        self.draw_app.app.inform.emit(_("Click on target location ..."))
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+    def click(self, point):
+
+        if self.drill_array == 'linear':   # 'Linear'
+            self.make()
+            return
+        else:
+            if self.flag_for_circ_array is None:
+                self.draw_app.in_action = True
+                self.pt.append(point)
+
+                self.flag_for_circ_array = True
+                self.set_origin(point)
+                self.draw_app.app.inform.emit(_("Click on the Drill Circular Array Start position"))
+            else:
+                self.destination = point
+                self.make()
+                self.flag_for_circ_array = None
+                return
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def utility_geometry(self, data=None, static=None):
+        self.drill_axis = self.draw_app.ui.drill_axis_radio.get_value()
+        self.drill_direction = self.draw_app.ui.drill_array_dir_radio.get_value()
+        self.drill_array = self.draw_app.ui.array_type_radio.get_value()
+        try:
+            self.drill_array_size = int(self.draw_app.ui.drill_array_size_entry.get_value())
+            try:
+                self.drill_pitch = float(self.draw_app.ui.drill_pitch_entry.get_value())
+                self.drill_linear_angle = float(self.draw_app.ui.linear_angle_spinner.get_value())
+                self.drill_angle = float(self.draw_app.ui.drill_angle_entry.get_value())
+            except TypeError:
+                self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                              _("The value is not Float. Check for comma instead of dot separator."))
+                return
+        except Exception as e:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s. %s' %
+                                          (_("The value is mistyped. Check the value"), str(e)))
+            return
+
+        if self.drill_array == 'linear':   # 'Linear'
+            if data[0] is None and data[1] is None:
+                dx = self.draw_app.x
+                dy = self.draw_app.y
+            else:
+                dx = data[0]
+                dy = data[1]
+
+            geo_list = []
+            geo = None
+            self.points = [dx, dy]
+
+            for item in range(self.drill_array_size):
+                if self.drill_axis == 'X':
+                    geo = self.util_shape(((dx + (self.drill_pitch * item)), dy))
+                if self.drill_axis == 'Y':
+                    geo = self.util_shape((dx, (dy + (self.drill_pitch * item))))
+                if self.drill_axis == 'A':
+                    x_adj = self.drill_pitch * math.cos(math.radians(self.drill_linear_angle))
+                    y_adj = self.drill_pitch * math.sin(math.radians(self.drill_linear_angle))
+                    geo = self.util_shape(
+                        ((dx + (x_adj * item)), (dy + (y_adj * item)))
+                    )
+
+                if static is None or static is False:
+                    geo_list.append(affinity.translate(geo, xoff=(dx - self.last_dx), yoff=(dy - self.last_dy)))
+                else:
+                    geo_list.append(geo)
+            # self.origin = data
+
+            self.last_dx = dx
+            self.last_dy = dy
+            return DrawToolUtilityShape(geo_list)
+        elif self.drill_array == 'circular':  # 'Circular'
+            if data[0] is None and data[1] is None:
+                cdx = self.draw_app.x
+                cdy = self.draw_app.y
+            else:
+                cdx = data[0]
+                cdy = data[1]
+
+            utility_list = []
+
+            try:
+                radius = distance((cdx, cdy), self.origin)
+            except Exception:
+                radius = 0
+
+            if radius == 0:
+                self.draw_app.delete_utility_geometry()
+
+            if len(self.pt) >= 1 and radius > 0:
+                try:
+                    if cdx < self.origin[0]:
+                        radius = -radius
+
+                    # draw the temp geometry
+                    initial_angle = math.asin((cdy - self.origin[1]) / radius)
+
+                    temp_circular_geo = self.circular_util_shape(radius, initial_angle)
+
+                    # draw the line
+                    temp_points = [x for x in self.pt]
+                    temp_points.append([cdx, cdy])
+                    temp_line = LineString(temp_points)
+
+                    for geo_shape in temp_circular_geo:
+                        utility_list.append(geo_shape.geo)
+                    utility_list.append(temp_line)
+
+                    return DrawToolUtilityShape(utility_list)
+                except Exception as e:
+                    log.debug("DrillArray.utility_geometry -- circular -> %s" % str(e))
+
+    def circular_util_shape(self, radius, angle):
+        self.drill_direction = self.draw_app.ui.drill_array_dir_radio.get_value()
+        self.drill_angle = self.draw_app.ui.drill_angle_entry.get_value()
+
+        circular_geo = []
+        if self.drill_direction == 'CW':
+            for i in range(self.drill_array_size):
+                angle_radians = math.radians(self.drill_angle * i)
+                x = self.origin[0] + radius * math.cos(-angle_radians + angle)
+                y = self.origin[1] + radius * math.sin(-angle_radians + angle)
+
+                geo_sol = self.util_shape((x, y))
+                # geo_sol = affinity.rotate(geo_sol, angle=(math.pi - angle_radians), use_radians=True)
+
+                circular_geo.append(DrawToolShape(geo_sol))
+        else:
+            for i in range(self.drill_array_size):
+                angle_radians = math.radians(self.drill_angle * i)
+                x = self.origin[0] + radius * math.cos(angle_radians + angle)
+                y = self.origin[1] + radius * math.sin(angle_radians + angle)
+
+                geo_sol = self.util_shape((x, y))
+                # geo_sol = affinity.rotate(geo_sol, angle=(angle_radians - math.pi), use_radians=True)
+
+                circular_geo.append(DrawToolShape(geo_sol))
+
+        return circular_geo
+
+    def util_shape(self, point):
+        if point[0] is None and point[1] is None:
+            point_x = self.draw_app.x
+            point_y = self.draw_app.y
+        else:
+            point_x = point[0]
+            point_y = point[1]
+
+        start_hor_line = ((point_x - (self.selected_dia / 2)), point_y)
+        stop_hor_line = ((point_x + (self.selected_dia / 2)), point_y)
+        start_vert_line = (point_x, (point_y - (self.selected_dia / 2)))
+        stop_vert_line = (point_x, (point_y + (self.selected_dia / 2)))
+
+        return MultiLineString([(start_hor_line, stop_hor_line), (start_vert_line, stop_vert_line)])
+
+    def make(self):
+        self.geometry = []
+        geo = None
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+
+        # add the point to drills if the diameter is a key in the dict, if not, create it add the drill location
+        # to the value, as a list of itself
+        if self.selected_dia not in self.draw_app.points_edit:
+            self.draw_app.points_edit[self.selected_dia] = []
+        for i in range(self.drill_array_size):
+            self.draw_app.points_edit[self.selected_dia].append(self.points)
+
+        self.draw_app.current_storage = self.draw_app.storage_dict[self.selected_dia]
+
+        if self.drill_array == 'linear':   # 'Linear'
+            for item in range(self.drill_array_size):
+                if self.drill_axis == 'X':
+                    geo = self.util_shape(((self.points[0] + (self.drill_pitch * item)), self.points[1]))
+                if self.drill_axis == 'Y':
+                    geo = self.util_shape((self.points[0], (self.points[1] + (self.drill_pitch * item))))
+                if self.drill_axis == 'A':
+                    x_adj = self.drill_pitch * math.cos(math.radians(self.drill_linear_angle))
+                    y_adj = self.drill_pitch * math.sin(math.radians(self.drill_linear_angle))
+                    geo = self.util_shape(
+                        ((self.points[0] + (x_adj * item)), (self.points[1] + (y_adj * item)))
+                    )
+
+                self.geometry.append(DrawToolShape(geo))
+        else:   # 'Circular'
+            if (self.drill_angle * self.drill_array_size) > 360:
+                self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                              _("Too many items for the selected spacing angle."))
+                self.draw_app.app.jump_signal.disconnect()
+                return
+
+            radius = distance(self.destination, self.origin)
+            if radius == 0:
+                self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
+                self.draw_app.delete_utility_geometry()
+                self.draw_app.select_tool('drill_select')
+                return
+
+            if self.destination[0] < self.origin[0]:
+                radius = -radius
+            initial_angle = math.asin((self.destination[1] - self.origin[1]) / radius)
+
+            circular_geo = self.circular_util_shape(radius, initial_angle)
+            self.geometry += circular_geo
+
+        self.complete = True
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        self.draw_app.in_action = False
+        self.draw_app.ui.array_frame.hide()
+
+        self.draw_app.app.jump_signal.disconnect()
+
+    def on_key(self, key):
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if key_modifier == QtCore.Qt.ShiftModifier:
+            mod_key = 'Shift'
+        elif key_modifier == QtCore.Qt.ControlModifier:
+            mod_key = 'Control'
+        else:
+            mod_key = None
+
+        if mod_key == 'Control':
+            pass
+        elif mod_key is None:
+            # Toggle Drill Array Direction
+            if key == QtCore.Qt.Key_Space:
+                if self.draw_app.ui.drill_axis_radio.get_value() == 'X':
+                    self.draw_app.ui.drill_axis_radio.set_value('Y')
+                elif self.draw_app.ui.drill_axis_radio.get_value() == 'Y':
+                    self.draw_app.ui.drill_axis_radio.set_value('A')
+                elif self.draw_app.ui.drill_axis_radio.get_value() == 'A':
+                    self.draw_app.ui.drill_axis_radio.set_value('X')
+
+                # ## Utility geometry (animated)
+                self.draw_app.update_utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.tools_table_exc.clearSelection()
+        self.draw_app.plot_all()
+
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class SlotAdd(FCShapeTool):
+    """
+    Resulting type: Polygon
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'slot_add'
+        self.draw_app = draw_app
+
+        self.draw_app.ui.slot_frame.show()
+
+        self.selected_dia = None
+        try:
+            self.draw_app.app.inform.emit(_("Click to place ..."))
+            self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
+
+            # as a visual marker, select again in tooltable the actual tool that we are using
+            # remember that it was deselected when clicking on canvas
+            item = self.draw_app.ui.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.ui.tools_table_exc.setCurrentItem(item)
+        except KeyError:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' % _("To add a slot first select a tool"))
+            self.draw_app.select_tool("drill_select")
+            return
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_slot.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+        self.half_height = 0.0
+        self.half_width = 0.0
+        self.radius = float(self.selected_dia / 2.0)
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo=geo)
+
+        self.draw_app.app.inform.emit(_("Click on target location ..."))
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+    def click(self, point):
+        self.make()
+        return "Done."
+
+    def utility_geometry(self, data=None):
+
+        self.points = data
+        geo_data = self.util_shape(data)
+        if geo_data:
+            return DrawToolUtilityShape(geo_data)
+        else:
+            return None
+
+    def util_shape(self, point):
+
+        if point is None:
+            return
+
+        # updating values here allows us to change the aperture on the fly, after the Tool has been started
+        self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
+        self.radius = float(self.selected_dia / 2.0)
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+        try:
+            slot_length = float(self.draw_app.ui.slot_length_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                slot_length = float(self.draw_app.ui.slot_length_entry.get_value().replace(',', '.'))
+                self.draw_app.ui.slot_length_entry.set_value(slot_length)
+            except ValueError:
+                self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                              _("Value is missing or wrong format. Add it and retry."))
+                return
+
+        try:
+            slot_angle = float(self.draw_app.ui.slot_angle_spinner.get_value())
+        except ValueError:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                          _("Value is missing or wrong format. Add it and retry."))
+            return
+
+        if self.draw_app.ui.slot_axis_radio.get_value() == 'X':
+            self.half_width = slot_length / 2.0
+            self.half_height = self.radius
+        else:
+            self.half_width = self.radius
+            self.half_height = slot_length / 2.0
+
+        if point[0] is None and point[1] is None:
+            point_x = self.draw_app.x
+            point_y = self.draw_app.y
+        else:
+            point_x = point[0]
+            point_y = point[1]
+
+        geo = []
+
+        if self.half_height > self.half_width:
+            p1 = (point_x - self.half_width, point_y - self.half_height + self.half_width)
+            p2 = (point_x + self.half_width, point_y - self.half_height + self.half_width)
+            p3 = (point_x + self.half_width, point_y + self.half_height - self.half_width)
+            p4 = (point_x - self.half_width, point_y + self.half_height - self.half_width)
+
+            down_center = [point_x, point_y - self.half_height + self.half_width]
+            d_start_angle = math.pi
+            d_stop_angle = 0.0
+            down_arc = arc(down_center, self.half_width, d_start_angle, d_stop_angle, 'ccw', self.steps_per_circ)
+
+            up_center = [point_x, point_y + self.half_height - self.half_width]
+            u_start_angle = 0.0
+            u_stop_angle = math.pi
+            up_arc = arc(up_center, self.half_width, u_start_angle, u_stop_angle, 'ccw', self.steps_per_circ)
+
+            geo.append(p1)
+            for pt in down_arc:
+                geo.append(pt)
+            geo.append(p2)
+            geo.append(p3)
+            for pt in up_arc:
+                geo.append(pt)
+            geo.append(p4)
+
+            if self.draw_app.ui.slot_axis_radio.get_value() == 'A':
+                return affinity.rotate(geom=Polygon(geo), angle=-slot_angle)
+            else:
+                return Polygon(geo)
+        else:
+            p1 = (point_x - self.half_width + self.half_height, point_y - self.half_height)
+            p2 = (point_x + self.half_width - self.half_height, point_y - self.half_height)
+            p3 = (point_x + self.half_width - self.half_height, point_y + self.half_height)
+            p4 = (point_x - self.half_width + self.half_height, point_y + self.half_height)
+
+            left_center = [point_x - self.half_width + self.half_height, point_y]
+            d_start_angle = math.pi / 2
+            d_stop_angle = 1.5 * math.pi
+            left_arc = arc(left_center, self.half_height, d_start_angle, d_stop_angle, 'ccw', self.steps_per_circ)
+
+            right_center = [point_x + self.half_width - self.half_height, point_y]
+            u_start_angle = 1.5 * math.pi
+            u_stop_angle = math.pi / 2
+            right_arc = arc(right_center, self.half_height, u_start_angle, u_stop_angle, 'ccw', self.steps_per_circ)
+
+            geo.append(p1)
+            geo.append(p2)
+            for pt in right_arc:
+                geo.append(pt)
+            geo.append(p3)
+            geo.append(p4)
+            for pt in left_arc:
+                geo.append(pt)
+
+            return Polygon(geo)
+
+    def make(self):
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+
+        try:
+            self.geometry = DrawToolShape(self.util_shape(self.points))
+        except Exception as e:
+            log.debug("SlotAdd.make() --> %s" % str(e))
+
+        # add the point to drills/slots if the diameter is a key in the dict, if not, create it add the drill location
+        # to the value, as a list of itself
+        if self.selected_dia in self.draw_app.slot_points_edit:
+            self.draw_app.slot_points_edit[self.selected_dia].append(self.points)
+        else:
+            self.draw_app.slot_points_edit[self.selected_dia] = [self.points]
+
+        self.draw_app.current_storage = self.draw_app.storage_dict[self.selected_dia]
+
+        self.draw_app.in_action = False
+        self.complete = True
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        self.draw_app.ui.slot_frame.hide()
+        self.draw_app.app.jump_signal.disconnect()
+
+    def on_key(self, key):
+        # Toggle Pad Direction
+        if key == QtCore.Qt.Key_Space:
+            if self.draw_app.ui.slot_axis_radio.get_value() == 'X':
+                self.draw_app.ui.slot_axis_radio.set_value('Y')
+            elif self.draw_app.ui.slot_axis_radio.get_value() == 'Y':
+                self.draw_app.ui.slot_axis_radio.set_value('A')
+            elif self.draw_app.ui.slot_axis_radio.get_value() == 'A':
+                self.draw_app.ui.slot_axis_radio.set_value('X')
+            # ## Utility geometry (animated)
+            self.draw_app.update_utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.tools_table_exc.clearSelection()
+        self.draw_app.plot_all()
+
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class SlotArray(FCShapeTool):
+    """
+    Resulting type: MultiPolygon
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'slot_array'
+        self.draw_app = draw_app
+
+        self.draw_app.ui.slot_frame.show()
+        self.draw_app.ui.slot_array_frame.show()
+
+        self.selected_dia = None
+        try:
+            self.draw_app.app.inform.emit(_("Click to place ..."))
+            self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
+            # as a visual marker, select again in tooltable the actual tool that we are using
+            # remember that it was deselected when clicking on canvas
+            item = self.draw_app.ui.tools_table_exc.item((self.draw_app.last_tool_selected - 1), 1)
+            self.draw_app.ui.tools_table_exc.setCurrentItem(item)
+        except KeyError:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                          _("To add an Slot Array first select a tool in Tool Table"))
+            return
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_array.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+        self.half_width = 0.0
+        self.half_height = 0.0
+        self.radius = float(self.selected_dia / 2.0)
+
+        self.slot_axis = 'X'
+        self.slot_array = 'linear'     # 'linear'
+        self.slot_array_size = None
+        self.slot_pitch = None
+        self.slot_linear_angle = None
+
+        self.slot_angle = None
+        self.slot_direction = None
+        self.slot_radius = None
+
+        self.origin = None
+        self.destination = None
+        self.flag_for_circ_array = None
+
+        self.last_dx = 0
+        self.last_dy = 0
+
+        self.pt = []
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True)
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo=geo)
+
+        self.draw_app.app.inform.emit(_("Click on target location ..."))
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+    def click(self, point):
+
+        if self.slot_array == 'linear':    # 'Linear'
+            self.make()
+            return
+        else:   # 'Circular'
+            if self.flag_for_circ_array is None:
+                self.draw_app.in_action = True
+                self.pt.append(point)
+
+                self.flag_for_circ_array = True
+                self.set_origin(point)
+                self.draw_app.app.inform.emit(_("Click on the Slot Circular Array Start position"))
+            else:
+                self.destination = point
+                self.make()
+                self.flag_for_circ_array = None
+                return
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def utility_geometry(self, data=None, static=None):
+        self.slot_axis = self.draw_app.ui.slot_array_axis_radio.get_value()
+        self.slot_direction = self.draw_app.ui.slot_array_direction_radio.get_value()
+        self.slot_array = self.draw_app.ui.slot_array_type_radio.get_value()
+        try:
+            self.slot_array_size = int(self.draw_app.ui.slot_array_size_entry.get_value())
+            try:
+                self.slot_pitch = float(self.draw_app.ui.slot_array_pitch_entry.get_value())
+                self.slot_linear_angle = float(self.draw_app.ui.slot_array_linear_angle_spinner.get_value())
+                self.slot_angle = float(self.draw_app.ui.slot_array_angle_entry.get_value())
+            except TypeError:
+                self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                              _("The value is not Float. Check for comma instead of dot separator."))
+                return
+        except Exception:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' % _("The value is mistyped. Check the value."))
+            return
+
+        if self.slot_array == 'linear':    # 'Linear'
+            if data[0] is None and data[1] is None:
+                dx = self.draw_app.x
+                dy = self.draw_app.y
+            else:
+                dx = data[0]
+                dy = data[1]
+
+            geo_el_list = []
+            geo_el = []
+            self.points = [dx, dy]
+
+            for item in range(self.slot_array_size):
+                if self.slot_axis == 'X':
+                    geo_el = self.util_shape(((dx + (self.slot_pitch * item)), dy))
+                if self.slot_axis == 'Y':
+                    geo_el = self.util_shape((dx, (dy + (self.slot_pitch * item))))
+                if self.slot_axis == 'A':
+                    x_adj = self.slot_pitch * math.cos(math.radians(self.slot_linear_angle))
+                    y_adj = self.slot_pitch * math.sin(math.radians(self.slot_linear_angle))
+                    geo_el = self.util_shape(
+                        ((dx + (x_adj * item)), (dy + (y_adj * item)))
+                    )
+
+                if static is None or static is False:
+                    geo_el = affinity.translate(geo_el, xoff=(dx - self.last_dx), yoff=(dy - self.last_dy))
+                geo_el_list.append(geo_el)
+
+            self.last_dx = dx
+            self.last_dy = dy
+            return DrawToolUtilityShape(geo_el_list)
+        else:   # 'Circular'
+            if data[0] is None and data[1] is None:
+                cdx = self.draw_app.x
+                cdy = self.draw_app.y
+            else:
+                cdx = data[0]
+                cdy = data[1]
+
+            # if len(self.pt) > 0:
+            #     temp_points = [x for x in self.pt]
+            #     temp_points.append([cdx, cdy])
+            #     return DrawToolUtilityShape(LineString(temp_points))
+
+            utility_list = []
+
+            try:
+                radius = distance((cdx, cdy), self.origin)
+            except Exception:
+                radius = 0
+
+            if radius == 0:
+                self.draw_app.delete_utility_geometry()
+
+            if len(self.pt) >= 1 and radius > 0:
+                try:
+                    if cdx < self.origin[0]:
+                        radius = -radius
+
+                    # draw the temp geometry
+                    initial_angle = math.asin((cdy - self.origin[1]) / radius)
+
+                    temp_circular_geo = self.circular_util_shape(radius, initial_angle)
+
+                    # draw the line
+                    temp_points = [x for x in self.pt]
+                    temp_points.append([cdx, cdy])
+                    temp_line = LineString(temp_points)
+
+                    for geo_shape in temp_circular_geo:
+                        utility_list.append(geo_shape.geo)
+                    utility_list.append(temp_line)
+
+                    return DrawToolUtilityShape(utility_list)
+                except Exception as e:
+                    log.debug("SlotArray.utility_geometry -- circular -> %s" % str(e))
+
+    def circular_util_shape(self, radius, angle):
+        self.slot_direction = self.draw_app.ui.slot_array_direction_radio.get_value()
+        self.slot_angle = self.draw_app.ui.slot_array_angle_entry.get_value()
+        self.slot_array_size = self.draw_app.ui.slot_array_size_entry.get_value()
+
+        circular_geo = []
+        if self.slot_direction == 'CW':
+            for i in range(self.slot_array_size):
+                angle_radians = math.radians(self.slot_angle * i)
+                x = self.origin[0] + radius * math.cos(-angle_radians + angle)
+                y = self.origin[1] + radius * math.sin(-angle_radians + angle)
+
+                geo_sol = self.util_shape((x, y))
+                geo_sol = affinity.rotate(geo_sol, angle=(math.pi - angle_radians + angle), use_radians=True)
+
+                circular_geo.append(DrawToolShape(geo_sol))
+        else:
+            for i in range(self.slot_array_size):
+                angle_radians = math.radians(self.slot_angle * i)
+                x = self.origin[0] + radius * math.cos(angle_radians + angle)
+                y = self.origin[1] + radius * math.sin(angle_radians + angle)
+
+                geo_sol = self.util_shape((x, y))
+                geo_sol = affinity.rotate(geo_sol, angle=(angle_radians + angle - math.pi), use_radians=True)
+
+                circular_geo.append(DrawToolShape(geo_sol))
+
+        return circular_geo
+
+    def util_shape(self, point):
+        # updating values here allows us to change the aperture on the fly, after the Tool has been started
+        self.selected_dia = self.draw_app.tool2tooldia[self.draw_app.last_tool_selected]
+        self.radius = float(self.selected_dia / 2.0)
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+        try:
+            slot_length = float(self.draw_app.ui.slot_length_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                slot_length = float(self.draw_app.ui.slot_length_entry.get_value().replace(',', '.'))
+                self.draw_app.ui.slot_length_entry.set_value(slot_length)
+            except ValueError:
+                self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                              _("Value is missing or wrong format. Add it and retry."))
+                return
+
+        try:
+            slot_angle = float(self.draw_app.ui.slot_angle_spinner.get_value())
+        except ValueError:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                          _("Value is missing or wrong format. Add it and retry."))
+            return
+
+        if self.draw_app.ui.slot_axis_radio.get_value() == 'X':
+            self.half_width = slot_length / 2.0
+            self.half_height = self.radius
+        else:
+            self.half_width = self.radius
+            self.half_height = slot_length / 2.0
+
+        if point[0] is None and point[1] is None:
+            point_x = self.draw_app.x
+            point_y = self.draw_app.y
+        else:
+            point_x = point[0]
+            point_y = point[1]
+
+        geo = []
+
+        if self.half_height > self.half_width:
+            p1 = (point_x - self.half_width, point_y - self.half_height + self.half_width)
+            p2 = (point_x + self.half_width, point_y - self.half_height + self.half_width)
+            p3 = (point_x + self.half_width, point_y + self.half_height - self.half_width)
+            p4 = (point_x - self.half_width, point_y + self.half_height - self.half_width)
+
+            down_center = [point_x, point_y - self.half_height + self.half_width]
+            d_start_angle = math.pi
+            d_stop_angle = 0.0
+            down_arc = arc(down_center, self.half_width, d_start_angle, d_stop_angle, 'ccw', self.steps_per_circ)
+
+            up_center = [point_x, point_y + self.half_height - self.half_width]
+            u_start_angle = 0.0
+            u_stop_angle = math.pi
+            up_arc = arc(up_center, self.half_width, u_start_angle, u_stop_angle, 'ccw', self.steps_per_circ)
+
+            geo.append(p1)
+            for pt in down_arc:
+                geo.append(pt)
+            geo.append(p2)
+            geo.append(p3)
+            for pt in up_arc:
+                geo.append(pt)
+            geo.append(p4)
+        else:
+            p1 = (point_x - self.half_width + self.half_height, point_y - self.half_height)
+            p2 = (point_x + self.half_width - self.half_height, point_y - self.half_height)
+            p3 = (point_x + self.half_width - self.half_height, point_y + self.half_height)
+            p4 = (point_x - self.half_width + self.half_height, point_y + self.half_height)
+
+            left_center = [point_x - self.half_width + self.half_height, point_y]
+            d_start_angle = math.pi / 2
+            d_stop_angle = 1.5 * math.pi
+            left_arc = arc(left_center, self.half_height, d_start_angle, d_stop_angle, 'ccw', self.steps_per_circ)
+
+            right_center = [point_x + self.half_width - self.half_height, point_y]
+            u_start_angle = 1.5 * math.pi
+            u_stop_angle = math.pi / 2
+            right_arc = arc(right_center, self.half_height, u_start_angle, u_stop_angle, 'ccw', self.steps_per_circ)
+
+            geo.append(p1)
+            geo.append(p2)
+            for pt in right_arc:
+                geo.append(pt)
+            geo.append(p3)
+            geo.append(p4)
+            for pt in left_arc:
+                geo.append(pt)
+
+        # this function return one slot in the slot array and the following will rotate that one slot around it's
+        # center if the radio value is "A".
+        if self.draw_app.ui.slot_axis_radio.get_value() == 'A':
+            return affinity.rotate(Polygon(geo), -slot_angle)
+        else:
+            return Polygon(geo)
+
+    def make(self):
+        self.geometry = []
+        geo = None
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+
+        # add the point to slots if the diameter is a key in the dict, if not, create it add the drill location
+        # to the value, as a list of itself
+        if self.selected_dia not in self.draw_app.slot_points_edit:
+            self.draw_app.slot_points_edit[self.selected_dia] = []
+        for i in range(self.slot_array_size):
+            self.draw_app.slot_points_edit[self.selected_dia].append(self.points)
+
+        self.draw_app.current_storage = self.draw_app.storage_dict[self.selected_dia]
+
+        if self.slot_array == 'linear':    # 'Linear'
+            for item in range(self.slot_array_size):
+                if self.slot_axis == 'X':
+                    geo = self.util_shape(((self.points[0] + (self.slot_pitch * item)), self.points[1]))
+                if self.slot_axis == 'Y':
+                    geo = self.util_shape((self.points[0], (self.points[1] + (self.slot_pitch * item))))
+                if self.slot_axis == 'A':
+                    x_adj = self.slot_pitch * math.cos(math.radians(self.slot_linear_angle))
+                    y_adj = self.slot_pitch * math.sin(math.radians(self.slot_linear_angle))
+                    geo = self.util_shape(
+                        ((self.points[0] + (x_adj * item)), (self.points[1] + (y_adj * item)))
+                    )
+
+                self.geometry.append(DrawToolShape(geo))
+        else:   # 'Circular'
+            if (self.slot_angle * self.slot_array_size) > 360:
+                self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                              _("Too many items for the selected spacing angle."))
+                self.draw_app.app.jump_signal.disconnect()
+                return
+
+            # radius = distance(self.destination, self.origin)
+            # initial_angle = math.asin((self.destination[1] - self.origin[1]) / radius)
+            # for i in range(self.slot_array_size):
+            #     angle_radians = math.radians(self.slot_angle * i)
+            #     if self.slot_direction == 'CW':
+            #         x = self.origin[0] + radius * math.cos(-angle_radians + initial_angle)
+            #         y = self.origin[1] + radius * math.sin(-angle_radians + initial_angle)
+            #     else:
+            #         x = self.origin[0] + radius * math.cos(angle_radians + initial_angle)
+            #         y = self.origin[1] + radius * math.sin(angle_radians + initial_angle)
+            #
+            #     geo = self.util_shape((x, y))
+            #     if self.slot_direction == 'CW':
+            #         geo = affinity.rotate(geo, angle=(math.pi - angle_radians), use_radians=True)
+            #     else:
+            #         geo = affinity.rotate(geo, angle=(angle_radians - math.pi), use_radians=True)
+            #
+            #     self.geometry.append(DrawToolShape(geo))
+
+            radius = distance(self.destination, self.origin)
+            if radius == 0:
+                self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
+                self.draw_app.delete_utility_geometry()
+                self.draw_app.select_tool('drill_select')
+                return
+
+            if self.destination[0] < self.origin[0]:
+                radius = -radius
+            initial_angle = math.asin((self.destination[1] - self.origin[1]) / radius)
+
+            circular_geo = self.circular_util_shape(radius, initial_angle)
+            self.geometry += circular_geo
+
+        self.complete = True
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        self.draw_app.in_action = False
+        self.draw_app.ui.slot_frame.hide()
+        self.draw_app.ui.slot_array_frame.hide()
+        self.draw_app.app.jump_signal.disconnect()
+
+    def on_key(self, key):
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if key_modifier == QtCore.Qt.ShiftModifier:
+            mod_key = 'Shift'
+        elif key_modifier == QtCore.Qt.ControlModifier:
+            mod_key = 'Control'
+        else:
+            mod_key = None
+
+        if mod_key == 'Control':
+            # Toggle Pad Array Direction
+            if key == QtCore.Qt.Key_Space:
+                if self.draw_app.ui.slot_array_axis_radio.get_value() == 'X':
+                    self.draw_app.ui.slot_array_axis_radio.set_value('Y')
+                elif self.draw_app.ui.slot_array_axis_radio.get_value() == 'Y':
+                    self.draw_app.ui.slot_array_axis_radio.set_value('A')
+                elif self.draw_app.ui.slot_array_axis_radio.get_value() == 'A':
+                    self.draw_app.ui.slot_array_axis_radio.set_value('X')
+
+                # ## Utility geometry (animated)
+                self.draw_app.update_utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+        elif mod_key is None:
+            # Toggle Pad Direction
+            if key == QtCore.Qt.Key_Space:
+                if self.draw_app.ui.slot_axis_radio.get_value() == 'X':
+                    self.draw_app.ui.slot_axis_radio.set_value('Y')
+                elif self.draw_app.ui.slot_axis_radio.get_value() == 'Y':
+                    self.draw_app.ui.slot_axis_radio.set_value('A')
+                elif self.draw_app.ui.slot_axis_radio.get_value() == 'A':
+                    self.draw_app.ui.slot_axis_radio.set_value('X')
+                # ## Utility geometry (animated)
+                self.draw_app.update_utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.tools_table_exc.clearSelection()
+        self.draw_app.plot_all()
+
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class ResizeEditorExc(FCShapeTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'drill_resize'
+
+        self.draw_app.app.inform.emit(_("Click on the Drill(s) to resize ..."))
+        self.resize_dia = None
+        self.draw_app.ui.resize_frame.show()
+        self.points = None
+
+        # made this a set so there are no duplicates
+        self.selected_dia_set = set()
+
+        self.current_storage = None
+        self.geometry = []
+        self.destination_storage = None
+
+        self.draw_app.ui.resize_btn.clicked.connect(self.make)
+        self.draw_app.ui.resdrill_entry.editingFinished.connect(self.make)
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+    def make(self):
+        self.draw_app.is_modified = True
+
+        try:
+            self.draw_app.ui.tools_table_exc.itemChanged.disconnect()
+        except TypeError:
+            pass
+
+        try:
+            new_dia = self.draw_app.ui.resdrill_entry.get_value()
+        except Exception:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                          _("Resize drill(s) failed. Please enter a diameter for resize."))
+            return
+
+        if new_dia not in self.draw_app.olddia_newdia:
+            self.destination_storage = AppGeoEditor.make_storage()
+            self.draw_app.storage_dict[new_dia] = self.destination_storage
+
+            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
+            # each time a tool diameter is edited or added
+            self.draw_app.olddia_newdia[new_dia] = new_dia
+        else:
+            self.destination_storage = self.draw_app.storage_dict[new_dia]
+
+        for index in self.draw_app.ui.tools_table_exc.selectedIndexes():
+            row = index.row()
+            # on column 1 in tool tables we hold the diameters, and we retrieve them as strings
+            # therefore below we convert to float
+            dia_on_row = self.draw_app.ui.tools_table_exc.item(row, 1).text()
+            self.selected_dia_set.add(float(dia_on_row))
+
+        # since we add a new tool, we update also the intial state of the tool_table through it's dictionary
+        # we add a new entry in the tool2tooldia dict
+        self.draw_app.tool2tooldia[len(self.draw_app.olddia_newdia)] = new_dia
+
+        sel_shapes_to_be_deleted = []
+
+        if self.selected_dia_set:
+            for sel_dia in self.selected_dia_set:
+                self.current_storage = self.draw_app.storage_dict[sel_dia]
+                for select_shape in self.draw_app.get_selected():
+                    if select_shape in self.current_storage.get_objects():
+
+                        # add new geometry according to the new size
+                        if isinstance(select_shape.geo, MultiLineString):
+                            factor = new_dia / sel_dia
+                            self.geometry.append(DrawToolShape(affinity.scale(select_shape.geo,
+                                                                              xfact=factor,
+                                                                              yfact=factor,
+                                                                              origin='center')))
+                        elif isinstance(select_shape.geo, Polygon):
+                            # I don't have any info regarding the angle of the slot geometry, nor how thick it is or
+                            # how long it is given the angle. So I will have to make an approximation because
+                            # we need to conserve the slot length, we only resize the diameter for the tool
+                            # Therefore scaling won't work and buffering will not work either.
+
+                            # First we get the Linestring that is one that the original slot is built around with the
+                            # tool having the diameter sel_dia
+                            poly = select_shape.geo
+                            xmin, ymin, xmax, ymax = poly.bounds
+                            # a line that is certain to be bigger than our slot because it's the diagonal
+                            # of it's bounding box
+                            poly_diagonal = LineString([(xmin, ymin), (xmax, ymax)])
+                            poly_centroid = poly.centroid
+                            # center of the slot geometry
+                            poly_center = (poly_centroid.x, poly_centroid.y)
+
+                            # make a list of intersections with the rotated line
+                            list_of_cuttings = []
+                            for angle in range(0, 359, 1):
+                                rot_poly_diagonal = affinity.rotate(poly_diagonal, angle=angle, origin=poly_center)
+                                cut_line = rot_poly_diagonal.intersection(poly)
+                                cut_line_len = cut_line.length
+                                list_of_cuttings.append(
+                                    (cut_line_len, cut_line)
+                                )
+                            # find the cut_line with the maximum length which is the LineString for which the start
+                            # and stop point are the start and stop point of the slot as in the Gerber file
+                            cut_line_with_max_length = max(list_of_cuttings, key=lambda i: i[0])[1]
+                            # find the coordinates of this line
+                            cut_line_with_max_length_coords = list(cut_line_with_max_length.coords)
+                            # extract the first and last point of the line and build some buffered polygon circles
+                            # around them
+                            start_pt = Point(cut_line_with_max_length_coords[0])
+                            stop_pt = Point(cut_line_with_max_length_coords[1])
+                            start_cut_geo = start_pt.buffer(new_dia / 2)
+                            stop_cut_geo = stop_pt.buffer(new_dia / 2)
+
+                            # and we cut the above circle polygons from our line and get in this way a line around
+                            # which we can build the new slot by buffering with the new tool diameter
+                            new_line = cut_line_with_max_length.difference(start_cut_geo)
+                            new_line = new_line.difference(stop_cut_geo)
+
+                            # create the geometry for the resized slot by buffering with half of the
+                            # new diameter value, new_dia
+                            new_poly = new_line.buffer(new_dia / 2)
+
+                            self.geometry.append(DrawToolShape(new_poly))
+                        else:
+                            # unexpected geometry so we cancel
+                            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled."))
+                            return
+
+                        # remove the geometry with the old size
+                        self.current_storage.remove(select_shape)
+
+                        # a hack to make the tool_table display less drills per diameter when shape(drill) is deleted
+                        # self.points_edit it's only useful first time when we load the data into the storage
+                        # but is still used as reference when building tool_table in self.build_ui()
+                        # the number of drills displayed in column 2 is just a len(self.points_edit) therefore
+                        # deleting self.points_edit elements (doesn't matter who but just the number)
+                        # solved the display issue.
+                        if isinstance(select_shape.geo, MultiLineString):
+                            try:
+                                del self.draw_app.points_edit[sel_dia][0]
+                            except KeyError:
+                                # if the exception happen here then we are not dealing with drills but with slots
+                                # This should not happen as the drills have MultiLineString geometry and slots have
+                                # Polygon geometry
+                                pass
+                        if isinstance(select_shape.geo, Polygon):
+                            try:
+                                del self.draw_app.slot_points_edit[sel_dia][0]
+                            except KeyError:
+                                # if the exception happen here then we are not dealing with slots but with drills
+                                # This should not happen as the drills have MultiLineString geometry and slots have
+                                # Polygon geometry
+                                pass
+
+                        sel_shapes_to_be_deleted.append(select_shape)
+
+                        # a hack to make the tool_table display more drills/slots per diameter when shape(drill/slot)
+                        # is added.
+                        # self.points_edit it's only useful first time when we load the data into the storage
+                        # but is still used as reference when building tool_table in self.build_ui()
+                        # the number of drills displayed in column 2 is just a len(self.points_edit) therefore
+                        # deleting self.points_edit elements (doesn't matter who but just the number)
+                        # solved the display issue.
+
+                        # for drills
+                        if isinstance(select_shape.geo, MultiLineString):
+                            if new_dia not in self.draw_app.points_edit:
+                                self.draw_app.points_edit[new_dia] = [(0, 0)]
+                            else:
+                                self.draw_app.points_edit[new_dia].append((0, 0))
+
+                        # for slots
+                        if isinstance(select_shape.geo, Polygon):
+                            if new_dia not in self.draw_app.slot_points_edit:
+                                self.draw_app.slot_points_edit[new_dia] = [(0, 0)]
+                            else:
+                                self.draw_app.slot_points_edit[new_dia].append((0, 0))
+
+            for dia_key in list(self.draw_app.storage_dict.keys()):
+                # if following the resize of the drills there will be no more drills for some of the tools then
+                # delete those tools
+                try:
+                    if not self.draw_app.points_edit[dia_key]:
+                        self.draw_app.on_tool_delete(dia_key)
+                except KeyError:
+                    # if the exception happen here then we are not dealing with drills but with slots
+                    # so we try for them
+                    try:
+                        if not self.draw_app.slot_points_edit[dia_key]:
+                            self.draw_app.on_tool_delete(dia_key)
+                    except KeyError:
+                        # if the exception happen here then we are not dealing with slots neither
+                        # therefore something else is not OK so we return
+                        self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' % _("Cancelled."))
+                        return
+
+            # this simple hack is used so we can delete form self.draw_app.selected but
+            # after we no longer iterate through it
+            for shp in sel_shapes_to_be_deleted:
+                self.draw_app.selected.remove(shp)
+
+            # add the new geometry to storage
+            self.draw_app.on_exc_shape_complete(self.destination_storage)
+
+            self.draw_app.build_ui()
+            self.draw_app.replot()
+
+            # empty the self.geometry
+            self.geometry = []
+
+            # we reactivate the signals after the after the tool editing
+            self.draw_app.ui.tools_table_exc.itemChanged.connect(self.draw_app.on_tool_edit)
+
+            self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        else:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Nothing selected."))
+
+        # init this set() for another use perhaps
+        self.selected_dia_set = set()
+
+        self.draw_app.ui.resize_frame.hide()
+        self.complete = True
+
+        # MS: always return to the Select Tool
+        self.draw_app.select_tool("drill_select")
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.tools_table_exc.clearSelection()
+        self.draw_app.plot_all()
+
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class MoveEditorExc(FCShapeTool):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'drill_move'
+
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.origin = None
+        self.destination = None
+        self.sel_limit = self.draw_app.app.defaults["excellon_editor_sel_limit"]
+        self.selection_shape = self.selection_bbox()
+        self.selected_dia_list = []
+
+        self.current_storage = None
+        self.geometry = []
+
+        for index in self.draw_app.ui.tools_table_exc.selectedIndexes():
+            row = index.row()
+            # on column 1 in tool tables we hold the diameters, and we retrieve them as strings
+            # therefore below we convert to float
+            dia_on_row = self.draw_app.ui.tools_table_exc.item(row, 1).text()
+            self.selected_dia_list.append(float(dia_on_row))
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+        if self.draw_app.launched_from_shortcuts is True:
+            self.draw_app.launched_from_shortcuts = False
+        else:
+            if not self.draw_app.get_selected():
+                self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Nothing selected."))
+                self.draw_app.app.ui.select_drill_btn.setChecked(True)
+                self.draw_app.on_tool_select('drill_select')
+            else:
+                self.draw_app.app.inform.emit(_("Click on reference location ..."))
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def click(self, point):
+        if not self.draw_app.get_selected():
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Nothing selected."))
+            return "Nothing to move."
+
+        if self.origin is None:
+            self.set_origin(point)
+            self.draw_app.app.inform.emit(_("Click on target location ..."))
+            return
+        else:
+            self.destination = point
+            self.make()
+
+            # MS: always return to the Select Tool
+            self.draw_app.select_tool("drill_select")
+            return
+
+    def make(self):
+        # Create new geometry
+        dx = self.destination[0] - self.origin[0]
+        dy = self.destination[1] - self.origin[1]
+        sel_shapes_to_be_deleted = []
+
+        for sel_dia in self.selected_dia_list:
+            self.current_storage = self.draw_app.storage_dict[sel_dia]
+            for select_shape in self.draw_app.get_selected():
+                if select_shape in self.current_storage.get_objects():
+
+                    self.geometry.append(DrawToolShape(affinity.translate(select_shape.geo, xoff=dx, yoff=dy)))
+                    self.current_storage.remove(select_shape)
+                    sel_shapes_to_be_deleted.append(select_shape)
+                    self.draw_app.on_exc_shape_complete(self.current_storage)
+                    self.geometry = []
+
+            for shp in sel_shapes_to_be_deleted:
+                self.draw_app.selected.remove(shp)
+            sel_shapes_to_be_deleted = []
+
+        self.draw_app.build_ui()
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except TypeError:
+            pass
+
+    def selection_bbox(self):
+        geo_list = []
+        for select_shape in self.draw_app.get_selected():
+            geometric_data = select_shape.geo
+            try:
+                for g in geometric_data:
+                    geo_list.append(g)
+            except TypeError:
+                geo_list.append(geometric_data)
+
+        xmin, ymin, xmax, ymax = get_shapely_list_bounds(geo_list)
+
+        pt1 = (xmin, ymin)
+        pt2 = (xmax, ymin)
+        pt3 = (xmax, ymax)
+        pt4 = (xmin, ymax)
+
+        return Polygon([pt1, pt2, pt3, pt4])
+
+    def utility_geometry(self, data=None):
+        """
+        Temporary geometry on screen while using this tool.
+
+        :param data:
+        :return:
+        """
+        geo_list = []
+
+        if self.origin is None:
+            return None
+
+        if len(self.draw_app.get_selected()) == 0:
+            return None
+
+        dx = data[0] - self.origin[0]
+        dy = data[1] - self.origin[1]
+
+        if len(self.draw_app.get_selected()) <= self.sel_limit:
+            try:
+                for geom in self.draw_app.get_selected():
+                    geo_list.append(affinity.translate(geom.geo, xoff=dx, yoff=dy))
+            except AttributeError:
+                self.draw_app.select_tool('drill_select')
+                self.draw_app.selected = []
+                return
+            return DrawToolUtilityShape(geo_list)
+        else:
+            try:
+                ss_el = affinity.translate(self.selection_shape, xoff=dx, yoff=dy)
+            except ValueError:
+                ss_el = None
+            return DrawToolUtilityShape(ss_el)
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.tools_table_exc.clearSelection()
+        self.draw_app.plot_all()
+
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class CopyEditorExc(MoveEditorExc):
+    def __init__(self, draw_app):
+        MoveEditorExc.__init__(self, draw_app)
+        self.name = 'drill_copy'
+
+    def make(self):
+        # Create new geometry
+        dx = self.destination[0] - self.origin[0]
+        dy = self.destination[1] - self.origin[1]
+        sel_shapes_to_be_deleted = []
+
+        for sel_dia in self.selected_dia_list:
+            self.current_storage = self.draw_app.storage_dict[sel_dia]
+            for select_shape in self.draw_app.get_selected():
+                if select_shape in self.current_storage.get_objects():
+                    self.geometry.append(DrawToolShape(affinity.translate(select_shape.geo, xoff=dx, yoff=dy)))
+
+                    # Add some fake drills into the self.draw_app.points_edit to update the drill count in tool table
+                    # This may fail if we copy slots.
+                    try:
+                        self.draw_app.points_edit[sel_dia].append((0, 0))
+                    except KeyError:
+                        pass
+
+                    # add some fake slots into the self.draw_app.slots_points_edit
+                    # to update the slot count in tool table
+                    # This may fail if we copy drills.
+                    try:
+                        self.draw_app.slot_points_edit[sel_dia].append((0, 0))
+                    except KeyError:
+                        pass
+
+                    sel_shapes_to_be_deleted.append(select_shape)
+                    self.draw_app.on_exc_shape_complete(self.current_storage)
+                    self.geometry = []
+
+            for shp in sel_shapes_to_be_deleted:
+                self.draw_app.selected.remove(shp)
+            sel_shapes_to_be_deleted = []
+
+        self.draw_app.build_ui()
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        self.draw_app.app.jump_signal.disconnect()
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.tools_table_exc.clearSelection()
+        self.draw_app.plot_all()
+
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class AppExcEditor(QtCore.QObject):
+
+    draw_shape_idx = -1
+
+    def __init__(self, app):
+        # assert isinstance(app, FlatCAMApp.App), "Expected the app to be a FlatCAMApp.App, got %s" % type(app)
+
+        super(AppExcEditor, self).__init__()
+
+        self.app = app
+        self.canvas = self.app.plotcanvas
+        self.units = self.app.defaults['units'].upper()
+
+        self.dec_format = self.app.dec_format
+
+        # Number of decimals used by tools in this class
+        self.decimals = self.app.decimals
+
+        self.ui = AppExcEditorUI(app=self.app)
+
+        self.exc_obj = None
+
+        # ## Toolbar events and properties
+        self.tools_exc = {}
+
+        # ## Data
+        self.active_tool = None
+        self.in_action = False
+
+        self.storage_dict = {}
+        self.current_storage = []
+
+        # build the data from the Excellon point into a dictionary
+        #  {tool_dia: [geometry_in_points]}
+        self.points_edit = {}
+        self.slot_points_edit = {}
+
+        self.sorted_diameters = []
+
+        self.new_drills = []
+        self.new_tools = {}
+        self.new_slots = []
+
+        # dictionary to store the tool_row and diameters in Tool_table
+        # it will be updated everytime self.build_ui() is called
+        self.olddia_newdia = {}
+
+        self.tool2tooldia = {}
+
+        # this will store the value for the last selected tool, for use after clicking on canvas when the selection
+        # is cleared but as a side effect also the selected tool is cleared
+        self.last_tool_selected = None
+        self.utility = []
+
+        # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
+        self.launched_from_shortcuts = False
+
+        # this var will store the state of the toolbar before starting the editor
+        self.toolbar_old_state = False
+
+        if self.units == 'MM':
+            self.tolerance = float(self.app.defaults["global_tolerance"])
+        else:
+            self.tolerance = float(self.app.defaults["global_tolerance"]) / 20
+
+        # VisPy Visuals
+        if self.app.is_legacy is False:
+            self.shapes = self.canvas.new_shape_collection(layers=1)
+            if self.canvas.big_cursor is True:
+                self.tool_shape = self.canvas.new_shape_collection(layers=1, line_width=2)
+            else:
+                self.tool_shape = self.canvas.new_shape_collection(layers=1)
+        else:
+            from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+            self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_exc_editor')
+            self.tool_shape = ShapeCollectionLegacy(obj=self, app=self.app, name='tool_shapes_exc_editor')
+
+        self.app.pool_recreated.connect(self.pool_recreated)
+
+        # Remove from scene
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
+
+        # ## List of selected shapes.
+        self.selected = []
+
+        self.move_timer = QtCore.QTimer()
+        self.move_timer.setSingleShot(True)
+
+        self.key = None  # Currently pressed key
+        self.modifiers = None
+        self.x = None  # Current mouse cursor pos
+        self.y = None
+        # Current snapped mouse pos
+        self.snap_x = None
+        self.snap_y = None
+        self.pos = None
+
+        self.complete = False
+
+        self.options = {
+            "global_gridx":     0.1,
+            "global_gridy":     0.1,
+            "snap_max":         0.05,
+            "grid_snap":        True,
+            "corner_snap":      False,
+            "grid_gap_link":    True
+        }
+        self.options.update(self.app.options)
+
+        for option in self.options:
+            if option in self.app.options:
+                self.options[option] = self.app.options[option]
+
+        self.data_defaults = {}
+
+        self.rtree_exc_index = rtindex.Index()
+        # flag to show if the object was modified
+        self.is_modified = False
+
+        self.edited_obj_name = ""
+
+        # variable to store the total amount of drills per job
+        self.tot_drill_cnt = 0
+        self.tool_row = 0
+
+        # variable to store the total amount of slots per job
+        self.tot_slot_cnt = 0
+        self.tool_row_slots = 0
+
+        self.tool_row = 0
+
+        # def entry2option(option, entry):
+        #     self.options[option] = float(entry.text())
+
+        # Event signals disconnect id holders
+        self.mp = None
+        self.mm = None
+        self.mr = None
+
+        # #############################################################################################################
+        # ######################### Excellon Editor Signals ###########################################################
+        # #############################################################################################################
+
+        # connect the toolbar signals
+        self.connect_exc_toolbar_signals()
+
+        self.ui.convert_slots_btn.clicked.connect(self.on_slots_conversion)
+        self.app.ui.delete_drill_btn.triggered.connect(self.on_delete_btn)
+        self.ui.name_entry.returnPressed.connect(self.on_name_activate)
+        self.ui.addtool_btn.clicked.connect(self.on_tool_add)
+        self.ui.addtool_entry.editingFinished.connect(self.on_tool_add)
+        self.ui.deltool_btn.clicked.connect(self.on_tool_delete)
+        # self.ui.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
+        self.ui.tools_table_exc.cellPressed.connect(self.on_row_selected)
+
+        self.ui.array_type_radio.activated_custom.connect(self.on_array_type_radio)
+        self.ui.slot_array_type_radio.activated_custom.connect(self.on_slot_array_type_radio)
+
+        self.ui.drill_axis_radio.activated_custom.connect(self.on_linear_angle_radio)
+        self.ui.slot_axis_radio.activated_custom.connect(self.on_slot_angle_radio)
+
+        self.ui.slot_array_axis_radio.activated_custom.connect(self.on_slot_array_linear_angle_radio)
+
+        self.app.ui.exc_add_array_drill_menuitem.triggered.connect(self.exc_add_drill_array)
+        self.app.ui.exc_add_drill_menuitem.triggered.connect(self.exc_add_drill)
+
+        self.app.ui.exc_add_array_slot_menuitem.triggered.connect(self.exc_add_slot_array)
+        self.app.ui.exc_add_slot_menuitem.triggered.connect(self.exc_add_slot)
+
+        self.app.ui.exc_resize_drill_menuitem.triggered.connect(self.exc_resize_drills)
+        self.app.ui.exc_copy_drill_menuitem.triggered.connect(self.exc_copy_drills)
+        self.app.ui.exc_delete_drill_menuitem.triggered.connect(self.on_delete_btn)
+
+        self.app.ui.exc_move_drill_menuitem.triggered.connect(self.exc_move_drills)
+        self.ui.exit_editor_button.clicked.connect(lambda: self.app.editor2object())
+
+        log.debug("Initialization of the Excellon Editor is finished ...")
+
+    def make_callback(self, thetool):
+        def f():
+            self.on_tool_select(thetool)
+
+        return f
+
+    def connect_exc_toolbar_signals(self):
+        self.tools_exc.update({
+            "drill_select":     {"button": self.app.ui.select_drill_btn,    "constructor": SelectEditorExc},
+            "drill_add":        {"button": self.app.ui.add_drill_btn,       "constructor": DrillAdd},
+            "drill_array":      {"button": self.app.ui.add_drill_array_btn, "constructor": DrillArray},
+            "slot_add":         {"button": self.app.ui.add_slot_btn,        "constructor": SlotAdd},
+            "slot_array":       {"button": self.app.ui.add_slot_array_btn,  "constructor": SlotArray},
+            "drill_resize":     {"button": self.app.ui.resize_drill_btn,    "constructor": ResizeEditorExc},
+            "drill_copy":       {"button": self.app.ui.copy_drill_btn,      "constructor": CopyEditorExc},
+            "drill_move":       {"button": self.app.ui.move_drill_btn,      "constructor": MoveEditorExc},
+        })
+
+        for tool in self.tools_exc:
+            self.tools_exc[tool]["button"].triggered.connect(self.make_callback(tool))  # Events
+            self.tools_exc[tool]["button"].setCheckable(True)  # Checkable
+
+    def pool_recreated(self, pool):
+        self.shapes.pool = pool
+        self.tool_shape.pool = pool
+
+    @staticmethod
+    def make_storage():
+        # ## Shape storage.
+        storage = FlatCAMRTreeStorage()
+        storage.get_points = DrawToolShape.get_pts
+
+        return storage
+
+    def set_ui(self):
+        # updated units
+        self.units = self.app.defaults['units'].upper()
+
+        self.olddia_newdia.clear()
+        self.tool2tooldia.clear()
+
+        # update the olddia_newdia dict to make sure we have an updated state of the tool_table
+        for key in self.points_edit:
+            self.olddia_newdia[key] = key
+
+        for key in self.slot_points_edit:
+            if key not in self.olddia_newdia:
+                self.olddia_newdia[key] = key
+
+        sort_temp = []
+        for diam in self.olddia_newdia:
+            sort_temp.append(float(diam))
+        self.sorted_diameters = sorted(sort_temp)
+
+        # populate self.intial_table_rows dict with the tool number as keys and tool diameters as values
+        if self.exc_obj.diameterless is False:
+            for i in range(len(self.sorted_diameters)):
+                tt_dia = self.sorted_diameters[i]
+                self.tool2tooldia[i + 1] = tt_dia
+        else:
+            # the Excellon object has diameters that are bogus information, added by the application because the
+            # Excellon file has no tool diameter information. In this case do not order the diameter in the table
+            # but use the real order found in the exc_obj.tools
+            for k, v in self.exc_obj.tools.items():
+                tool_dia = float('%.*f' % (self.decimals, v['tooldia']))
+                self.tool2tooldia[int(k)] = tool_dia
+
+        # Init appGUI
+        self.ui.addtool_entry.set_value(float(self.app.defaults['excellon_editor_newdia']))
+        self.ui.drill_array_size_entry.set_value(int(self.app.defaults['excellon_editor_array_size']))
+        self.ui.drill_axis_radio.set_value(self.app.defaults['excellon_editor_lin_dir'])
+        self.ui.drill_pitch_entry.set_value(float(self.app.defaults['excellon_editor_lin_pitch']))
+        self.ui.linear_angle_spinner.set_value(float(self.app.defaults['excellon_editor_lin_angle']))
+        self.ui.drill_array_dir_radio.set_value(self.app.defaults['excellon_editor_circ_dir'])
+        self.ui.drill_angle_entry.set_value(float(self.app.defaults['excellon_editor_circ_angle']))
+
+        self.ui.slot_length_entry.set_value(float(self.app.defaults['excellon_editor_slot_length']))
+        self.ui.slot_axis_radio.set_value(self.app.defaults['excellon_editor_slot_direction'])
+        self.ui.slot_angle_spinner.set_value(float(self.app.defaults['excellon_editor_slot_angle']))
+
+        self.ui.slot_array_size_entry.set_value(int(self.app.defaults['excellon_editor_slot_array_size']))
+        self.ui.slot_array_axis_radio.set_value(self.app.defaults['excellon_editor_slot_lin_dir'])
+        self.ui.slot_array_pitch_entry.set_value(float(self.app.defaults['excellon_editor_slot_lin_pitch']))
+        self.ui.slot_array_linear_angle_spinner.set_value(float(self.app.defaults['excellon_editor_slot_lin_angle']))
+        self.ui.slot_array_direction_radio.set_value(self.app.defaults['excellon_editor_slot_circ_dir'])
+        self.ui.slot_array_angle_entry.set_value(float(self.app.defaults['excellon_editor_slot_circ_angle']))
+
+        # make sure that th visibility of the various UI frame are updated
+        # according to the set Preferences already loaded
+        self.on_slot_angle_radio()
+
+        self.ui.array_type_radio.set_value('linear')
+        self.on_array_type_radio(val=self.ui.array_type_radio.get_value())
+        self.ui.slot_array_type_radio.set_value('linear')
+        self.on_slot_array_type_radio(val=self.ui.slot_array_type_radio.get_value())
+        self.on_linear_angle_radio()
+        self.on_slot_array_linear_angle_radio()
+
+    def build_ui(self, first_run=None):
+
+        try:
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.ui.tools_table_exc.itemChanged.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.ui.tools_table_exc.cellPressed.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        # updated units
+        self.units = self.app.defaults['units'].upper()
+
+        # make a new name for the new Excellon object (the one with edited content)
+        self.edited_obj_name = self.exc_obj.options['name']
+        self.ui.name_entry.set_value(self.edited_obj_name)
+
+        sort_temp = []
+
+        for diam in self.olddia_newdia:
+            sort_temp.append(float(diam))
+        self.sorted_diameters = sorted(sort_temp)
+
+        # here, self.sorted_diameters will hold in a oblique way, the number of tools
+        n = len(self.sorted_diameters)
+        # we have (n+2) rows because there are 'n' tools, each a row, plus the last 2 rows for totals.
+        self.ui.tools_table_exc.setRowCount(n + 2)
+
+        self.tot_drill_cnt = 0
+        self.tot_slot_cnt = 0
+
+        self.tool_row = 0
+        # this variable will serve as the real tool_number
+        tool_id = 0
+
+        for tool_no in self.sorted_diameters:
+            tool_id += 1
+            drill_cnt = 0  # variable to store the nr of drills per tool
+            slot_cnt = 0  # variable to store the nr of slots per tool
+
+            # Find no of drills for the current tool
+            for tool_dia in self.points_edit:
+                if float(tool_dia) == tool_no:
+                    drill_cnt = len(self.points_edit[tool_dia])
+
+            self.tot_drill_cnt += drill_cnt
+
+            # try:
+            #     # Find no of slots for the current tool
+            #     for slot in self.slot_points_edit:
+            #         if float(slot) == tool_no:
+            #             slot_cnt += 1
+            #
+            #     self.tot_slot_cnt += slot_cnt
+            # except AttributeError:
+            #     # log.debug("No slots in the Excellon file")
+            #     # Find no of slots for the current tool
+            #     for tool_dia in self.slot_points_edit:
+            #         if float(tool_dia) == tool_no:
+            #             slot_cnt = len(self.slot_points_edit[tool_dia])
+            #
+            #     self.tot_slot_cnt += slot_cnt
+
+            for tool_dia in self.slot_points_edit:
+                if float(tool_dia) == tool_no:
+                    slot_cnt = len(self.slot_points_edit[tool_dia])
+
+            self.tot_slot_cnt += slot_cnt
+
+            idd = QtWidgets.QTableWidgetItem('%d' % int(tool_id))
+            idd.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.tools_table_exc.setItem(self.tool_row, 0, idd)  # Tool name/id
+
+            # Make sure that the drill diameter when in MM is with no more than 2 decimals
+            # There are no drill bits in MM with more than 2 decimals diameter
+            # For INCH the decimals should be no more than 4. There are no drills under 10mils
+            dia = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, self.olddia_newdia[tool_no]))
+
+            dia.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            drill_count = QtWidgets.QTableWidgetItem('%d' % drill_cnt)
+            drill_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            # if the slot number is zero is better to not clutter the GUI with zero's so we print a space
+            if slot_cnt > 0:
+                slot_count = QtWidgets.QTableWidgetItem('%d' % slot_cnt)
+            else:
+                slot_count = QtWidgets.QTableWidgetItem('')
+            slot_count.setFlags(QtCore.Qt.ItemIsEnabled)
+
+            self.ui.tools_table_exc.setItem(self.tool_row, 1, dia)  # Diameter
+            self.ui.tools_table_exc.setItem(self.tool_row, 2, drill_count)  # Number of drills per tool
+            self.ui.tools_table_exc.setItem(self.tool_row, 3, slot_count)  # Number of drills per tool
+
+            if first_run is True:
+                # set now the last tool selected
+                self.last_tool_selected = int(tool_id)
+
+            self.tool_row += 1
+
+        # make the diameter column editable
+        for row in range(self.tool_row):
+            self.ui.tools_table_exc.item(row, 1).setFlags(
+                QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.tools_table_exc.item(row, 2).setForeground(QtGui.QColor(0, 0, 0))
+            self.ui.tools_table_exc.item(row, 3).setForeground(QtGui.QColor(0, 0, 0))
+
+        # add a last row with the Total number of drills
+        # HACK: made the text on this cell '9999' such it will always be the one before last when sorting
+        # it will have to have the foreground color (font color) white
+        empty = QtWidgets.QTableWidgetItem('9998')
+        empty.setForeground(QtGui.QColor(255, 255, 255))
+
+        empty.setFlags(empty.flags() ^ QtCore.Qt.ItemIsEnabled)
+        empty_b = QtWidgets.QTableWidgetItem('')
+        empty_b.setFlags(empty_b.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        label_tot_drill_count = QtWidgets.QTableWidgetItem(_('Total Drills'))
+        tot_drill_count = QtWidgets.QTableWidgetItem('%d' % self.tot_drill_cnt)
+
+        label_tot_drill_count.setFlags(label_tot_drill_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+        tot_drill_count.setFlags(tot_drill_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        self.ui.tools_table_exc.setItem(self.tool_row, 0, empty)
+        self.ui.tools_table_exc.setItem(self.tool_row, 1, label_tot_drill_count)
+        self.ui.tools_table_exc.setItem(self.tool_row, 2, tot_drill_count)  # Total number of drills
+        self.ui.tools_table_exc.setItem(self.tool_row, 3, empty_b)
+
+        font = QtGui.QFont()
+        font.setBold(True)
+        font.setWeight(75)
+
+        for k in [1, 2]:
+            self.ui.tools_table_exc.item(self.tool_row, k).setForeground(QtGui.QColor(127, 0, 255))
+            self.ui.tools_table_exc.item(self.tool_row, k).setFont(font)
+
+        self.tool_row += 1
+
+        # add a last row with the Total number of slots
+        # HACK: made the text on this cell '9999' such it will always be the last when sorting
+        # it will have to have the foreground color (font color) white
+        empty_2 = QtWidgets.QTableWidgetItem('9999')
+        empty_2.setForeground(QtGui.QColor(255, 255, 255))
+
+        empty_2.setFlags(empty_2.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        empty_3 = QtWidgets.QTableWidgetItem('')
+        empty_3.setFlags(empty_3.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        label_tot_slot_count = QtWidgets.QTableWidgetItem(_('Total Slots'))
+        tot_slot_count = QtWidgets.QTableWidgetItem('%d' % self.tot_slot_cnt)
+        label_tot_slot_count.setFlags(label_tot_slot_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+        tot_slot_count.setFlags(tot_slot_count.flags() ^ QtCore.Qt.ItemIsEnabled)
+
+        self.ui.tools_table_exc.setItem(self.tool_row, 0, empty_2)
+        self.ui.tools_table_exc.setItem(self.tool_row, 1, label_tot_slot_count)
+        self.ui.tools_table_exc.setItem(self.tool_row, 2, empty_3)
+        self.ui.tools_table_exc.setItem(self.tool_row, 3, tot_slot_count)  # Total number of slots
+
+        for kl in [1, 2, 3]:
+            self.ui.tools_table_exc.item(self.tool_row, kl).setFont(font)
+            self.ui.tools_table_exc.item(self.tool_row, kl).setForeground(QtGui.QColor(0, 70, 255))
+
+        # all the tools are selected by default
+        self.ui.tools_table_exc.selectColumn(0)
+        #
+        self.ui.tools_table_exc.resizeColumnsToContents()
+        self.ui.tools_table_exc.resizeRowsToContents()
+
+        vertical_header = self.ui.tools_table_exc.verticalHeader()
+        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        vertical_header.hide()
+        self.ui.tools_table_exc.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.tools_table_exc.horizontalHeader()
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        # horizontal_header.setStretchLastSection(True)
+
+        # self.ui.tools_table_exc.setSortingEnabled(True)
+        # sort by tool diameter
+        self.ui.tools_table_exc.sortItems(1)
+
+        # After sorting, to display also the number of drills in the right row we need to update self.initial_rows dict
+        # with the new order. Of course the last 2 rows in the tool table are just for display therefore we don't
+        # use them
+        self.tool2tooldia.clear()
+        for row in range(self.ui.tools_table_exc.rowCount() - 2):
+            tool = int(self.ui.tools_table_exc.item(row, 0).text())
+            diameter = float(self.ui.tools_table_exc.item(row, 1).text())
+            self.tool2tooldia[tool] = diameter
+
+        self.ui.tools_table_exc.setMinimumHeight(self.ui.tools_table_exc.getHeight())
+        self.ui.tools_table_exc.setMaximumHeight(self.ui.tools_table_exc.getHeight())
+
+        # make sure no rows are selected so the user have to click the correct row, meaning selecting the correct tool
+        self.ui.tools_table_exc.clearSelection()
+
+        # Remove anything else in the GUI Selected Tab
+        self.app.ui.properties_scroll_area.takeWidget()
+        # Put ourselves in the GUI Properties Tab
+        self.app.ui.properties_scroll_area.setWidget(self.ui.exc_edit_widget)
+        # Switch notebook to Properties page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
+
+        # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated
+        self.ui.tools_table_exc.itemChanged.connect(self.on_tool_edit)
+        self.ui.tools_table_exc.cellPressed.connect(self.on_row_selected)
+
+    def on_tool_add(self, tooldia=None):
+        self.is_modified = True
+        if tooldia:
+            tool_dia = tooldia
+        else:
+            try:
+                tool_dia = float(self.ui.addtool_entry.get_value())
+            except ValueError:
+                # try to convert comma to decimal point. if it's still not working error message and return
+                try:
+                    tool_dia = float(self.ui.addtool_entry.get_value().replace(',', '.'))
+                except ValueError:
+                    self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
+                    return
+
+        if tool_dia not in self.olddia_newdia:
+            storage_elem = AppGeoEditor.make_storage()
+            self.storage_dict[tool_dia] = storage_elem
+
+            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
+            # each time a tool diameter is edited or added
+            self.olddia_newdia[tool_dia] = tool_dia
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Tool already in the original or actual tool list.\n" 
+                                                          "Save and reedit Excellon if you need to add this tool. "))
+            return
+
+        # since we add a new tool, we update also the initial state of the tool_table through it's dictionary
+        # we add a new entry in the tool2tooldia dict
+        self.tool2tooldia[len(self.olddia_newdia)] = tool_dia
+
+        self.app.inform.emit('[success] %s: %s %s' % (_("Added new tool with dia"), str(tool_dia), str(self.units)))
+
+        self.build_ui()
+
+        # make a quick sort through the tool2tooldia dict so we find which row to select
+        row_to_be_selected = None
+        for key in sorted(self.tool2tooldia):
+            if self.tool2tooldia[key] == tool_dia:
+                row_to_be_selected = int(key) - 1
+                self.last_tool_selected = int(key)
+                break
+        try:
+            self.ui.tools_table_exc.selectRow(row_to_be_selected)
+        except TypeError as e:
+            log.debug("AppExcEditor.on_tool_add() --> %s" % str(e))
+
+    def on_tool_delete(self, dia=None):
+        self.is_modified = True
+        deleted_tool_dia_list = []
+
+        try:
+            if dia is None or dia is False:
+                # deleted_tool_dia = float(
+                #     self.ui.tools_table_exc.item(self.ui.tools_table_exc.currentRow(), 1).text())
+                for index in self.ui.tools_table_exc.selectionModel().selectedRows():
+                    row = index.row()
+                    deleted_tool_dia_list.append(float(self.ui.tools_table_exc.item(row, 1).text()))
+            else:
+                if isinstance(dia, list):
+                    for dd in dia:
+                        deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dd)))
+                else:
+                    deleted_tool_dia_list.append(float('%.*f' % (self.decimals, dia)))
+        except Exception:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Select a tool in Tool Table"))
+            return
+
+        for deleted_tool_dia in deleted_tool_dia_list:
+
+            # delete the storage used for that tool
+            storage_elem = AppGeoEditor.make_storage()
+            self.storage_dict[deleted_tool_dia] = storage_elem
+            self.storage_dict.pop(deleted_tool_dia, None)
+
+            # I've added this flag_del variable because dictionary don't like
+            # having keys deleted while iterating through them
+            flag_del = []
+            # self.points_edit.pop(deleted_tool_dia, None)
+            for deleted_tool in self.tool2tooldia:
+                if self.tool2tooldia[deleted_tool] == deleted_tool_dia:
+                    flag_del.append(deleted_tool)
+
+            if flag_del:
+                for tool_to_be_deleted in flag_del:
+                    # delete the tool
+                    self.tool2tooldia.pop(tool_to_be_deleted, None)
+
+                    # delete also the drills from points_edit dict just in case we add the tool again,
+                    # we don't want to show the number of drills from before was deleter
+                    self.points_edit[deleted_tool_dia] = []
+
+            self.olddia_newdia.pop(deleted_tool_dia, None)
+
+            self.app.inform.emit('[success] %s: %s %s' %
+                                 (_("Deleted tool with diameter"), str(deleted_tool_dia), str(self.units)))
+
+        self.replot()
+        # self.app.inform.emit("Could not delete selected tool")
+
+        self.build_ui()
+
+    def on_tool_edit(self, item_changed):
+        # if connected, disconnect the signal from the slot on item_changed as it creates issues
+        try:
+            self.ui.tools_table_exc.itemChanged.disconnect()
+        except TypeError:
+            pass
+
+        try:
+            self.ui.tools_table_exc.cellPressed.disconnect()
+        except TypeError:
+            pass
+        # self.ui.tools_table_exc.selectionModel().currentChanged.disconnect()
+
+        self.is_modified = True
+        # new_dia = None
+
+        try:
+            new_dia = float(self.ui.tools_table_exc.currentItem().text())
+        except ValueError as e:
+            log.debug("AppExcEditor.on_tool_edit() --> %s" % str(e))
+            return
+
+        row_of_item_changed = self.ui.tools_table_exc.currentRow()
+        # rows start with 0, tools start with 1 so we adjust the value by 1
+        key_in_tool2tooldia = row_of_item_changed + 1
+        old_dia = self.tool2tooldia[key_in_tool2tooldia]
+
+        # SOURCE storage
+        source_storage = self.storage_dict[old_dia]
+
+        # DESTINATION storage
+        # tool diameter is not used so we create a new tool with the desired diameter
+        if new_dia not in self.olddia_newdia:
+            destination_storage = AppGeoEditor.make_storage()
+            self.storage_dict[new_dia] = destination_storage
+
+            # self.olddia_newdia dict keeps the evidence on current tools diameters as keys and gets updated on values
+            # each time a tool diameter is edited or added
+            self.olddia_newdia[new_dia] = new_dia
+        else:
+            # tool diameter is already in use so we move the drills from the prior tool to the new tool
+            destination_storage = self.storage_dict[new_dia]
+
+        # since we add a new tool, we update also the intial state of the tool_table through it's dictionary
+        # we add a new entry in the tool2tooldia dict
+        self.tool2tooldia[len(self.olddia_newdia)] = new_dia
+
+        # CHANGE the elements geometry according to the new diameter
+        factor = new_dia / old_dia
+        new_geo = Polygon()
+        for shape_exc in source_storage.get_objects():
+            geo_list = []
+            if isinstance(shape_exc.geo, MultiLineString):
+                for subgeo in shape_exc.geo:
+                    geo_list.append(affinity.scale(subgeo, xfact=factor, yfact=factor, origin='center'))
+                new_geo = MultiLineString(geo_list)
+            elif isinstance(shape_exc.geo, Polygon):
+                # I don't have any info regarding the angle of the slot geometry, nor how thick it is or
+                # how long it is given the angle. So I will have to make an approximation because
+                # we need to conserve the slot length, we only resize the diameter for the tool
+                # Therefore scaling won't work and buffering will not work either.
+
+                # First we get the Linestring that is one that the original slot is built around with the
+                # tool having the diameter sel_dia
+                poly = shape_exc.geo
+                xmin, ymin, xmax, ymax = poly.bounds
+                # a line that is certain to be bigger than our slot because it's the diagonal
+                # of it's bounding box
+                poly_diagonal = LineString([(xmin, ymin), (xmax, ymax)])
+                poly_centroid = poly.centroid
+                # center of the slot geometry
+                poly_center = (poly_centroid.x, poly_centroid.y)
+
+                # make a list of intersections with the rotated line
+                list_of_cuttings = []
+                for angle in range(0, 359, 1):
+                    rot_poly_diagonal = affinity.rotate(poly_diagonal, angle=angle, origin=poly_center)
+                    cut_line = rot_poly_diagonal.intersection(poly)
+                    cut_line_len = cut_line.length
+                    list_of_cuttings.append(
+                        (cut_line_len, cut_line)
+                    )
+                # find the cut_line with the maximum length which is the LineString for which the start
+                # and stop point are the start and stop point of the slot as in the Gerber file
+                cut_line_with_max_length = max(list_of_cuttings, key=lambda i: i[0])[1]
+                # find the coordinates of this line
+                cut_line_with_max_length_coords = list(cut_line_with_max_length.coords)
+                # extract the first and last point of the line and build some buffered polygon circles
+                # around them
+                start_pt = Point(cut_line_with_max_length_coords[0])
+                stop_pt = Point(cut_line_with_max_length_coords[1])
+                start_cut_geo = start_pt.buffer(new_dia / 2)
+                stop_cut_geo = stop_pt.buffer(new_dia / 2)
+
+                # and we cut the above circle polygons from our line and get in this way a line around
+                # which we can build the new slot by buffering with the new tool diameter
+                new_line = cut_line_with_max_length.difference(start_cut_geo)
+                new_line = new_line.difference(stop_cut_geo)
+
+                # create the geometry for the resized slot by buffering with half of the
+                # new diameter value: new_dia
+                new_geo = new_line.buffer(new_dia / 2)
+
+            try:
+                self.points_edit.pop(old_dia, None)
+            except KeyError:
+                pass
+            try:
+                self.slot_points_edit.pop(old_dia, None)
+            except KeyError:
+                pass
+
+            # add bogus drill/slots points (for total count of drills/slots)
+            # for drills
+            if isinstance(shape_exc.geo, MultiLineString):
+                if new_dia not in self.points_edit:
+                    self.points_edit[new_dia] = [(0, 0)]
+                else:
+                    self.points_edit[new_dia].append((0, 0))
+
+            # for slots
+            if isinstance(shape_exc.geo, Polygon):
+                if new_dia not in self.slot_points_edit:
+                    self.slot_points_edit[new_dia] = [(0, 0)]
+                else:
+                    self.slot_points_edit[new_dia].append((0, 0))
+
+            self.add_exc_shape(shape=DrawToolShape(new_geo), storage=destination_storage)
+
+        # update the UI and the CANVAS
+        self.build_ui()
+        self.replot()
+
+        # delete the old tool
+        self.on_tool_delete(dia=old_dia)
+
+        # we reactivate the signals after the after the tool editing
+        self.ui.tools_table_exc.itemChanged.connect(self.on_tool_edit)
+        self.ui.tools_table_exc.cellPressed.connect(self.on_row_selected)
+
+        self.app.inform.emit('[success] %s' % _("Done."))
+
+        # self.ui.tools_table_exc.selectionModel().currentChanged.connect(self.on_row_selected)
+
+    def on_name_activate(self):
+        self.edited_obj_name = self.ui.name_entry.get_value()
+
+    def activate(self):
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(True)
+        self.app.ui.menueditok.setDisabled(False)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(False)
+        self.app.ui.popmenu_save.setVisible(True)
+
+        self.connect_canvas_event_handlers()
+
+        # initialize working objects
+        self.storage_dict = {}
+        self.current_storage = []
+        self.points_edit = {}
+        self.sorted_diameters = []
+        self.new_drills = []
+        self.new_tools = {}
+        self.new_slots = []
+
+        self.olddia_newdia = {}
+
+        self.shapes.enabled = True
+        self.tool_shape.enabled = True
+        # self.app.app_cursor.enabled = True
+
+        self.app.ui.corner_snap_btn.setVisible(True)
+        self.app.ui.snap_magnet.setVisible(True)
+
+        self.app.ui.exc_editor_menu.setDisabled(False)
+        self.app.ui.exc_editor_menu.menuAction().setVisible(True)
+
+        self.app.ui.update_obj_btn.setEnabled(True)
+        self.app.ui.e_editor_cmenu.setEnabled(True)
+
+        self.app.ui.exc_edit_toolbar.setDisabled(False)
+        self.app.ui.exc_edit_toolbar.setVisible(True)
+        # self.app.ui.grid_toolbar.setDisabled(False)
+
+        # start with GRID toolbar activated
+        if self.app.ui.grid_snap_btn.isChecked() is False:
+            self.app.ui.grid_snap_btn.trigger()
+
+        self.app.ui.popmenu_disable.setVisible(False)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(False)
+        self.app.ui.popmenu_properties.setVisible(False)
+        self.app.ui.e_editor_cmenu.menuAction().setVisible(True)
+        self.app.ui.g_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.grb_editor_cmenu.menuAction().setVisible(False)
+
+        # show the UI
+        self.ui.drills_frame.show()
+
+    def deactivate(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(False)
+        self.app.ui.menueditok.setDisabled(True)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(True)
+        self.app.ui.popmenu_save.setVisible(False)
+
+        self.disconnect_canvas_event_handlers()
+        self.clear()
+        self.app.ui.exc_edit_toolbar.setDisabled(True)
+
+        self.app.ui.corner_snap_btn.setVisible(False)
+        self.app.ui.snap_magnet.setVisible(False)
+
+        # set the Editor Toolbar visibility to what was before entering in the Editor
+        self.app.ui.exc_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \
+            else self.app.ui.exc_edit_toolbar.setVisible(True)
+
+        # Disable visuals
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
+        # self.app.app_cursor.enabled = False
+
+        self.app.ui.exc_editor_menu.setDisabled(True)
+        self.app.ui.exc_editor_menu.menuAction().setVisible(False)
+
+        self.app.ui.update_obj_btn.setEnabled(False)
+
+        self.app.ui.popmenu_disable.setVisible(True)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(True)
+        self.app.ui.popmenu_properties.setVisible(True)
+        self.app.ui.g_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.e_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.grb_editor_cmenu.menuAction().setVisible(False)
+
+        # Show original geometry
+        if self.exc_obj:
+            self.exc_obj.visible = True
+
+        # hide the UI
+        self.ui.drills_frame.hide()
+
+    def connect_canvas_event_handlers(self):
+        # ## Canvas events
+
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
+        self.mp = self.canvas.graph_event_connect('mouse_press', self.on_canvas_click)
+        self.mm = self.canvas.graph_event_connect('mouse_move', self.on_canvas_move)
+        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_exc_click_release)
+
+        # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
+        # but those from AppGeoEditor
+        if self.app.is_legacy is False:
+            self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            self.app.plotcanvas.graph_event_disconnect('mouse_double_click', self.app.on_mouse_double_click_over_plot)
+        else:
+            self.app.plotcanvas.graph_event_disconnect(self.app.mp)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mm)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mr)
+            self.app.plotcanvas.graph_event_disconnect(self.app.mdc)
+
+        self.app.collection.view.clicked.disconnect()
+
+        self.app.ui.popmenu_copy.triggered.disconnect()
+        self.app.ui.popmenu_delete.triggered.disconnect()
+        self.app.ui.popmenu_move.triggered.disconnect()
+
+        self.app.ui.popmenu_copy.triggered.connect(self.exc_copy_drills)
+        self.app.ui.popmenu_delete.triggered.connect(self.on_delete_btn)
+        self.app.ui.popmenu_move.triggered.connect(self.exc_move_drills)
+
+        # Excellon Editor
+        self.app.ui.drill.triggered.connect(self.exc_add_drill)
+        self.app.ui.drill_array.triggered.connect(self.exc_add_drill_array)
+
+    def disconnect_canvas_event_handlers(self):
+        # we restore the key and mouse control to FlatCAMApp method
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
+        self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
+        self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
+        self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
+                                                              self.app.on_mouse_click_release_over_plot)
+        self.app.mdc = self.app.plotcanvas.graph_event_connect('mouse_double_click',
+                                                               self.app.on_mouse_double_click_over_plot)
+        self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
+
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_press', self.on_canvas_click)
+            self.canvas.graph_event_disconnect('mouse_move', self.on_canvas_move)
+            self.canvas.graph_event_disconnect('mouse_release', self.on_exc_click_release)
+        else:
+            self.canvas.graph_event_disconnect(self.mp)
+            self.canvas.graph_event_disconnect(self.mm)
+            self.canvas.graph_event_disconnect(self.mr)
+
+        try:
+            self.app.ui.popmenu_copy.triggered.disconnect(self.exc_copy_drills)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.popmenu_delete.triggered.disconnect(self.on_delete_btn)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.popmenu_move.triggered.disconnect(self.exc_move_drills)
+        except (TypeError, AttributeError):
+            pass
+
+        self.app.ui.popmenu_copy.triggered.connect(self.app.on_copy_command)
+        self.app.ui.popmenu_delete.triggered.connect(self.app.on_delete)
+        self.app.ui.popmenu_move.triggered.connect(self.app.obj_move)
+
+        # Excellon Editor
+        try:
+            self.app.ui.drill.triggered.disconnect(self.exc_add_drill)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.drill_array.triggered.disconnect(self.exc_add_drill_array)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+    def clear(self):
+        self.active_tool = None
+        # self.shape_buffer = []
+        self.selected = []
+
+        self.points_edit = {}
+        self.new_tools = {}
+        self.new_drills = []
+
+        # self.storage_dict = {}
+
+        self.shapes.clear(update=True)
+        self.tool_shape.clear(update=True)
+
+        # self.storage = AppExcEditor.make_storage()
+        self.replot()
+
+    def edit_fcexcellon(self, exc_obj):
+        """
+        Imports the geometry from the given FlatCAM Excellon object
+        into the editor.
+
+        :param exc_obj: ExcellonObject object
+        :return: None
+        """
+
+        self.deactivate()
+        self.activate()
+
+        # Hide original geometry
+        self.exc_obj = exc_obj
+        exc_obj.visible = False
+
+        if self.exc_obj:
+            outname = self.exc_obj.options['name']
+        else:
+            outname = ''
+
+        self.data_defaults = {
+            "name":                         outname + '_drill',
+            "plot":                         self.app.defaults["excellon_plot"],
+            "solid":                        self.app.defaults["excellon_solid"],
+            "multicolored":                 self.app.defaults["excellon_multicolored"],
+            "merge_fuse_tools":             self.app.defaults["excellon_merge_fuse_tools"],
+            "format_upper_in":              self.app.defaults["excellon_format_upper_in"],
+            "format_lower_in":              self.app.defaults["excellon_format_lower_in"],
+            "format_upper_mm":              self.app.defaults["excellon_format_upper_mm"],
+            "lower_mm":                     self.app.defaults["excellon_format_lower_mm"],
+            "zeros":                        self.app.defaults["excellon_zeros"],
+
+            "tools_drill_tool_order":       self.app.defaults["tools_drill_tool_order"],
+            "tools_drill_cutz":             self.app.defaults["tools_drill_cutz"],
+            "tools_drill_multidepth":       self.app.defaults["tools_drill_multidepth"],
+            "tools_drill_depthperpass":     self.app.defaults["tools_drill_depthperpass"],
+            "tools_drill_travelz":          self.app.defaults["tools_drill_travelz"],
+
+            "tools_drill_feedrate_z":       self.app.defaults["tools_drill_feedrate_z"],
+            "tools_drill_feedrate_rapid":   self.app.defaults["tools_drill_feedrate_rapid"],
+
+            "tools_drill_toolchange":       self.app.defaults["tools_drill_toolchange"],
+            "tools_drill_toolchangez":      self.app.defaults["tools_drill_toolchangez"],
+            "tools_drill_toolchangexy":     self.app.defaults["tools_drill_toolchangexy"],
+
+            # Drill Slots
+            "tools_drill_drill_slots":      self.app.defaults["tools_drill_drill_slots"],
+            "tools_drill_drill_overlap":    self.app.defaults["tools_drill_drill_overlap"],
+            "tools_drill_last_drill":       self.app.defaults["tools_drill_last_drill"],
+
+            "tools_drill_endz":             self.app.defaults["tools_drill_endz"],
+            "tools_drill_endxy":            self.app.defaults["tools_drill_endxy"],
+            "tools_drill_startz":           self.app.defaults["tools_drill_startz"],
+            "tools_drill_offset":           self.app.defaults["tools_drill_offset"],
+            "tools_drill_spindlespeed":     self.app.defaults["tools_drill_spindlespeed"],
+            "tools_drill_dwell":            self.app.defaults["tools_drill_dwell"],
+            "tools_drill_dwelltime":        self.app.defaults["tools_drill_dwelltime"],
+            "tools_drill_ppname_e":         self.app.defaults["tools_drill_ppname_e"],
+            "tools_drill_z_pdepth":         self.app.defaults["tools_drill_z_pdepth"],
+            "tools_drill_feedrate_probe":   self.app.defaults["tools_drill_feedrate_probe"],
+            "tools_drill_spindledir":       self.app.defaults["tools_drill_spindledir"],
+            "tools_drill_f_plunge":         self.app.defaults["tools_drill_f_plunge"],
+            "tools_drill_f_retract":        self.app.defaults["tools_drill_f_retract"],
+
+            "tools_drill_area_exclusion":   self.app.defaults["tools_drill_area_exclusion"],
+            "tools_drill_area_shape":       self.app.defaults["tools_drill_area_shape"],
+            "tools_drill_area_strategy":    self.app.defaults["tools_drill_area_strategy"],
+            "tools_drill_area_overz":       self.app.defaults["tools_drill_area_overz"],
+        }
+
+        # fill in self.default_data values from self.options
+        for opt_key, opt_val in self.app.options.items():
+            if opt_key.find('excellon_') == 0:
+                self.data_defaults[opt_key] = deepcopy(opt_val)
+
+        self.points_edit = {}
+        # build the self.points_edit dict {dimaters: [point_list]}
+        for tool, tool_dict in self.exc_obj.tools.items():
+            tool_dia = self.dec_format(self.exc_obj.tools[tool]['tooldia'])
+
+            if 'drills' in tool_dict and tool_dict['drills']:
+                for drill in tool_dict['drills']:
+                    try:
+                        self.points_edit[tool_dia].append(drill)
+                    except KeyError:
+                        self.points_edit[tool_dia] = [drill]
+
+        self.slot_points_edit = {}
+        # build the self.slot_points_edit dict {dimaters: {"start": Point, "stop": Point}}
+        for tool, tool_dict in self.exc_obj.tools.items():
+            tool_dia = float('%.*f' % (self.decimals, self.exc_obj.tools[tool]['tooldia']))
+
+            if 'slots' in tool_dict and tool_dict['slots']:
+                for slot in tool_dict['slots']:
+                    try:
+                        self.slot_points_edit[tool_dia].append({
+                            "start": slot[0],
+                            "stop": slot[1]
+                        })
+                    except KeyError:
+                        self.slot_points_edit[tool_dia] = [{
+                            "start": slot[0],
+                            "stop": slot[1]
+                        }]
+
+        # Set selection tolerance
+        # DrawToolShape.tolerance = fc_excellon.drawing_tolerance * 10
+
+        self.select_tool("drill_select")
+
+        # reset the tool table
+        self.ui.tools_table_exc.clear()
+        self.ui.tools_table_exc.setHorizontalHeaderLabels(['#', _('Diameter'), 'D', 'S'])
+        self.last_tool_selected = None
+
+        self.set_ui()
+
+        # now that we have data, create the appGUI interface and add it to the Tool Tab
+        self.build_ui(first_run=True)
+
+        # we activate this after the initial build as we don't need to see the tool been populated
+        self.ui.tools_table_exc.itemChanged.connect(self.on_tool_edit)
+
+        # build the geometry for each tool-diameter, each drill will be represented by a '+' symbol
+        # and then add it to the storage elements (each storage elements is a member of a list
+        for tool_dia in self.points_edit:
+            storage_elem = AppGeoEditor.make_storage()
+            for point in self.points_edit[tool_dia]:
+                # make a '+' sign, the line length is the tool diameter
+                start_hor_line = ((point.x - (tool_dia / 2)), point.y)
+                stop_hor_line = ((point.x + (tool_dia / 2)), point.y)
+                start_vert_line = (point.x, (point.y - (tool_dia / 2)))
+                stop_vert_line = (point.x, (point.y + (tool_dia / 2)))
+                shape_geo = MultiLineString([(start_hor_line, stop_hor_line), (start_vert_line, stop_vert_line)])
+                if shape_geo is not None:
+                    self.add_exc_shape(DrawToolShape(shape_geo), storage_elem)
+            self.storage_dict[tool_dia] = storage_elem
+
+        # slots
+        for tool_dia in self.slot_points_edit:
+            buf_value = float(tool_dia) / 2
+            for elem_dict in self.slot_points_edit[tool_dia]:
+
+                line_geo = LineString([elem_dict['start'], elem_dict['stop']])
+                shape_geo = line_geo.buffer(buf_value)
+
+                if tool_dia not in self.storage_dict:
+                    storage_elem = AppGeoEditor.make_storage()
+                    self.storage_dict[tool_dia] = storage_elem
+
+                if shape_geo is not None:
+                    self.add_exc_shape(DrawToolShape(shape_geo), self.storage_dict[tool_dia])
+
+        self.replot()
+
+        # add a first tool in the Tool Table but only if the Excellon Object is empty
+        if not self.tool2tooldia:
+            self.on_tool_add(self.dec_format(float(self.app.defaults['excellon_editor_newdia'])))
+
+    def update_fcexcellon(self, exc_obj):
+        """
+        Create a new Excellon object that contain the edited content of the source Excellon object
+
+        :param exc_obj: ExcellonObject
+        :return: None
+        """
+
+        # this dictionary will contain tooldia's as keys and a list of coordinates tuple as values
+        # the values of this dict are coordinates of the holes (drills)
+        edited_points = {}
+
+        """
+         - this dictionary will contain tooldia's as keys and a list of another dicts as values
+         - the dict element of the list has the structure
+         ================  ====================================
+        Key               Value
+        ================  ====================================
+        start             (Shapely.Point) Start point of the slot
+        stop              (Shapely.Point) Stop point of the slot
+        ================  ====================================
+        """
+        edited_slot_points = {}
+
+        for storage_tooldia in self.storage_dict:
+            for x in self.storage_dict[storage_tooldia].get_objects():
+                if isinstance(x.geo, MultiLineString):
+                    # all x.geo in self.storage_dict[storage] are MultiLinestring objects for drills
+                    # each MultiLineString is made out of Linestrings
+                    # select first Linestring object in the current MultiLineString
+                    first_linestring = x.geo[0]
+                    # get it's coordinates
+                    first_linestring_coords = first_linestring.coords
+                    x_coord = first_linestring_coords[0][0] + (float(first_linestring.length / 2))
+                    y_coord = first_linestring_coords[0][1]
+
+                    # create a tuple with the coordinates (x, y) and add it to the list that is the value of the
+                    # edited_points dictionary
+                    point = (x_coord, y_coord)
+                    if storage_tooldia not in edited_points:
+                        edited_points[storage_tooldia] = [point]
+                    else:
+                        edited_points[storage_tooldia].append(point)
+                elif isinstance(x.geo, Polygon):
+                    # create a tuple with the points (start, stop) and add it to the list that is the value of the
+                    # edited_points dictionary
+
+                    # first determine the start and stop coordinates for the slot knowing the geometry and the tool
+                    # diameter
+                    radius = float(storage_tooldia) / 2
+                    radius = radius - 0.0000001
+
+                    poly = x.geo
+                    poly = poly.buffer(-radius)
+
+                    if not poly.is_valid or poly.is_empty:
+                        # print("Polygon not valid: %s" % str(poly.wkt))
+                        continue
+
+                    xmin, ymin, xmax, ymax = poly.bounds
+                    line_one = LineString([(xmin, ymin), (xmax, ymax)]).intersection(poly).length
+                    line_two = LineString([(xmin, ymax), (xmax, ymin)]).intersection(poly).length
+
+                    if line_one < line_two:
+                        point_elem = {
+                            "start": (xmin, ymax),
+                            "stop": (xmax, ymin)
+                        }
+                    else:
+                        point_elem = {
+                            "start": (xmin, ymin),
+                            "stop": (xmax, ymax)
+                        }
+
+                    if storage_tooldia not in edited_slot_points:
+                        edited_slot_points[storage_tooldia] = [point_elem]
+                    else:
+                        edited_slot_points[storage_tooldia].append(point_elem)
+
+        # recreate the drills and tools to be added to the new Excellon edited object
+        # first, we look in the tool table if one of the tool diameters was changed then
+        # append that a tuple formed by (old_dia, edited_dia) to a list
+        changed_key = set()
+        for initial_dia in self.olddia_newdia:
+            edited_dia = self.olddia_newdia[initial_dia]
+            if edited_dia != initial_dia:
+                # for drills
+                for old_dia in edited_points:
+                    if old_dia == initial_dia:
+                        changed_key.add((old_dia, edited_dia))
+                # for slots
+                for old_dia in edited_slot_points:
+                    if old_dia == initial_dia:
+                        changed_key.add((old_dia, edited_dia))
+            # if the initial_dia is not in edited_points it means it is a new tool with no drill points
+            # (and we have to add it)
+            # because in case we have drill points it will have to be already added in edited_points
+            # if initial_dia not in edited_points.keys():
+            #     edited_points[initial_dia] = []
+
+        for el in changed_key:
+            edited_points[el[1]] = edited_points.pop(el[0])
+            edited_slot_points[el[1]] = edited_slot_points.pop(el[0])
+
+        # Let's sort the edited_points dictionary by keys (diameters) and store the result in a zipped list
+        # ordered_edited_points is a ordered list of tuples;
+        # element[0] of the tuple is the diameter and
+        # element[1] of the tuple is a list of coordinates (a tuple themselves)
+        ordered_edited_points = sorted(zip(edited_points.keys(), edited_points.values()))
+
+        current_tool = 0
+        for tool_dia in ordered_edited_points:
+            current_tool += 1
+
+            # create the self.tools for the new Excellon object (the one with edited content)
+            if current_tool not in self.new_tools:
+                self.new_tools[current_tool] = {}
+            self.new_tools[current_tool]['tooldia'] = float(tool_dia[0])
+
+            # add in self.tools the 'solid_geometry' key, the value (a list) is populated below
+            self.new_tools[current_tool]['solid_geometry'] = []
+
+            # create the self.drills for the new Excellon object (the one with edited content)
+            for point in tool_dia[1]:
+                try:
+                    self.new_tools[current_tool]['drills'].append(Point(point))
+                except KeyError:
+                    self.new_tools[current_tool]['drills'] = [Point(point)]
+
+                # repopulate the 'solid_geometry' for each tool
+                poly = Point(point).buffer(float(tool_dia[0]) / 2.0, int(int(exc_obj.geo_steps_per_circle) / 4))
+                self.new_tools[current_tool]['solid_geometry'].append(poly)
+
+        ordered_edited_slot_points = sorted(zip(edited_slot_points.keys(), edited_slot_points.values()))
+        for tool_dia in ordered_edited_slot_points:
+
+            tool_exist_flag = False
+            for tool in self.new_tools:
+                if tool_dia[0] == self.new_tools[tool]["tooldia"]:
+                    current_tool = tool
+                    tool_exist_flag = True
+                    break
+
+            if tool_exist_flag is False:
+                current_tool += 1
+
+                # create the self.tools for the new Excellon object (the one with edited content)
+                if current_tool not in self.new_tools:
+                    self.new_tools[current_tool] = {}
+                self.new_tools[current_tool]['tooldia'] = float(tool_dia[0])
+
+                # add in self.tools the 'solid_geometry' key, the value (a list) is populated below
+                self.new_tools[current_tool]['solid_geometry'] = []
+
+            # create the self.slots for the new Excellon object (the one with edited content)
+            for coord_dict in tool_dia[1]:
+                slot = (
+                    Point(coord_dict['start']),
+                    Point(coord_dict['stop'])
+                )
+                try:
+                    self.new_tools[current_tool]['slots'].append(slot)
+                except KeyError:
+                    self.new_tools[current_tool]['slots'] = [slot]
+
+                # repopulate the 'solid_geometry' for each tool
+                poly = LineString([coord_dict['start'], coord_dict['stop']]).buffer(
+                    float(tool_dia[0]) / 2.0, int(int(exc_obj.geo_steps_per_circle) / 4)
+                )
+                self.new_tools[current_tool]['solid_geometry'].append(poly)
+
+        if self.is_modified is True:
+            if "_edit" in self.edited_obj_name:
+                try:
+                    idd = int(self.edited_obj_name[-1]) + 1
+                    self.edited_obj_name = self.edited_obj_name[:-1] + str(idd)
+                except ValueError:
+                    self.edited_obj_name += "_1"
+            else:
+                self.edited_obj_name += "_edit"
+
+        self.app.worker_task.emit({'fcn': self.new_edited_excellon,
+                                   'params': [self.edited_obj_name,
+                                              self.new_drills,
+                                              self.new_slots,
+                                              self.new_tools]})
+
+        return self.edited_obj_name
+
+    @staticmethod
+    def update_options(obj):
+        try:
+            if not obj.options:
+                obj.options = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0}
+                return True
+            else:
+                return False
+        except AttributeError:
+            obj.options = {}
+            return True
+
+    def new_edited_excellon(self, outname, n_drills, n_slots, n_tools):
+        """
+        Creates a new Excellon object for the edited Excellon. Thread-safe.
+
+        :param outname:     Name of the resulting object. None causes the
+                            name to be that of the file.
+        :type outname:      str
+
+        :param n_drills:    The new Drills storage
+        :param n_slots:     The new Slots storage
+        :param n_tools:     The new Tools storage
+        :return:            None
+        """
+
+        self.app.log.debug("Update the Excellon object with edited content. Source is %s" %
+                           self.exc_obj.options['name'])
+
+        new_drills = n_drills
+        new_slots = n_slots
+        new_tools = n_tools
+
+        # How the object should be initialized
+        def obj_init(excellon_obj, app_obj):
+
+            excellon_obj.drills = deepcopy(new_drills)
+            excellon_obj.tools = deepcopy(new_tools)
+            excellon_obj.slots = deepcopy(new_slots)
+
+            excellon_obj.options['name'] = outname
+
+            # add a 'data' dict for each tool with the default values
+            for tool in excellon_obj.tools:
+                excellon_obj.tools[tool]['data'] = {}
+                excellon_obj.tools[tool]['data'].update(deepcopy(self.data_defaults))
+
+            try:
+                excellon_obj.create_geometry()
+            except KeyError:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("There are no Tools definitions in the file. Aborting Excellon creation.")
+                                     )
+            except Exception:
+                msg = '[ERROR] %s' % \
+                      _("An internal error has occurred. See shell.\n")
+                msg += traceback.format_exc()
+                app_obj.inform.emit(msg)
+                return
+
+        with self.app.proc_container.new(_("Creating Excellon.")):
+
+            try:
+                edited_obj = self.app.app_obj.new_object("excellon", outname, obj_init)
+                edited_obj.source_file = self.app.f_handlers.export_excellon(obj_name=edited_obj.options['name'],
+                                                                             local_use=edited_obj,
+                                                                             filename=None,
+                                                                             use_thread=False)
+            except Exception as e:
+                self.deactivate()
+                log.error("Error on Edited object creation: %s" % str(e))
+                return
+
+            self.deactivate()
+            self.app.inform.emit('[success] %s' % _("Excellon editing finished."))
+
+    def on_tool_select(self, tool):
+        """
+        Behavior of the toolbar. Tool initialization.
+
+        :rtype : None
+        """
+        current_tool = tool
+
+        self.app.log.debug("on_tool_select('%s')" % tool)
+
+        if self.last_tool_selected is None and current_tool != 'drill_select':
+            # self.draw_app.select_tool('drill_select')
+            self.complete = True
+            current_tool = 'drill_select'
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. There is no Tool/Drill selected"))
+
+        # This is to make the group behave as radio group
+        if current_tool in self.tools_exc:
+            if self.tools_exc[current_tool]["button"].isChecked():
+                self.app.log.debug("%s is checked." % current_tool)
+                for t in self.tools_exc:
+                    if t != current_tool:
+                        self.tools_exc[t]["button"].setChecked(False)
+
+                # this is where the Editor toolbar classes (button's) are instantiated
+                self.active_tool = self.tools_exc[current_tool]["constructor"](self)
+                # self.app.inform.emit(self.active_tool.start_msg)
+            else:
+                self.app.log.debug("%s is NOT checked." % current_tool)
+                for t in self.tools_exc:
+                    self.tools_exc[t]["button"].setChecked(False)
+
+                self.select_tool('drill_select')
+                self.active_tool = SelectEditorExc(self)
+
+    def on_row_selected(self, row, col):
+        if col == 0:
+            key_modifier = QtWidgets.QApplication.keyboardModifiers()
+            if self.app.defaults["global_mselect_key"] == 'Control':
+                modifier_to_use = Qt.ControlModifier
+            else:
+                modifier_to_use = Qt.ShiftModifier
+
+            if key_modifier == modifier_to_use:
+                pass
+            else:
+                self.selected = []
+
+            try:
+                selected_dia = self.tool2tooldia[self.ui.tools_table_exc.currentRow() + 1]
+                self.last_tool_selected = int(self.ui.tools_table_exc.currentRow()) + 1
+                for obj in self.storage_dict[selected_dia].get_objects():
+                    self.selected.append(obj)
+            except Exception as e:
+                self.app.log.debug(str(e))
+
+            self.replot()
+
+    def on_canvas_click(self, event):
+        """
+        event.x and .y have canvas coordinates
+        event.xdata and .ydata have plot coordinates
+
+        :param event:       Event object dispatched by VisPy
+        :return:            None
+        """
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            # right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            # right_button = 3
+
+        if event.button == 1:
+            self.pos = self.canvas.translate_coords(event_pos)
+
+            if self.app.grid_status():
+                self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
+            else:
+                self.pos = (self.pos[0], self.pos[1])
+
+            self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
+
+            # Selection with left mouse button
+            if self.active_tool is not None and event.button == 1:
+                # Dispatch event to active_tool
+                # msg = self.active_tool.click(self.app.geo_editor.snap(event.xdata, event.ydata))
+                self.active_tool.click(self.app.geo_editor.snap(self.pos[0], self.pos[1]))
+
+                # If it is a shape generating tool
+                if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
+                    if self.current_storage is not None:
+                        self.on_exc_shape_complete(self.current_storage)
+                        self.build_ui()
+
+                    # MS: always return to the Select Tool if modifier key is not pressed
+                    # else return to the current tool
+                    key_modifier = QtWidgets.QApplication.keyboardModifiers()
+                    if self.app.defaults["global_mselect_key"] == 'Control':
+                        modifier_to_use = Qt.ControlModifier
+                    else:
+                        modifier_to_use = Qt.ShiftModifier
+
+                    # if modifier key is pressed then we add to the selected list the current shape but if it's already
+                    # in the selected list, we removed it. Therefore first click selects, second deselects.
+                    if key_modifier == modifier_to_use:
+                        self.select_tool(self.active_tool.name)
+                    else:
+                        # return to Select tool but not for FCDrillAdd or SlotAdd
+                        if isinstance(self.active_tool, DrillAdd) or isinstance(self.active_tool, SlotAdd):
+                            self.select_tool(self.active_tool.name)
+                        else:
+                            self.select_tool("drill_select")
+                        return
+
+                if isinstance(self.active_tool, SelectEditorExc):
+                    # self.app.log.debug("Replotting after click.")
+                    self.replot()
+            else:
+                self.app.log.debug("No active tool to respond to click!")
+
+    def on_exc_click_release(self, event):
+        """
+        Handler of the "mouse_release" event.
+        It will pop-up the context menu on right mouse click unless there was a panning move (decided in the
+        "mouse_move" event handler) and only if the current tool is the Select tool.
+        It will 'close' a Editor tool if it is the case.
+
+        :param event:       Event object dispatched by VisPy SceneCavas
+        :return:            None
+        """
+
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
+
+        pos_canvas = self.canvas.translate_coords(event_pos)
+
+        if self.app.grid_status():
+            pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+        else:
+            pos = (pos_canvas[0], pos_canvas[1])
+
+        # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
+        # canvas menu
+        try:
+            if event.button == right_button:  # right click
+                if self.app.ui.popMenu.mouse_is_panning is False:
+                    try:
+                        QtGui.QGuiApplication.restoreOverrideCursor()
+                    except Exception:
+                        pass
+                    if self.active_tool.complete is False and not isinstance(self.active_tool, SelectEditorExc):
+                        self.active_tool.complete = True
+                        self.in_action = False
+                        self.delete_utility_geometry()
+                        self.app.inform.emit('[success] %s' % _("Done."))
+                        self.select_tool('drill_select')
+                    else:
+                        if isinstance(self.active_tool, DrillAdd):
+                            self.active_tool.complete = True
+                            self.in_action = False
+                            self.delete_utility_geometry()
+                            self.app.inform.emit('[success] %s' % _("Done."))
+                            self.select_tool('drill_select')
+
+                        self.app.cursor = QtGui.QCursor()
+                        self.app.populate_cmenu_grids()
+                        self.app.ui.popMenu.popup(self.app.cursor.pos())
+
+        except Exception as e:
+            log.warning("AppExcEditor.on_exc_click_release() RMB click --> Error: %s" % str(e))
+            raise
+
+        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
+        # selection and then select a type of selection ("enclosing" or "touching")
+        try:
+            if event.button == 1:  # left click
+                if self.app.selection_type is not None:
+                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
+                    self.app.selection_type = None
+
+                elif isinstance(self.active_tool, SelectEditorExc):
+                    self.active_tool.click_release((self.pos[0], self.pos[1]))
+
+                    # if there are selected objects then plot them
+                    if self.selected:
+                        self.replot()
+        except Exception as e:
+            log.warning("AppExcEditor.on_exc_click_release() LMB click --> Error: %s" % str(e))
+            raise
+
+    def on_canvas_move(self, event):
+        """
+        Called on 'mouse_move' event.
+        It updates the mouse cursor if the grid snapping is ON.
+        It decide if we have a mouse drag and if it is done with the right mouse click. Then it passes this info to a
+        class object which is used in the "mouse_release" handler to decide if to pop-up the context menu or not.
+        It draws utility_geometry for the Editor tools.
+        Update the position labels from status bar.
+        Decide if we have a right to left or a left to right mouse drag with left mouse button and call a function
+        that will draw a selection shape on canvas.
+
+        event.pos have canvas screen coordinates
+
+        :param event:       Event object dispatched by VisPy SceneCavas
+        :return:            None
+        """
+
+        if not self.app.plotcanvas.native.hasFocus():
+            self.app.plotcanvas.native.setFocus()
+
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            event_is_dragging = event.is_dragging
+            right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
+
+        pos = self.canvas.translate_coords(event_pos)
+        event.xdata, event.ydata = pos[0], pos[1]
+
+        self.x = event.xdata
+        self.y = event.ydata
+
+        self.app.ui.popMenu.mouse_is_panning = False
+
+        # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
+        if event.button == right_button and event_is_dragging == 1:
+            self.app.ui.popMenu.mouse_is_panning = True
+            return
+
+        try:
+            x = float(event.xdata)
+            y = float(event.ydata)
+        except TypeError:
+            return
+
+        if self.active_tool is None:
+            return
+
+        # ## Snap coordinates
+        if self.app.grid_status():
+            x, y = self.app.geo_editor.snap(x, y)
+
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
+                                         edge_width=self.app.defaults["global_cursor_width"],
+                                         size=self.app.defaults["global_cursor_size"])
+
+        self.snap_x = x
+        self.snap_y = y
+
+        if self.pos is None:
+            self.pos = (0, 0)
+        self.app.dx = x - self.pos[0]
+        self.app.dy = y - self.pos[1]
+
+        # # update the position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                           "<b>Y</b>: %.4f&nbsp;" % (x, y))
+        # # update the reference position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+
+        units = self.app.defaults["units"].lower()
+        self.app.plotcanvas.text_hud.text = \
+            'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX:  \t{:<.4f} [{:s}]\nY:  \t{:<.4f} [{:s}]'.format(
+                self.app.dx, units, self.app.dy, units, x, units, y, units)
+
+        # ## Utility geometry (animated)
+        self.update_utility_geometry(data=(x, y))
+
+        # ## Selection area on canvas section # ##
+        if event_is_dragging == 1 and event.button == 1:
+            # I make an exception for FCDrillAdd and DrillArray because clicking and dragging while making regions
+            # can create strange issues. Also for SlotAdd and SlotArray
+            if isinstance(self.active_tool, DrillAdd) or isinstance(self.active_tool, DrillArray) or \
+                    isinstance(self.active_tool, SlotAdd) or isinstance(self.active_tool, SlotArray):
+                self.app.selection_type = None
+            else:
+                dx = pos[0] - self.pos[0]
+                self.app.delete_selection_shape()
+                if dx < 0:
+                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y),
+                                                         color=self.app.defaults["global_alt_sel_line"],
+                                                         face_color=self.app.defaults['global_alt_sel_fill'])
+                    self.app.selection_type = False
+                else:
+                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y))
+                    self.app.selection_type = True
+        else:
+            self.app.selection_type = None
+
+        # Update cursor
+        self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
+                                     edge_width=self.app.defaults["global_cursor_width"],
+                                     size=self.app.defaults["global_cursor_size"])
+
+    def add_exc_shape(self, shape, storage):
+        """
+        Adds a shape to a specified shape storage.
+
+        :param shape:       Shape to be added.
+        :type shape:        DrawToolShape
+        :param storage:     object where to store the shapes
+        :return:            None
+        """
+        # List of DrawToolShape?
+        if isinstance(shape, list):
+            for subshape in shape:
+                self.add_exc_shape(subshape, storage)
+            return
+
+        assert isinstance(shape, DrawToolShape), \
+            "Expected a DrawToolShape, got %s" % str(type(shape))
+
+        assert shape.geo is not None, \
+            "Shape object has empty geometry (None)"
+
+        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list), \
+            "Shape objects has empty geometry ([])"
+
+        if isinstance(shape, DrawToolUtilityShape):
+            self.utility.append(shape)
+        else:
+            storage.insert(shape)  # TODO: Check performance
+
+    def add_shape(self, shape):
+        """
+        Adds a shape to the shape storage.
+
+        :param shape:       Shape to be added.
+        :type shape:        DrawToolShape
+        :return:            None
+        """
+
+        # List of DrawToolShape?
+        if isinstance(shape, list):
+            for subshape in shape:
+                self.add_shape(subshape)
+            return
+
+        assert isinstance(shape, DrawToolShape), \
+            "Expected a DrawToolShape, got %s" % type(shape)
+
+        assert shape.geo is not None, \
+            "Shape object has empty geometry (None)"
+
+        assert (isinstance(shape.geo, list) and len(shape.geo) > 0) or not isinstance(shape.geo, list), \
+            "Shape objects has empty geometry ([])"
+
+        if isinstance(shape, DrawToolUtilityShape):
+            self.utility.append(shape)
+        # else:
+        #     self.storage.insert(shape)
+
+    def on_exc_shape_complete(self, storage):
+        self.app.log.debug("on_shape_complete()")
+
+        # Add shape
+        if type(storage) is list:
+            for item_storage in storage:
+                self.add_exc_shape(self.active_tool.geometry, item_storage)
+        else:
+            self.add_exc_shape(self.active_tool.geometry, storage)
+
+        # Remove any utility shapes
+        self.delete_utility_geometry()
+        self.tool_shape.clear(update=True)
+
+        # Replot and reset tool.
+        self.replot()
+        # self.active_tool = type(self.active_tool)(self)
+
+    def draw_selection_area_handler(self, start, end, sel_type):
+        """
+        This function is called whenever we have a left mouse click release and only we have a left mouse click drag,
+        be it from left to right or from right to left. The direction of the drag is decided in the "mouse_move"
+        event handler.
+        Pressing a modifier key (eg. Ctrl, Shift or Alt) will change the behavior of the selection.
+
+        Depending on which tool belongs the selected shapes, the corresponding rows in the Tools Table are selected or
+        deselected.
+
+        :param start:       mouse position when the selection LMB click was done
+        :param end:         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:
+        """
+
+        start_pos = (start[0], start[1])
+        end_pos = (end[0], end[1])
+        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
+        modifiers = None
+
+        # delete the selection shape that was just drawn, we no longer need it
+        self.app.delete_selection_shape()
+
+        # detect if a modifier key was pressed while the left mouse button was released
+        self.modifiers = QtWidgets.QApplication.keyboardModifiers()
+        if self.modifiers == QtCore.Qt.ShiftModifier:
+            modifiers = 'Shift'
+        elif self.modifiers == QtCore.Qt.ControlModifier:
+            modifiers = 'Control'
+
+        if modifiers == self.app.defaults["global_mselect_key"]:
+            for storage in self.storage_dict:
+                for obj in self.storage_dict[storage].get_objects():
+                    if (sel_type is True and poly_selection.contains(obj.geo)) or \
+                            (sel_type is False and poly_selection.intersects(obj.geo)):
+
+                        if obj in self.selected:
+                            # remove the shape object from the selected shapes storage
+                            self.selected.remove(obj)
+                        else:
+                            # add the shape object to the selected shapes storage
+                            self.selected.append(obj)
+        else:
+            # clear the selection shapes storage
+            self.selected = []
+            # then add to the selection shapes storage the shapes that are included (touched) by the selection rectangle
+            for storage in self.storage_dict:
+                for obj in self.storage_dict[storage].get_objects():
+                    if (sel_type is True and poly_selection.contains(obj.geo)) or \
+                            (sel_type is False and poly_selection.intersects(obj.geo)):
+                        self.selected.append(obj)
+
+        try:
+            self.ui.tools_table_exc.cellPressed.disconnect()
+        except Exception:
+            pass
+
+        # first deselect all rows (tools) in the Tools Table
+        self.ui.tools_table_exc.clearSelection()
+        # and select the rows (tools) in the tool table according to the diameter(s) of the selected shape(s)
+        self.ui.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
+        for storage in self.storage_dict:
+            for shape_s in self.selected:
+                if shape_s in self.storage_dict[storage].get_objects():
+                    for key_tool_nr in self.tool2tooldia:
+                        if self.tool2tooldia[key_tool_nr] == storage:
+                            row_to_sel = key_tool_nr - 1
+                            # item = self.ui.tools_table_exc.item(row_to_sel, 1)
+                            # self.ui.tools_table_exc.setCurrentItem(item)
+                            # item.setSelected(True)
+
+                            # if the row to be selected is not already in the selected rows then select it
+                            # otherwise don't do it as it seems that we have a toggle effect
+                            if row_to_sel not in set(
+                                    index.row() for index in self.ui.tools_table_exc.selectedIndexes()):
+                                self.ui.tools_table_exc.selectRow(row_to_sel)
+                            self.last_tool_selected = int(key_tool_nr)
+
+        self.ui.tools_table_exc.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+
+        self.ui.tools_table_exc.cellPressed.connect(self.on_row_selected)
+        self.replot()
+
+    def update_utility_geometry(self, data):
+        # ### Utility geometry (animated) ###
+        geo = self.active_tool.utility_geometry(data=data)
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            # Remove any previous utility shape
+            self.tool_shape.clear(update=True)
+            self.draw_utility_geometry(geo=geo)
+
+    def on_canvas_key_release(self, event):
+        self.key = None
+
+    def draw_utility_geometry(self, geo):
+        # Add the new utility shape
+        try:
+            # this case is for the Font Parse
+            for el in list(geo.geo):
+                if type(el) == MultiPolygon:
+                    for poly in el:
+                        self.tool_shape.add(
+                            shape=poly,
+                            color=(self.app.defaults["global_draw_color"] + '80'),
+                            update=False,
+                            layer=0,
+                            tolerance=None
+                        )
+                elif type(el) == MultiLineString:
+                    for linestring in el:
+                        self.tool_shape.add(
+                            shape=linestring,
+                            color=(self.app.defaults["global_draw_color"] + '80'),
+                            update=False,
+                            layer=0,
+                            tolerance=None
+                        )
+                else:
+                    self.tool_shape.add(
+                        shape=el,
+                        color=(self.app.defaults["global_draw_color"] + '80'),
+                        update=False,
+                        layer=0,
+                        tolerance=None
+                    )
+        except TypeError:
+            self.tool_shape.add(
+                shape=geo.geo, color=(self.app.defaults["global_draw_color"] + '80'),
+                update=False, layer=0, tolerance=None)
+        self.tool_shape.redraw()
+
+    def replot(self):
+        self.plot_all()
+
+    def plot_all(self):
+        """
+        Plots all shapes in the editor.
+
+        :return:    None
+        :rtype:     None
+        """
+
+        self.shapes.clear(update=True)
+
+        for storage in self.storage_dict:
+            for shape_plus in self.storage_dict[storage].get_objects():
+                if shape_plus.geo is None:
+                    continue
+
+                if shape_plus in self.selected:
+                    self.plot_shape(geometry=shape_plus.geo, color=self.app.defaults['global_sel_draw_color'] + 'FF',
+                                    linewidth=2)
+                    continue
+                self.plot_shape(geometry=shape_plus.geo, color=self.app.defaults['global_draw_color'] + 'FF')
+
+        for shape_form in self.utility:
+            self.plot_shape(geometry=shape_form.geo, linewidth=1)
+            continue
+
+        self.shapes.redraw()
+
+    def plot_shape(self, geometry=None, color='0x000000FF', linewidth=1):
+        """
+        Plots a geometric object or list of objects without rendering. Plotted objects
+        are returned as a list. This allows for efficient/animated rendering.
+
+        :param geometry:    Geometry to be plotted (Any Shapely.geom kind or list of such)
+        :param color:       Shape color
+        :param linewidth:   Width of lines in # of pixels.
+        :return:            List of plotted elements.
+        """
+        plot_elements = []
+
+        if geometry is None:
+            geometry = self.active_tool.geometry
+
+        try:
+            for geo in geometry:
+                plot_elements += self.plot_shape(geometry=geo, color=color, linewidth=linewidth)
+
+        # ## Non-iterable
+        except TypeError:
+            # ## DrawToolShape
+            if isinstance(geometry, DrawToolShape):
+                plot_elements += self.plot_shape(geometry=geometry.geo, color=color, linewidth=linewidth)
+
+            # ## Polygon: Descend into exterior and each interior.
+            if type(geometry) == Polygon:
+                plot_elements += self.plot_shape(geometry=geometry.exterior, color=color, linewidth=linewidth)
+                plot_elements += self.plot_shape(geometry=geometry.interiors, color=color, linewidth=linewidth)
+
+            if type(geometry) == LineString or type(geometry) == LinearRing:
+                plot_elements.append(self.shapes.add(shape=geometry, color=color, layer=0, tolerance=self.tolerance))
+
+            if type(geometry) == Point:
+                pass
+
+        return plot_elements
+
+    def on_shape_complete(self):
+        # Add shape
+        self.add_shape(self.active_tool.geometry)
+
+        # Remove any utility shapes
+        self.delete_utility_geometry()
+        self.tool_shape.clear(update=True)
+
+        # Replot and reset tool.
+        self.replot()
+        # self.active_tool = type(self.active_tool)(self)
+
+    def get_selected(self):
+        """
+        Returns list of shapes that are selected in the editor.
+
+        :return: List of shapes.
+        """
+        return self.selected
+
+    def delete_selected(self):
+        temp_ref = [s for s in self.selected]
+        for shape_sel in temp_ref:
+            self.delete_shape(shape_sel)
+
+        self.selected = []
+        self.build_ui()
+        self.app.inform.emit('[success] %s' % _("Done."))
+
+    def delete_shape(self, del_shape):
+        self.is_modified = True
+
+        if del_shape in self.utility:
+            self.utility.remove(del_shape)
+            return
+
+        for storage in self.storage_dict:
+            # try:
+            #     self.storage_dict[storage].remove(shape)
+            # except:
+            #     pass
+            if del_shape in self.storage_dict[storage].get_objects():
+                if isinstance(del_shape.geo, MultiLineString):
+                    self.storage_dict[storage].remove(del_shape)
+                    # a hack to make the tool_table display less drills per diameter
+                    # self.points_edit it's only useful first time when we load the data into the storage
+                    # but is still used as referecen when building tool_table in self.build_ui()
+                    # the number of drills displayed in column 2 is just a len(self.points_edit) therefore
+                    # deleting self.points_edit elements (doesn't matter who but just the number)
+                    # solved the display issue.
+                    del self.points_edit[storage][0]
+                else:
+                    self.storage_dict[storage].remove(del_shape)
+                    del self.slot_points_edit[storage][0]
+
+        if del_shape in self.selected:
+            self.selected.remove(del_shape)
+
+    def delete_utility_geometry(self):
+        for_deletion = [util_shape for util_shape in self.utility]
+        for util_shape in for_deletion:
+            self.delete_shape(util_shape)
+
+        self.tool_shape.clear(update=True)
+        self.tool_shape.redraw()
+
+    def on_delete_btn(self):
+        self.delete_selected()
+        self.replot()
+
+    def select_tool(self, toolname):
+        """
+        Selects a drawing tool. Impacts the object and appGUI.
+
+        :param toolname:    Name of the tool.
+        :return:            None
+        """
+        self.tools_exc[toolname]["button"].setChecked(True)
+        self.on_tool_select(toolname)
+
+    def set_selected(self, sel_shape):
+
+        # Remove and add to the end.
+        if sel_shape in self.selected:
+            self.selected.remove(sel_shape)
+
+        self.selected.append(sel_shape)
+
+    def set_unselected(self, unsel_shape):
+        if unsel_shape in self.selected:
+            self.selected.remove(unsel_shape)
+
+    def on_array_type_radio(self, val):
+        if val == 'linear':
+            self.ui.array_circular_frame.hide()
+            self.ui.array_linear_frame.show()
+        else:
+            self.delete_utility_geometry()
+            self.ui.array_circular_frame.show()
+            self.ui.array_linear_frame.hide()
+            self.app.inform.emit(_("Click on the circular array Center position"))
+
+    def on_slot_array_type_radio(self, val):
+        if val == 'linear':
+            self.ui.slot_array_circular_frame.hide()
+            self.ui.slot_array_linear_frame.show()
+        else:
+            self.delete_utility_geometry()
+            self.ui.slot_array_circular_frame.show()
+            self.ui.slot_array_linear_frame.hide()
+            self.app.inform.emit(_("Click on the circular array Center position"))
+
+    def on_linear_angle_radio(self):
+        val = self.ui.drill_axis_radio.get_value()
+        if val == 'A':
+            self.ui.linear_angle_spinner.show()
+            self.ui.linear_angle_label.show()
+        else:
+            self.ui.linear_angle_spinner.hide()
+            self.ui.linear_angle_label.hide()
+
+    def on_slot_array_linear_angle_radio(self):
+        val = self.ui.slot_array_axis_radio.get_value()
+        if val == 'A':
+            self.ui.slot_array_linear_angle_spinner.show()
+            self.ui.slot_array_linear_angle_label.show()
+        else:
+            self.ui.slot_array_linear_angle_spinner.hide()
+            self.ui.slot_array_linear_angle_label.hide()
+
+    def on_slot_angle_radio(self):
+        val = self.ui.slot_axis_radio.get_value()
+        if val == 'A':
+            self.ui.slot_angle_spinner.show()
+            self.ui.slot_angle_label.show()
+        else:
+            self.ui.slot_angle_spinner.hide()
+            self.ui.slot_angle_label.hide()
+
+    def exc_add_drill(self):
+        self.select_tool('drill_add')
+        return
+
+    def exc_add_drill_array(self):
+        self.select_tool('drill_array')
+        return
+
+    def exc_add_slot(self):
+        self.select_tool('slot_add')
+        return
+
+    def exc_add_slot_array(self):
+        self.select_tool('slot_array')
+        return
+
+    def exc_resize_drills(self):
+        self.select_tool('drill_resize')
+        return
+
+    def exc_copy_drills(self):
+        self.select_tool('drill_copy')
+        return
+
+    def exc_move_drills(self):
+        self.select_tool('drill_move')
+        return
+
+    def on_slots_conversion(self):
+        # selected rows
+        selected_rows = set()
+        for it in self.ui.tools_table_exc.selectedItems():
+            selected_rows.add(it.row())
+
+        # convert a Polygon (slot) to a MultiLineString (drill)
+        def convert_slot2drill(geo_elem, tool_dia):
+            point = geo_elem.centroid
+            start_hor_line = ((point.x - (tool_dia / 2)), point.y)
+            stop_hor_line = ((point.x + (tool_dia / 2)), point.y)
+            start_vert_line = (point.x, (point.y - (tool_dia / 2)))
+            stop_vert_line = (point.x, (point.y + (tool_dia / 2)))
+            return MultiLineString([(start_hor_line, stop_hor_line), (start_vert_line, stop_vert_line)])
+
+        # temporary new storage: a dist with keys the tool diameter and values Rtree storage
+        new_storage_dict = {}
+
+        for row in selected_rows:
+            table_tooldia = self.dec_format(float(self.ui.tools_table_exc.item(row, 1).text()))
+            for dict_dia, geo_dict in self.storage_dict.items():
+                if self.dec_format(float(dict_dia)) == table_tooldia:
+                    storage_elem = AppGeoEditor.make_storage()
+                    for shape in geo_dict.get_objects():
+                        if isinstance(shape.geo, MultiLineString):
+                            # it's a drill just add it as it is to storage
+                            self.add_exc_shape(shape, storage_elem)
+                        if isinstance(shape.geo, Polygon):
+                            # it's a slot, convert drill to slot and then add it to storage
+                            new_shape = convert_slot2drill(shape.geo, table_tooldia)
+                            self.add_exc_shape(DrawToolShape(new_shape), storage_elem)
+
+                    new_storage_dict[table_tooldia] = storage_elem
+
+        self.storage_dict.update(new_storage_dict)
+        self.replot()
+
+
+class AppExcEditorUI:
+    def __init__(self, app):
+        self.app = app
+
+        # Number of decimals used by tools in this class
+        self.decimals = self.app.decimals
+
+        # ## Current application units in Upper Case
+        self.units = self.app.defaults['units'].upper()
+
+        self.exc_edit_widget = QtWidgets.QWidget()
+        # ## Box for custom widgets
+        # This gets populated in offspring implementations.
+        layout = QtWidgets.QVBoxLayout()
+        self.exc_edit_widget.setLayout(layout)
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.drills_frame = QtWidgets.QFrame()
+        self.drills_frame.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self.drills_frame)
+
+        # #############################################################################################################
+        # ######################## MAIN Grid ##########################################################################
+        # #############################################################################################################
+        self.ui_vertical_lay = QtWidgets.QVBoxLayout()
+        self.ui_vertical_lay.setContentsMargins(0, 0, 0, 0)
+        self.drills_frame.setLayout(self.ui_vertical_lay)
+
+        # Page Title box (spacing between children)
+        self.title_box = QtWidgets.QHBoxLayout()
+        self.ui_vertical_lay.addLayout(self.title_box)
+
+        # Page Title
+        pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png')
+        self.icon = FCLabel()
+        self.icon.setPixmap(pixmap)
+
+        self.title_label = FCLabel("<font size=5><b>%s</b></font>" % _('Excellon Editor'))
+        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        self.title_box.addWidget(self.icon, stretch=0)
+        self.title_box.addWidget(self.title_label, stretch=1)
+
+        # Object name box
+        self.name_box = QtWidgets.QHBoxLayout()
+        self.ui_vertical_lay.addLayout(self.name_box)
+
+        # Object Name
+        name_label = FCLabel(_("Name:"))
+        self.name_entry = FCEntry()
+
+        self.name_box.addWidget(name_label)
+        self.name_box.addWidget(self.name_entry)
+
+        # Tools Drills Table Title
+        self.tools_table_label = FCLabel("<b>%s</b>" % _('Tools Table'))
+        self.tools_table_label.setToolTip(
+            _("Tools in this Excellon object\n"
+              "when are used for drilling.")
+        )
+        self.ui_vertical_lay.addWidget(self.tools_table_label)
+
+        # #############################################################################################################
+        # ########################################## Drills TABLE #####################################################
+        # #############################################################################################################
+        self.tools_table_exc = FCTable()
+        self.tools_table_exc.setColumnCount(4)
+        self.tools_table_exc.setHorizontalHeaderLabels(['#', _('Diameter'), 'D', 'S'])
+        self.tools_table_exc.setSortingEnabled(False)
+        self.tools_table_exc.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        self.ui_vertical_lay.addWidget(self.tools_table_exc)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.ui_vertical_lay.addWidget(separator_line)
+
+        self.convert_slots_btn = FCButton('%s' % _("Convert Slots"))
+        self.convert_slots_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/convert32.png'))
+
+        self.convert_slots_btn.setToolTip(
+            _("Convert the slots in the selected tools to drills.")
+        )
+        self.ui_vertical_lay.addWidget(self.convert_slots_btn)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.ui_vertical_lay.addWidget(separator_line)
+
+        # Add a new Tool
+        self.addtool_label = FCLabel('<b>%s</b>' % _('Add/Delete Tool'))
+        self.addtool_label.setToolTip(
+            _("Add/Delete a tool to the tool list\n"
+              "for this Excellon object.")
+        )
+        self.ui_vertical_lay.addWidget(self.addtool_label)
+
+        # #############################################################################################################
+        # ######################## ADD New Tool Grid ##################################################################
+        # #############################################################################################################
+        grid1 = QtWidgets.QGridLayout()
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+        self.ui_vertical_lay.addLayout(grid1)
+
+        # Tool Diameter Label
+        addtool_entry_lbl = FCLabel('%s:' % _('Tool Dia'))
+        addtool_entry_lbl.setToolTip(
+            _("Diameter for the new tool")
+        )
+
+        hlay = QtWidgets.QHBoxLayout()
+        # Tool Diameter Entry
+        self.addtool_entry = FCDoubleSpinner(policy=False)
+        self.addtool_entry.set_precision(self.decimals)
+        self.addtool_entry.set_range(0.0000, 10000.0000)
+
+        hlay.addWidget(self.addtool_entry)
+
+        # Tool Diameter Button
+        self.addtool_btn = FCButton(_('Add'))
+        self.addtool_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png'))
+        self.addtool_btn.setToolTip(
+            _("Add a new tool to the tool list\n"
+              "with the diameter specified above.")
+        )
+        hlay.addWidget(self.addtool_btn)
+
+        grid1.addWidget(addtool_entry_lbl, 0, 0)
+        grid1.addLayout(hlay, 0, 1)
+
+        # Delete Tool
+        self.deltool_btn = FCButton(_('Delete Tool'))
+        self.deltool_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/trash32.png'))
+        self.deltool_btn.setToolTip(
+            _("Delete a tool in the tool list\n"
+              "by selecting a row in the tool table.")
+        )
+        grid1.addWidget(self.deltool_btn, 2, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 4, 0, 1, 2)
+
+        # #############################################################################################################
+        # ############################## Resize Tool Grid #############################################################
+        # #############################################################################################################
+        # add a frame and inside add a grid box layout. Inside this layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.resize_frame = QtWidgets.QFrame()
+        self.resize_frame.setContentsMargins(0, 0, 0, 0)
+        self.ui_vertical_lay.addWidget(self.resize_frame)
+
+        self.resize_grid = QtWidgets.QGridLayout()
+        self.resize_grid.setColumnStretch(0, 0)
+        self.resize_grid.setColumnStretch(1, 1)
+        self.resize_grid.setContentsMargins(0, 0, 0, 0)
+        self.resize_frame.setLayout(self.resize_grid)
+
+        self.drillresize_label = FCLabel('<b>%s</b>' % _("Resize Tool"))
+        self.drillresize_label.setToolTip(
+            _("Resize a drill or a selection of drills.")
+        )
+        self.resize_grid.addWidget(self.drillresize_label, 0, 0, 1, 2)
+
+        # Resize Diameter
+        res_entry_lbl = FCLabel('%s:' % _('Resize Dia'))
+        res_entry_lbl.setToolTip(
+            _("Diameter to resize to.")
+        )
+
+        hlay2 = QtWidgets.QHBoxLayout()
+        self.resdrill_entry = FCDoubleSpinner(policy=False)
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)
+        self.resdrill_entry.setSizePolicy(sizePolicy)
+        self.resdrill_entry.set_precision(self.decimals)
+        self.resdrill_entry.set_range(0.0000, 10000.0000)
+
+        hlay2.addWidget(self.resdrill_entry)
+
+        # Resize Button
+        self.resize_btn = FCButton(_('Resize'))
+        self.resize_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/resize16.png'))
+        self.resize_btn.setToolTip(
+            _("Resize drill(s)")
+        )
+        hlay2.addWidget(self.resize_btn)
+
+        self.resize_grid.addWidget(res_entry_lbl, 2, 0)
+        self.resize_grid.addLayout(hlay2, 2, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.resize_grid.addWidget(separator_line, 6, 0, 1, 2)
+
+        self.resize_frame.hide()
+
+        # #############################################################################################################
+        # ################################## Add DRILL Array ##########################################################
+        # #############################################################################################################
+        # add a frame and inside add a grid box layout. Inside this grid layout I add
+        # all the add drill array  widgets
+        # this way I can hide/show the frame
+        self.array_frame = QtWidgets.QFrame()
+        self.array_frame.setContentsMargins(0, 0, 0, 0)
+        self.ui_vertical_lay.addWidget(self.array_frame)
+
+        self.array_grid = QtWidgets.QGridLayout()
+        self.array_grid.setColumnStretch(0, 0)
+        self.array_grid.setColumnStretch(1, 1)
+        self.array_grid.setContentsMargins(0, 0, 0, 0)
+        self.array_frame.setLayout(self.array_grid)
+
+        # Type of Drill Array
+        self.drill_array_label = FCLabel('<b>%s</b>' % _("Add Drill Array"))
+        self.drill_array_label.setToolTip(
+            _("Add an array of drills (linear or circular array)")
+        )
+        
+        self.array_grid.addWidget(self.drill_array_label, 0, 0, 1, 2)
+
+        # Array Type
+        array_type_lbl = FCLabel('%s:' % _("Type"))
+        array_type_lbl.setToolTip(
+            _("Select the type of drills array to create.\n"
+              "It can be Linear X(Y) or Circular")
+        )
+
+        self.array_type_radio = RadioSet([{'label': _('Linear'), 'value': 'linear'},
+                                          {'label': _('Circular'), 'value': 'circular'}])
+
+        self.array_grid.addWidget(array_type_lbl, 2, 0)
+        self.array_grid.addWidget(self.array_type_radio, 2, 1)
+
+        # Set the number of drill holes in the drill array
+        self.drill_array_size_label = FCLabel('%s:' % _('Number'))
+        self.drill_array_size_label.setToolTip(_("Specify how many drills to be in the array."))
+
+        self.drill_array_size_entry = FCSpinner(policy=False)
+        self.drill_array_size_entry.set_range(1, 10000)
+
+        self.array_grid.addWidget(self.drill_array_size_label, 4, 0)
+        self.array_grid.addWidget(self.drill_array_size_entry, 4, 1)
+
+        # #############################################################################################################
+        # ###################### LINEAR Drill Array ###################################################################
+        # #############################################################################################################
+        self.array_linear_frame = QtWidgets.QFrame()
+        self.array_linear_frame.setContentsMargins(0, 0, 0, 0)
+        self.array_grid.addWidget(self.array_linear_frame, 6, 0, 1, 2)
+        self.lin_grid = QtWidgets.QGridLayout()
+        self.lin_grid.setColumnStretch(0, 0)
+        self.lin_grid.setColumnStretch(1, 1)
+        self.lin_grid.setContentsMargins(0, 0, 0, 0)
+        self.array_linear_frame.setLayout(self.lin_grid)
+
+        # Linear Drill Array direction
+        self.drill_axis_label = FCLabel('%s:' % _('Direction'))
+        self.drill_axis_label.setToolTip(
+            _("Direction on which the linear array is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the array inclination")
+        )
+
+        self.drill_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                          {'label': _('Y'), 'value': 'Y'},
+                                          {'label': _('Angle'), 'value': 'A'}])
+
+        self.lin_grid.addWidget(self.drill_axis_label, 0, 0)
+        self.lin_grid.addWidget(self.drill_axis_radio, 0, 1)
+
+        # Linear Drill Array pitch distance
+        self.drill_pitch_label = FCLabel('%s:' % _('Pitch'))
+        self.drill_pitch_label.setToolTip(
+            _("Pitch = Distance between elements of the array.")
+        )
+
+        self.drill_pitch_entry = FCDoubleSpinner(policy=False)
+        self.drill_pitch_entry.set_precision(self.decimals)
+        self.drill_pitch_entry.set_range(0.0000, 10000.0000)
+
+        self.lin_grid.addWidget(self.drill_pitch_label, 2, 0)
+        self.lin_grid.addWidget(self.drill_pitch_entry, 2, 1)
+
+        # Linear Drill Array angle
+        self.linear_angle_label = FCLabel('%s:' % _('Angle'))
+        self.linear_angle_label.setToolTip(
+            _("Angle at which the linear array is placed.\n"
+              "The precision is of max 2 decimals.\n"
+              "Min value is: -360.00 degrees.\n"
+              "Max value is: 360.00 degrees.")
+        )
+
+        self.linear_angle_spinner = FCDoubleSpinner(policy=False)
+        self.linear_angle_spinner.set_precision(self.decimals)
+        self.linear_angle_spinner.setSingleStep(1.0)
+        self.linear_angle_spinner.setRange(-360.00, 360.00)
+
+        self.lin_grid.addWidget(self.linear_angle_label, 4, 0)
+        self.lin_grid.addWidget(self.linear_angle_spinner, 4, 1)
+
+        # #############################################################################################################
+        # ###################### CIRCULAR Drill Array #################################################################
+        # #############################################################################################################
+        self.array_circular_frame = QtWidgets.QFrame()
+        self.array_circular_frame.setContentsMargins(0, 0, 0, 0)
+        self.array_grid.addWidget(self.array_circular_frame, 8, 0, 1, 2)
+
+        self.circ_grid = QtWidgets.QGridLayout()
+        self.circ_grid.setColumnStretch(0, 0)
+        self.circ_grid.setColumnStretch(1, 1)
+        self.circ_grid.setContentsMargins(0, 0, 0, 0)
+        self.array_circular_frame.setLayout(self.circ_grid)
+
+        # Array Direction
+        self.drill_array_dir_lbl = FCLabel('%s:' % _('Direction'))
+        self.drill_array_dir_lbl.setToolTip(_("Direction for circular array.\n"
+                                                "Can be CW = clockwise or CCW = counter clockwise."))
+
+        self.drill_array_dir_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                               {'label': _('CCW'), 'value': 'CCW'}])
+
+        self.circ_grid.addWidget(self.drill_array_dir_lbl, 0, 0)
+        self.circ_grid.addWidget(self.drill_array_dir_radio, 0, 1)
+
+        # Array Angle
+        self.drill_array_angle_lbl = FCLabel('%s:' % _('Angle'))
+        self.drill_array_angle_lbl.setToolTip(_("Angle at which each element in circular array is placed."))
+
+        self.drill_angle_entry = FCDoubleSpinner(policy=False)
+        self.drill_angle_entry.set_precision(self.decimals)
+        self.drill_angle_entry.setSingleStep(1.0)
+        self.drill_angle_entry.setRange(-360.00, 360.00)
+
+        self.circ_grid.addWidget(self.drill_array_angle_lbl, 2, 0)
+        self.circ_grid.addWidget(self.drill_angle_entry, 2, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.array_grid.addWidget(separator_line, 10, 0, 1, 2)
+
+        # #############################################################################################################
+        # ################################### ADDING SLOTS ############################################################
+        # #############################################################################################################
+        # add a frame and inside add a grid box layout. Inside this grid layout I add
+        # all the add slot  widgets
+        # this way I can hide/show the frame
+        self.slot_frame = QtWidgets.QFrame()
+        self.slot_frame.setContentsMargins(0, 0, 0, 0)
+        self.ui_vertical_lay.addWidget(self.slot_frame)
+
+        self.slot_grid = QtWidgets.QGridLayout()
+        self.slot_grid.setColumnStretch(0, 0)
+        self.slot_grid.setColumnStretch(1, 1)
+        self.slot_grid.setContentsMargins(0, 0, 0, 0)
+        self.slot_frame.setLayout(self.slot_grid)
+
+        # Slot Tile Label
+        self.slot_label = FCLabel('<b>%s</b>' % _("Slot Parameters"))
+        self.slot_label.setToolTip(
+            _("Parameters for adding a slot (hole with oval shape)\n"
+              "either single or as an part of an array.")
+        )
+        self.slot_grid.addWidget(self.slot_label, 0, 0, 1, 2)
+
+        # Slot length
+        self.slot_length_label = FCLabel('%s:' % _('Length'))
+        self.slot_length_label.setToolTip(
+            _("Length. The length of the slot.")
+        )
+
+        self.slot_length_entry = FCDoubleSpinner(policy=False)
+        self.slot_length_entry.set_precision(self.decimals)
+        self.slot_length_entry.setSingleStep(0.1)
+        self.slot_length_entry.setRange(0.0000, 10000.0000)
+
+        self.slot_grid.addWidget(self.slot_length_label, 2, 0)
+        self.slot_grid.addWidget(self.slot_length_entry, 2, 1)
+
+        # Slot direction
+        self.slot_axis_label = FCLabel('%s:' % _('Direction'))
+        self.slot_axis_label.setToolTip(
+            _("Direction on which the slot is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the slot inclination")
+        )
+
+        self.slot_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                         {'label': _('Y'), 'value': 'Y'},
+                                         {'label': _('Angle'), 'value': 'A'}])
+
+        self.slot_grid.addWidget(self.slot_axis_label, 4, 0)
+        self.slot_grid.addWidget(self.slot_axis_radio, 4, 1)
+
+        # Slot custom angle
+        self.slot_angle_label = FCLabel('%s:' % _('Angle'))
+        self.slot_angle_label.setToolTip(
+            _("Angle at which the slot is placed.\n"
+              "The precision is of max 2 decimals.\n"
+              "Min value is: -360.00 degrees.\n"
+              "Max value is: 360.00 degrees.")
+        )
+
+        self.slot_angle_spinner = FCDoubleSpinner(policy=False)
+        self.slot_angle_spinner.set_precision(self.decimals)
+        self.slot_angle_spinner.setWrapping(True)
+        self.slot_angle_spinner.setRange(-360.00, 360.00)
+        self.slot_angle_spinner.setSingleStep(1.0)
+
+        self.slot_grid.addWidget(self.slot_angle_label, 6, 0)
+        self.slot_grid.addWidget(self.slot_angle_spinner, 6, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.slot_grid.addWidget(separator_line, 8, 0, 1, 2)
+
+        # #############################################################################################################
+        # ##################################### ADDING SLOT ARRAY  ####################################################
+        # #############################################################################################################
+        self.slot_array_frame = QtWidgets.QFrame()
+        self.slot_array_frame.setContentsMargins(0, 0, 0, 0)
+        self.ui_vertical_lay.addWidget(self.slot_array_frame)
+
+        self.slot_array_grid = QtWidgets.QGridLayout()
+        self.slot_array_grid.setColumnStretch(0, 0)
+        self.slot_array_grid.setColumnStretch(1, 1)
+        self.slot_array_grid.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_frame.setLayout(self.slot_array_grid)
+
+        # Slot Array Title
+        self.slot_array_label = FCLabel('<b>%s</b>' % _("Slot Array Parameters"))
+        self.slot_array_label.setToolTip(
+            _("Parameters for the array of slots (linear or circular array)")
+        )
+
+        self.slot_array_grid.addWidget(self.slot_array_label, 0, 0, 1, 2)
+
+        # Array Type
+        array_type_lbl = FCLabel('%s:' % _("Type"))
+        array_type_lbl.setToolTip(
+            _("Select the type of slot array to create.\n"
+              "It can be Linear X(Y) or Circular")
+        )
+
+        self.slot_array_type_radio = RadioSet([{'label': _('Linear'), 'value': 'linear'},
+                                               {'label': _('Circular'), 'value': 'circular'}])
+
+        self.slot_array_grid.addWidget(array_type_lbl, 2, 0)
+        self.slot_array_grid.addWidget(self.slot_array_type_radio, 2, 1)
+
+        # Set the number of slot holes in the slot array
+        self.slot_array_size_label = FCLabel('%s:' % _('Number'))
+        self.slot_array_size_label.setToolTip(_("Specify how many slots to be in the array."))
+
+        self.slot_array_size_entry = FCSpinner(policy=False)
+        self.slot_array_size_entry.set_range(0, 10000)
+
+        self.slot_array_grid.addWidget(self.slot_array_size_label, 4, 0)
+        self.slot_array_grid.addWidget(self.slot_array_size_entry, 4, 1)
+
+        # #############################################################################################################
+        # ##################################### Linear SLOT ARRAY  ####################################################
+        # #############################################################################################################
+        self.slot_array_linear_frame = QtWidgets.QFrame()
+        self.slot_array_linear_frame.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_grid.addWidget(self.slot_array_linear_frame, 6, 0, 1, 2)
+
+        self.slot_array_lin_grid = QtWidgets.QGridLayout()
+        self.slot_array_lin_grid.setColumnStretch(0, 0)
+        self.slot_array_lin_grid.setColumnStretch(1, 1)
+        self.slot_array_lin_grid.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_linear_frame.setLayout(self.slot_array_lin_grid)
+
+        # Linear Slot Array direction
+        self.slot_array_axis_label = FCLabel('%s:' % _('Direction'))
+        self.slot_array_axis_label.setToolTip(
+            _("Direction on which the linear array is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the array inclination")
+        )
+
+        self.slot_array_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                               {'label': _('Y'), 'value': 'Y'},
+                                               {'label': _('Angle'), 'value': 'A'}])
+
+        self.slot_array_lin_grid.addWidget(self.slot_array_axis_label, 0, 0)
+        self.slot_array_lin_grid.addWidget(self.slot_array_axis_radio, 0, 1)
+
+        # Linear Slot Array pitch distance
+        self.slot_array_pitch_label = FCLabel('%s:' % _('Pitch'))
+        self.slot_array_pitch_label.setToolTip(
+            _("Pitch = Distance between elements of the array.")
+        )
+
+        self.slot_array_pitch_entry = FCDoubleSpinner(policy=False)
+        self.slot_array_pitch_entry.set_precision(self.decimals)
+        self.slot_array_pitch_entry.setSingleStep(0.1)
+        self.slot_array_pitch_entry.setRange(0.0000, 10000.0000)
+
+        self.slot_array_lin_grid.addWidget(self.slot_array_pitch_label, 2, 0)
+        self.slot_array_lin_grid.addWidget(self.slot_array_pitch_entry, 2, 1)
+
+        # Linear Slot Array angle
+        self.slot_array_linear_angle_label = FCLabel('%s:' % _('Angle'))
+        self.slot_array_linear_angle_label.setToolTip(
+            _("Angle at which the linear array is placed.\n"
+              "The precision is of max 2 decimals.\n"
+              "Min value is: -360.00 degrees.\n"
+              "Max value is: 360.00 degrees.")
+        )
+
+        self.slot_array_linear_angle_spinner = FCDoubleSpinner(policy=False)
+        self.slot_array_linear_angle_spinner.set_precision(self.decimals)
+        self.slot_array_linear_angle_spinner.setSingleStep(1.0)
+        self.slot_array_linear_angle_spinner.setRange(-360.00, 360.00)
+
+        self.slot_array_lin_grid.addWidget(self.slot_array_linear_angle_label, 4, 0)
+        self.slot_array_lin_grid.addWidget(self.slot_array_linear_angle_spinner, 4, 1)
+
+        # #############################################################################################################
+        # ##################################### Circular SLOT ARRAY  ##################################################
+        # #############################################################################################################
+        self.slot_array_circular_frame = QtWidgets.QFrame()
+        self.slot_array_circular_frame.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_grid.addWidget(self.slot_array_circular_frame, 8, 0, 1, 2)
+
+        self.slot_array_circ_grid = QtWidgets.QGridLayout()
+        self.slot_array_circ_grid.setColumnStretch(0, 0)
+        self.slot_array_circ_grid.setColumnStretch(1, 1)
+        self.slot_array_circ_grid.setContentsMargins(0, 0, 0, 0)
+        self.slot_array_circular_frame.setLayout(self.slot_array_circ_grid)
+
+        # Slot Circular Array Direction
+        self.slot_array_direction_label = FCLabel('%s:' % _('Direction'))
+        self.slot_array_direction_label.setToolTip(_("Direction for circular array.\n"
+                                                     "Can be CW = clockwise or CCW = counter clockwise."))
+
+        self.slot_array_direction_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                                    {'label': _('CCW'), 'value': 'CCW'}])
+
+        self.slot_array_circ_grid.addWidget(self.slot_array_direction_label, 0, 0)
+        self.slot_array_circ_grid.addWidget(self.slot_array_direction_radio, 0, 1)
+
+        # Slot Circular Array Angle
+        self.slot_array_angle_label = FCLabel('%s:' % _('Angle'))
+        self.slot_array_angle_label.setToolTip(_("Angle at which each element in circular array is placed."))
+
+        self.slot_array_angle_entry = FCDoubleSpinner(policy=False)
+        self.slot_array_angle_entry.set_precision(self.decimals)
+        self.slot_array_angle_entry.setSingleStep(1)
+        self.slot_array_angle_entry.setRange(-360.00, 360.00)
+
+        self.slot_array_circ_grid.addWidget(self.slot_array_angle_label, 2, 0)
+        self.slot_array_circ_grid.addWidget(self.slot_array_angle_entry, 2, 1)
+
+        self.ui_vertical_lay.addStretch()
+        layout.addStretch(1)
+
+        # Editor
+        self.exit_editor_button = FCButton(_('Exit Editor'))
+        self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
+        self.exit_editor_button.setToolTip(
+            _("Exit from Editor.")
+        )
+        self.exit_editor_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        layout.addWidget(self.exit_editor_button)
+
+        # #############################################################################################################
+        # ###################### INIT Excellon Editor UI ##############################################################
+        # #############################################################################################################
+        self.linear_angle_spinner.hide()
+        self.linear_angle_label.hide()
+        self.array_linear_frame.hide()
+        self.array_circular_frame.hide()
+        self.array_frame.hide()
+
+        self.slot_frame.hide()
+        self.slot_array_linear_angle_spinner.hide()
+        self.slot_array_linear_angle_label.hide()
+        self.slot_array_frame.hide()
+
+        # ############################ FINSIHED GUI ###################################
+        # #############################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+
+def get_shapely_list_bounds(geometry_list):
+    xmin = np.Inf
+    ymin = np.Inf
+    xmax = -np.Inf
+    ymax = -np.Inf
+
+    for gs in geometry_list:
+        try:
+            gxmin, gymin, gxmax, gymax = gs.bounds
+            xmin = min([xmin, gxmin])
+            ymin = min([ymin, gymin])
+            xmax = max([xmax, gxmax])
+            ymax = max([ymax, gymax])
+        except Exception as e:
+            log.warning("DEVELOPMENT: Tried to get bounds of empty geometry. --> %s" % str(e))
+
+    return [xmin, ymin, xmax, ymax]
+
+# EOF

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 841 - 851
appEditors/AppGeoEditor.py


+ 6729 - 0
appEditors/AppGerberEditor.py

@@ -0,0 +1,6729 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 8/17/2019                                          #
+# MIT Licence                                              #
+# ##########################################################
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import Qt
+
+from shapely.geometry import LineString, LinearRing, MultiLineString, Point, Polygon, MultiPolygon, box
+from shapely.ops import unary_union
+import shapely.affinity as affinity
+
+from vispy.geometry import Rect
+
+from copy import copy, deepcopy
+import logging
+
+from camlib import distance, arc, three_point_circle
+from appGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, FCSpinner, RadioSet, EvalEntry2, \
+    FCInputDoubleSpinner, FCButton, OptionalInputSection, FCCheckBox, NumericalEvalTupleEntry, FCComboBox2, FCLabel
+from appTool import AppTool
+
+import numpy as np
+from numpy.linalg import norm as numpy_norm
+import math
+
+# from vispy.io import read_png
+# import pngcanvas
+import traceback
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class DrawToolShape(object):
+    """
+    Encapsulates "shapes" under a common class.
+    """
+
+    tolerance = None
+
+    @staticmethod
+    def get_pts(o):
+        """
+        Returns a list of all points in the object, where
+        the object can be a Polygon, Not a polygon, or a list
+        of such. Search is done recursively.
+
+        :param: geometric object
+        :return: List of points
+        :rtype: list
+        """
+        pts = []
+
+        # ## Iterable: descend into each item.
+        try:
+            for sub_o in o:
+                pts += DrawToolShape.get_pts(sub_o)
+
+        # Non-iterable
+        except TypeError:
+            if o is not None:
+                # DrawToolShape: descend into .geo.
+                if isinstance(o, DrawToolShape):
+                    pts += DrawToolShape.get_pts(o.geo)
+
+                # ## Descend into .exerior and .interiors
+                elif type(o) == Polygon:
+                    pts += DrawToolShape.get_pts(o.exterior)
+                    for i in o.interiors:
+                        pts += DrawToolShape.get_pts(i)
+                elif type(o) == MultiLineString:
+                    for line in o:
+                        pts += DrawToolShape.get_pts(line)
+                # ## Has .coords: list them.
+                else:
+                    if DrawToolShape.tolerance is not None:
+                        pts += list(o.simplify(DrawToolShape.tolerance).coords)
+                    else:
+                        pts += list(o.coords)
+            else:
+                return
+        return pts
+
+    def __init__(self, geo=None):
+
+        # Shapely type or list of such
+        self.geo = geo
+        self.utility = False
+
+
+class DrawToolUtilityShape(DrawToolShape):
+    """
+    Utility shapes are temporary geometry in the editor
+    to assist in the creation of shapes. For example it
+    will show the outline of a rectangle from the first
+    point to the current mouse pointer before the second
+    point is clicked and the final geometry is created.
+    """
+
+    def __init__(self, geo=None):
+        super(DrawToolUtilityShape, self).__init__(geo=geo)
+        self.utility = True
+
+
+class DrawTool(object):
+    """
+    Abstract Class representing a tool in the drawing
+    program. Can generate geometry, including temporary
+    utility geometry that is updated on user clicks
+    and mouse motion.
+    """
+
+    def __init__(self, draw_app):
+        self.draw_app = draw_app
+        self.complete = False
+        self.points = []
+        self.geometry = None  # DrawToolShape or None
+
+    def click(self, point):
+        """
+        :param point: [x, y] Coordinate pair.
+        """
+        return ""
+
+    def click_release(self, point):
+        """
+        :param point: [x, y] Coordinate pair.
+        """
+        return ""
+
+    def on_key(self, key):
+        # Jump to coords
+        if key == QtCore.Qt.Key_J or key == 'J':
+            self.draw_app.app.on_jump_to()
+
+    def utility_geometry(self, data=None):
+        return None
+
+    @staticmethod
+    def bounds(obj):
+        def bounds_rec(o):
+            if type(o) is list:
+                minx = np.Inf
+                miny = np.Inf
+                maxx = -np.Inf
+                maxy = -np.Inf
+
+                for k in o:
+                    try:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(k)
+                    except Exception as e:
+                        log.debug("camlib.Gerber.bounds() --> %s" % str(e))
+                        return
+
+                    minx = min(minx, minx_)
+                    miny = min(miny, miny_)
+                    maxx = max(maxx, maxx_)
+                    maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            else:
+                # it's a Shapely object, return it's bounds
+                if 'solid' in o.geo:
+                    return o.geo['solid'].bounds
+
+        return bounds_rec(obj)
+
+
+class ShapeToolEditorGrb(DrawTool):
+    """
+    Abstract class for tools that create a shape.
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = None
+
+    def make(self):
+        pass
+
+
+class PadEditorGrb(ShapeToolEditorGrb):
+    """
+    Resulting type: Polygon
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'pad'
+        self.draw_app = draw_app
+        self.dont_execute = False
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_circle.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        try:
+            self.radius = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size']) / 2
+        except KeyError:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                          _("You need to preselect a aperture in the Aperture Table that has a size."))
+            try:
+                QtGui.QGuiApplication.restoreOverrideCursor()
+            except Exception:
+                pass
+            self.dont_execute = True
+            self.draw_app.in_action = False
+            self.complete = True
+            self.draw_app.select_tool('select')
+            return
+
+        if self.radius == 0:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                          _("Aperture size is zero. It needs to be greater than zero."))
+            self.dont_execute = True
+            return
+        else:
+            self.dont_execute = False
+
+        self.storage_obj = self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['geometry']
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+        # if those cause KeyError exception it means that the aperture type is not 'R'. Only 'R' type has those keys
+        try:
+            self.half_width = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['width']) / 2
+        except KeyError:
+            pass
+        try:
+            self.half_height = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['height']) / 2
+        except KeyError:
+            pass
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo_shape=geo)
+
+        self.draw_app.app.inform.emit(_("Click to place ..."))
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+        self.start_msg = _("Click to place ...")
+
+    def click(self, point):
+        self.make()
+        return "Done."
+
+    def utility_geometry(self, data=None):
+        if self.dont_execute is True:
+            self.draw_app.select_tool('select')
+            return
+
+        self.points = data
+        geo_data = self.util_shape(data)
+        if geo_data:
+            return DrawToolUtilityShape(geo_data)
+        else:
+            return None
+
+    def util_shape(self, point):
+        # updating values here allows us to change the aperture on the fly, after the Tool has been started
+        self.storage_obj = self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['geometry']
+        self.radius = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size']) / 2
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+        # if those cause KeyError exception it means that the aperture type is not 'R'. Only 'R' type has those keys
+        try:
+            self.half_width = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['width']) / 2
+        except KeyError:
+            pass
+        try:
+            self.half_height = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['height']) / 2
+        except KeyError:
+            pass
+
+        if point[0] is None and point[1] is None:
+            point_x = self.draw_app.x
+            point_y = self.draw_app.y
+        else:
+            point_x = point[0]
+            point_y = point[1]
+
+        ap_type = self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['type']
+        if ap_type == 'C':
+            new_geo_el = {}
+
+            center = Point([point_x, point_y])
+            new_geo_el['solid'] = center.buffer(self.radius)
+            new_geo_el['follow'] = center
+            return new_geo_el
+        elif ap_type == 'R':
+            new_geo_el = {}
+
+            p1 = (point_x - self.half_width, point_y - self.half_height)
+            p2 = (point_x + self.half_width, point_y - self.half_height)
+            p3 = (point_x + self.half_width, point_y + self.half_height)
+            p4 = (point_x - self.half_width, point_y + self.half_height)
+            center = Point([point_x, point_y])
+            new_geo_el['solid'] = Polygon([p1, p2, p3, p4, p1])
+            new_geo_el['follow'] = center
+            return new_geo_el
+        elif ap_type == 'O':
+            geo = []
+            new_geo_el = {}
+
+            if self.half_height > self.half_width:
+                p1 = (point_x - self.half_width, point_y - self.half_height + self.half_width)
+                p2 = (point_x + self.half_width, point_y - self.half_height + self.half_width)
+                p3 = (point_x + self.half_width, point_y + self.half_height - self.half_width)
+                p4 = (point_x - self.half_width, point_y + self.half_height - self.half_width)
+
+                down_center = [point_x, point_y - self.half_height + self.half_width]
+                d_start_angle = np.pi
+                d_stop_angle = 0.0
+                down_arc = arc(down_center, self.half_width, d_start_angle, d_stop_angle, 'ccw', self.steps_per_circ)
+
+                up_center = [point_x, point_y + self.half_height - self.half_width]
+                u_start_angle = 0.0
+                u_stop_angle = np.pi
+                up_arc = arc(up_center, self.half_width, u_start_angle, u_stop_angle, 'ccw', self.steps_per_circ)
+
+                geo.append(p1)
+                for pt in down_arc:
+                    geo.append(pt)
+                geo.append(p2)
+                geo.append(p3)
+                for pt in up_arc:
+                    geo.append(pt)
+                geo.append(p4)
+                new_geo_el['solid'] = Polygon(geo)
+                center = Point([point_x, point_y])
+                new_geo_el['follow'] = center
+                return new_geo_el
+
+            else:
+                p1 = (point_x - self.half_width + self.half_height, point_y - self.half_height)
+                p2 = (point_x + self.half_width - self.half_height, point_y - self.half_height)
+                p3 = (point_x + self.half_width - self.half_height, point_y + self.half_height)
+                p4 = (point_x - self.half_width + self.half_height, point_y + self.half_height)
+
+                left_center = [point_x - self.half_width + self.half_height, point_y]
+                d_start_angle = np.pi / 2
+                d_stop_angle = 1.5 * np.pi
+                left_arc = arc(left_center, self.half_height, d_start_angle, d_stop_angle, 'ccw', self.steps_per_circ)
+
+                right_center = [point_x + self.half_width - self.half_height, point_y]
+                u_start_angle = 1.5 * np.pi
+                u_stop_angle = np.pi / 2
+                right_arc = arc(right_center, self.half_height, u_start_angle, u_stop_angle, 'ccw', self.steps_per_circ)
+
+                geo.append(p1)
+                geo.append(p2)
+                for pt in right_arc:
+                    geo.append(pt)
+                geo.append(p3)
+                geo.append(p4)
+                for pt in left_arc:
+                    geo.append(pt)
+                new_geo_el['solid'] = Polygon(geo)
+                center = Point([point_x, point_y])
+                new_geo_el['follow'] = center
+                return new_geo_el
+        else:
+            self.draw_app.app.inform.emit(_(
+                "Incompatible aperture type. Select an aperture with type 'C', 'R' or 'O'."))
+            return None
+
+    def make(self):
+        self.draw_app.current_storage = self.storage_obj
+        try:
+            self.geometry = DrawToolShape(self.util_shape(self.points))
+        except Exception as e:
+            log.debug("PadEditorGrb.make() --> %s" % str(e))
+
+        self.draw_app.in_action = False
+        self.complete = True
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        self.draw_app.app.jump_signal.disconnect()
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class PadArrayEditorGrb(ShapeToolEditorGrb):
+    """
+    Resulting type: MultiPolygon
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'array'
+        self.draw_app = draw_app
+        self.dont_execute = False
+
+        try:
+            self.radius = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size']) / 2
+        except KeyError:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                          _("You need to preselect a aperture in the Aperture Table that has a size."))
+            self.complete = True
+            self.dont_execute = True
+            self.draw_app.in_action = False
+            self.draw_app.ui.array_frame.hide()
+            self.draw_app.select_tool('select')
+            return
+
+        if self.radius == 0:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                          _("Aperture size is zero. It needs to be greater than zero."))
+            self.dont_execute = True
+            return
+        else:
+            self.dont_execute = False
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_array.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        self.storage_obj = self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['geometry']
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+        # if those cause KeyError exception it means that the aperture type is not 'R'. Only 'R' type has those keys
+        try:
+            self.half_width = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['width']) / 2
+        except KeyError:
+            pass
+        try:
+            self.half_height = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['height']) / 2
+        except KeyError:
+            pass
+
+        self.draw_app.ui.array_frame.show()
+
+        self.selected_size = None
+        self.pad_axis = 'X'
+        self.pad_array = 'linear'  # 'linear'
+        self.pad_array_size = None
+        self.pad_pitch = None
+        self.pad_linear_angle = None
+
+        self.pad_angle = None
+        self.pad_direction = None
+        self.pad_radius = None
+
+        self.origin = None
+        self.destination = None
+        self.flag_for_circ_array = None
+
+        self.last_dx = 0
+        self.last_dy = 0
+
+        self.pt = []
+
+        geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y), static=True)
+
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            self.draw_app.draw_utility_geometry(geo_shape=geo)
+
+        self.draw_app.app.inform.emit(_("Click on target location ..."))
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+    def click(self, point):
+
+        if self.draw_app.ui.array_type_radio.get_value() == 0:     # 'Linear'
+            self.make()
+            return
+        else:
+            if self.flag_for_circ_array is None:
+                self.draw_app.in_action = True
+                self.pt.append(point)
+
+                self.flag_for_circ_array = True
+                self.set_origin(point)
+                self.draw_app.app.inform.emit(_("Click on the Pad Circular Array Start position"))
+            else:
+                self.destination = point
+                self.make()
+                self.flag_for_circ_array = None
+                return
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def utility_geometry(self, data=None, static=None):
+        """
+
+        :param data:    a tuple of coordinates (x, y)
+        :type data:     tuple
+        :param static:  if to draw a static temp geometry
+        :type static:   bool
+        :return:
+        """
+        if self.dont_execute is True:
+            self.draw_app.select_tool('select')
+            return
+
+        self.pad_axis = self.draw_app.ui.pad_axis_radio.get_value()
+        self.pad_direction = self.draw_app.ui.pad_direction_radio.get_value()
+        self.pad_array = self.draw_app.ui.array_type_radio.get_value()
+
+        try:
+            self.pad_array_size = int(self.draw_app.ui.pad_array_size_entry.get_value())
+            try:
+                self.pad_pitch = self.draw_app.ui.pad_pitch_entry.get_value()
+                self.pad_linear_angle = self.draw_app.ui.linear_angle_spinner.get_value()
+                self.pad_angle = self.draw_app.ui.pad_angle_entry.get_value()
+            except TypeError:
+                self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                              _("The value is not Float. Check for comma instead of dot separator."))
+                return
+        except Exception:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' % _("The value is mistyped. Check the value."))
+            return
+
+        if self.pad_array == 'linear':     # 'Linear'
+            if data[0] is None and data[1] is None:
+                dx = self.draw_app.x
+                dy = self.draw_app.y
+            else:
+                dx = data[0]
+                dy = data[1]
+
+            geo_el_list = []
+            geo_el = {}
+            self.points = [dx, dy]
+
+            for item in range(self.pad_array_size):
+                if self.pad_axis == 'X':
+                    geo_el = self.util_shape(((dx + (self.pad_pitch * item)), dy))
+                if self.pad_axis == 'Y':
+                    geo_el = self.util_shape((dx, (dy + (self.pad_pitch * item))))
+                if self.pad_axis == 'A':
+                    x_adj = self.pad_pitch * math.cos(math.radians(self.pad_linear_angle))
+                    y_adj = self.pad_pitch * math.sin(math.radians(self.pad_linear_angle))
+                    geo_el = self.util_shape(
+                        ((dx + (x_adj * item)), (dy + (y_adj * item)))
+                    )
+
+                if static is None or static is False:
+                    new_geo_el = {}
+
+                    if 'solid' in geo_el:
+                        new_geo_el['solid'] = affinity.translate(
+                            geo_el['solid'], xoff=(dx - self.last_dx), yoff=(dy - self.last_dy)
+                        )
+                    if 'follow' in geo_el:
+                        new_geo_el['follow'] = affinity.translate(
+                            geo_el['follow'], xoff=(dx - self.last_dx), yoff=(dy - self.last_dy)
+                        )
+                    geo_el_list.append(new_geo_el)
+
+                else:
+                    geo_el_list.append(geo_el)
+            # self.origin = data
+
+            self.last_dx = dx
+            self.last_dy = dy
+            return DrawToolUtilityShape(geo_el_list)
+        elif self.pad_array == 'circular':   # 'Circular'
+            if data[0] is None and data[1] is None:
+                cdx = self.draw_app.x
+                cdy = self.draw_app.y
+            else:
+                cdx = data[0]
+                cdy = data[1]
+
+            utility_list = []
+
+            try:
+                radius = distance((cdx, cdy), self.origin)
+            except Exception:
+                radius = 0
+
+            if radius == 0:
+                self.draw_app.delete_utility_geometry()
+
+            if len(self.pt) >= 1 and radius > 0:
+                try:
+                    if cdx < self.origin[0]:
+                        radius = -radius
+
+                    # draw the temp geometry
+                    initial_angle = math.asin((cdy - self.origin[1]) / radius)
+
+                    temp_circular_geo = self.circular_util_shape(radius, initial_angle)
+
+                    # draw the line
+                    temp_points = [x for x in self.pt]
+                    temp_points.append([cdx, cdy])
+
+                    temp_line_el = {
+                        'solid': LineString(temp_points)
+                    }
+
+                    for geo_shape in temp_circular_geo:
+                        utility_list.append(geo_shape.geo)
+                    utility_list.append(temp_line_el)
+
+                    return DrawToolUtilityShape(utility_list)
+                except Exception as e:
+                    log.debug(str(e))
+
+    def util_shape(self, point):
+        # updating values here allows us to change the aperture on the fly, after the Tool has been started
+        self.storage_obj = self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['geometry']
+        self.radius = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size']) / 2
+        self.steps_per_circ = self.draw_app.app.defaults["geometry_circle_steps"]
+
+        # if those cause KeyError exception it means that the aperture type is not 'R'. Only 'R' type has those keys
+        try:
+            self.half_width = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['width']) / 2
+        except KeyError:
+            pass
+        try:
+            self.half_height = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['height']) / 2
+        except KeyError:
+            pass
+
+        if point[0] is None and point[1] is None:
+            point_x = self.draw_app.x
+            point_y = self.draw_app.y
+        else:
+            point_x = point[0]
+            point_y = point[1]
+
+        ap_type = self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['type']
+        if ap_type == 'C':
+            new_geo_el = {}
+
+            center = Point([point_x, point_y])
+            new_geo_el['solid'] = center.buffer(self.radius)
+            new_geo_el['follow'] = center
+            return new_geo_el
+        elif ap_type == 'R':
+            new_geo_el = {}
+
+            p1 = (point_x - self.half_width, point_y - self.half_height)
+            p2 = (point_x + self.half_width, point_y - self.half_height)
+            p3 = (point_x + self.half_width, point_y + self.half_height)
+            p4 = (point_x - self.half_width, point_y + self.half_height)
+            new_geo_el['solid'] = Polygon([p1, p2, p3, p4, p1])
+            new_geo_el['follow'] = Point([point_x, point_y])
+            return new_geo_el
+        elif ap_type == 'O':
+            geo = []
+            new_geo_el = {}
+
+            if self.half_height > self.half_width:
+                p1 = (point_x - self.half_width, point_y - self.half_height + self.half_width)
+                p2 = (point_x + self.half_width, point_y - self.half_height + self.half_width)
+                p3 = (point_x + self.half_width, point_y + self.half_height - self.half_width)
+                p4 = (point_x - self.half_width, point_y + self.half_height - self.half_width)
+
+                down_center = [point_x, point_y - self.half_height + self.half_width]
+                d_start_angle = np.pi
+                d_stop_angle = 0.0
+                down_arc = arc(down_center, self.half_width, d_start_angle, d_stop_angle, 'ccw', self.steps_per_circ)
+
+                up_center = [point_x, point_y + self.half_height - self.half_width]
+                u_start_angle = 0.0
+                u_stop_angle = np.pi
+                up_arc = arc(up_center, self.half_width, u_start_angle, u_stop_angle, 'ccw', self.steps_per_circ)
+
+                geo.append(p1)
+                for pt in down_arc:
+                    geo.append(pt)
+                geo.append(p2)
+                geo.append(p3)
+                for pt in up_arc:
+                    geo.append(pt)
+                geo.append(p4)
+
+                new_geo_el['solid'] = Polygon(geo)
+                center = Point([point_x, point_y])
+                new_geo_el['follow'] = center
+                return new_geo_el
+            else:
+                p1 = (point_x - self.half_width + self.half_height, point_y - self.half_height)
+                p2 = (point_x + self.half_width - self.half_height, point_y - self.half_height)
+                p3 = (point_x + self.half_width - self.half_height, point_y + self.half_height)
+                p4 = (point_x - self.half_width + self.half_height, point_y + self.half_height)
+
+                left_center = [point_x - self.half_width + self.half_height, point_y]
+                d_start_angle = np.pi / 2
+                d_stop_angle = 1.5 * np.pi
+                left_arc = arc(left_center, self.half_height, d_start_angle, d_stop_angle, 'ccw', self.steps_per_circ)
+
+                right_center = [point_x + self.half_width - self.half_height, point_y]
+                u_start_angle = 1.5 * np.pi
+                u_stop_angle = np.pi / 2
+                right_arc = arc(right_center, self.half_height, u_start_angle, u_stop_angle, 'ccw', self.steps_per_circ)
+
+                geo.append(p1)
+                geo.append(p2)
+                for pt in right_arc:
+                    geo.append(pt)
+                geo.append(p3)
+                geo.append(p4)
+                for pt in left_arc:
+                    geo.append(pt)
+
+                new_geo_el['solid'] = Polygon(geo)
+                center = Point([point_x, point_y])
+                new_geo_el['follow'] = center
+                return new_geo_el
+        else:
+            self.draw_app.app.inform.emit(_(
+                "Incompatible aperture type. Select an aperture with type 'C', 'R' or 'O'."))
+            return None
+
+    def circular_util_shape(self, radius, angle):
+        self.pad_direction = self.draw_app.ui.pad_direction_radio.get_value()
+        self.pad_angle = self.draw_app.ui.pad_angle_entry.get_value()
+
+        circular_geo = []
+        if self.pad_direction == 'CW':
+            for i in range(self.pad_array_size):
+                angle_radians = math.radians(self.pad_angle * i)
+                x = self.origin[0] + radius * math.cos(-angle_radians + angle)
+                y = self.origin[1] + radius * math.sin(-angle_radians + angle)
+
+                geo = self.util_shape((x, y))
+                geo_sol = affinity.rotate(geo['solid'], angle=(math.pi - angle_radians + angle), use_radians=True)
+                geo_fol = affinity.rotate(geo['follow'], angle=(math.pi - angle_radians + angle), use_radians=True)
+                geo_el = {
+                    'solid': geo_sol,
+                    'follow': geo_fol
+                }
+                circular_geo.append(DrawToolShape(geo_el))
+        else:
+            for i in range(self.pad_array_size):
+                angle_radians = math.radians(self.pad_angle * i)
+                x = self.origin[0] + radius * math.cos(angle_radians + angle)
+                y = self.origin[1] + radius * math.sin(angle_radians + angle)
+
+                geo = self.util_shape((x, y))
+                geo_sol = affinity.rotate(geo['solid'], angle=(angle_radians + angle - math.pi), use_radians=True)
+                geo_fol = affinity.rotate(geo['follow'], angle=(angle_radians + angle - math.pi), use_radians=True)
+                geo_el = {
+                    'solid': geo_sol,
+                    'follow': geo_fol
+                }
+                circular_geo.append(DrawToolShape(geo_el))
+
+        return circular_geo
+
+    def make(self):
+        self.geometry = []
+        geo = None
+
+        self.draw_app.current_storage = self.storage_obj
+
+        if self.pad_array == 'linear':     # 'Linear'
+            for item in range(self.pad_array_size):
+                if self.pad_axis == 'X':
+                    geo = self.util_shape(((self.points[0] + (self.pad_pitch * item)), self.points[1]))
+                if self.pad_axis == 'Y':
+                    geo = self.util_shape((self.points[0], (self.points[1] + (self.pad_pitch * item))))
+                if self.pad_axis == 'A':
+                    x_adj = self.pad_pitch * math.cos(math.radians(self.pad_linear_angle))
+                    y_adj = self.pad_pitch * math.sin(math.radians(self.pad_linear_angle))
+                    geo = self.util_shape(
+                        ((self.points[0] + (x_adj * item)), (self.points[1] + (y_adj * item)))
+                    )
+
+                self.geometry.append(DrawToolShape(geo))
+        else:   # 'Circular'
+            if (self.pad_angle * self.pad_array_size) > 360:
+                self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                              _("Too many items for the selected spacing angle."))
+                return
+
+            radius = distance(self.destination, self.origin)
+            if radius == 0:
+                self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' % _("Failed."))
+                self.draw_app.delete_utility_geometry()
+                self.draw_app.select_tool('select')
+                return
+
+            if self.destination[0] < self.origin[0]:
+                radius = -radius
+            initial_angle = math.asin((self.destination[1] - self.origin[1]) / radius)
+
+            circular_geo = self.circular_util_shape(radius, initial_angle)
+            self.geometry += circular_geo
+
+        self.complete = True
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        self.draw_app.in_action = False
+        self.draw_app.ui.array_frame.hide()
+        self.draw_app.app.jump_signal.disconnect()
+
+    def on_key(self, key):
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if key_modifier == QtCore.Qt.ShiftModifier:
+            mod_key = 'Shift'
+        elif key_modifier == QtCore.Qt.ControlModifier:
+            mod_key = 'Control'
+        else:
+            mod_key = None
+
+        if mod_key == 'Control':
+            pass
+        elif mod_key is None:
+            # Toggle Drill Array Direction
+            if key == QtCore.Qt.Key_Space:
+                if self.draw_app.ui.pad_axis_radio.get_value() == 'X':
+                    self.draw_app.ui.pad_axis_radio.set_value('Y')
+                elif self.draw_app.ui.pad_axis_radio.get_value() == 'Y':
+                    self.draw_app.ui.pad_axis_radio.set_value('A')
+                elif self.draw_app.ui.pad_axis_radio.get_value() == 'A':
+                    self.draw_app.ui.pad_axis_radio.set_value('X')
+
+                # ## Utility geometry (animated)
+                self.draw_app.update_utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class PoligonizeEditorGrb(ShapeToolEditorGrb):
+    """
+    Resulting type: Polygon
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'poligonize'
+        self.draw_app = draw_app
+
+        self.draw_app.app.inform.emit(_("Select shape(s) and then click ..."))
+        self.draw_app.in_action = True
+        self.make()
+
+    def click(self, point):
+        return ""
+
+    def make(self):
+        if not self.draw_app.selected:
+            self.draw_app.in_action = False
+            self.complete = True
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                          _("Failed. Nothing selected."))
+            self.draw_app.select_tool("select")
+            return
+
+        apcode_set = set()
+        for elem in self.draw_app.selected:
+            for apcode in self.draw_app.storage_dict:
+                if 'geometry' in self.draw_app.storage_dict[apcode]:
+                    if elem in self.draw_app.storage_dict[apcode]['geometry']:
+                        apcode_set.add(apcode)
+                        break
+
+        if len(apcode_set) > 1:
+            self.draw_app.in_action = False
+            self.complete = True
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s' %
+                                          _("Failed. Poligonize works only on geometries belonging "
+                                            "to the same aperture."))
+            self.draw_app.select_tool("select")
+            return
+
+        # exterior_geo = [Polygon(sh.geo.exterior) for sh in self.draw_app.selected]
+
+        exterior_geo = []
+        for geo_shape in self.draw_app.selected:
+            geometric_data = geo_shape.geo
+            if 'solid' in geometric_data:
+                exterior_geo.append(Polygon(geometric_data['solid'].exterior))
+
+        fused_geo = MultiPolygon(exterior_geo)
+        fused_geo = fused_geo.buffer(0.0000001)
+
+        current_storage = self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['geometry']
+        if isinstance(fused_geo, MultiPolygon):
+            for geo in fused_geo:
+                # clean-up the geo
+                geo = geo.buffer(0)
+
+                if len(geo.interiors) == 0:
+                    try:
+                        current_storage = self.draw_app.storage_dict['0']['geometry']
+                    except KeyError:
+                        self.draw_app.on_aperture_add(apcode='0')
+                        current_storage = self.draw_app.storage_dict['0']['geometry']
+                new_el = {'solid': geo, 'follow': geo.exterior}
+                self.draw_app.on_grb_shape_complete(current_storage, specific_shape=DrawToolShape(deepcopy(new_el)))
+        else:
+            # clean-up the geo
+            fused_geo = fused_geo.buffer(0)
+
+            if len(fused_geo.interiors) == 0 and len(exterior_geo) == 1:
+                try:
+                    current_storage = self.draw_app.storage_dict['0']['geometry']
+                except KeyError:
+                    self.draw_app.on_aperture_add(apcode='0')
+                    current_storage = self.draw_app.storage_dict['0']['geometry']
+
+            new_el = {'solid': fused_geo, 'follow': fused_geo.exterior}
+            self.draw_app.on_grb_shape_complete(current_storage, specific_shape=DrawToolShape(deepcopy(new_el)))
+
+        self.draw_app.delete_selected()
+        self.draw_app.plot_all()
+
+        self.draw_app.in_action = False
+        self.complete = True
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+
+        # MS: always return to the Select Tool if modifier key is not pressed
+        # else return to the current tool
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+        if self.draw_app.app.defaults["global_mselect_key"] == 'Control':
+            modifier_to_use = Qt.ControlModifier
+        else:
+            modifier_to_use = Qt.ShiftModifier
+        # if modifier key is pressed then we add to the selected list the current shape but if it's already
+        # in the selected list, we removed it. Therefore first click selects, second deselects.
+        if key_modifier == modifier_to_use:
+            self.draw_app.select_tool(self.draw_app.active_tool.name)
+        else:
+            self.draw_app.select_tool("select")
+            return
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+
+
+class RegionEditorGrb(ShapeToolEditorGrb):
+    """
+    Resulting type: Polygon
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'region'
+        self.draw_app = draw_app
+        self.dont_execute = False
+
+        self.steps_per_circle = self.draw_app.app.defaults["gerber_circle_steps"]
+
+        try:
+            size_ap = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size'])
+        except KeyError:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                          _("You need to preselect a aperture in the Aperture Table that has a size."))
+            try:
+                QtGui.QGuiApplication.restoreOverrideCursor()
+            except Exception:
+                pass
+            self.dont_execute = True
+            self.draw_app.in_action = False
+            self.complete = True
+            self.draw_app.select_tool('select')
+            return
+
+        self.buf_val = (size_ap / 2) if size_ap > 0 else 0.0000001
+
+        self.gridx_size = float(self.draw_app.app.ui.grid_gap_x_entry.get_value())
+        self.gridy_size = float(self.draw_app.app.ui.grid_gap_y_entry.get_value())
+
+        self.temp_points = []
+        # this will store the inflexion point in the geometry
+        self.inter_point = None
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception as e:
+            log.debug("AppGerberEditor.RegionEditorGrb --> %s" % str(e))
+
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        self.draw_app.app.inform.emit(_('Corner Mode 1: 45 degrees ...'))
+
+        self.start_msg = _("Click on 1st point ...")
+
+    def click(self, point):
+        self.draw_app.in_action = True
+
+        if self.inter_point is not None:
+            self.points.append(self.inter_point)
+        self.points.append(point)
+
+        if len(self.points) > 0:
+            self.draw_app.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
+            return "Click on next point or hit ENTER to complete ..."
+
+        return ""
+
+    def update_grid_info(self):
+        self.gridx_size = float(self.draw_app.app.ui.grid_gap_x_entry.get_value())
+        self.gridy_size = float(self.draw_app.app.ui.grid_gap_y_entry.get_value())
+
+    def utility_geometry(self, data=None):
+        if self.dont_execute is True:
+            self.draw_app.select_tool('select')
+            return
+
+        new_geo_el = {}
+        x = data[0]
+        y = data[1]
+
+        if len(self.points) == 0:
+            new_geo_el['solid'] = Point((x, y)).buffer(self.buf_val, resolution=int(self.steps_per_circle / 4))
+            return DrawToolUtilityShape(new_geo_el)
+
+        elif len(self.points) == 1:
+            self.temp_points = [x for x in self.points]
+
+            # previous point coordinates
+            old_x = self.points[0][0]
+            old_y = self.points[0][1]
+            # how many grid sections between old point and new point
+            mx = abs(round((x - old_x) / self.gridx_size))
+            my = abs(round((y - old_y) / self.gridy_size))
+
+            if self.draw_app.app.ui.grid_snap_btn.isChecked() and mx and my:
+                # calculate intermediary point
+                if self.draw_app.bend_mode != 5:
+                    if self.draw_app.bend_mode == 1:
+                        # if we move from left to right
+                        if x > old_x:
+                            # if the number of grid sections is greater on the X axis
+                            if mx > my:
+                                self.inter_point = (old_x + self.gridx_size * (mx - my), old_y)
+                            # if the number of grid sections is greater on the Y axis
+                            if mx < my:
+                                # if we move from top to down
+                                if y < old_y:
+                                    self.inter_point = (old_x, old_y - self.gridy_size * (my - mx))
+                                # if we move from down to top or at the same height
+                                else:
+                                    self.inter_point = (old_x, old_y - self.gridy_size * (mx - my))
+                        # if we move from right to left
+                        elif x < old_x:
+                            # if the number of grid sections is greater on the X axis
+                            if mx > my:
+                                self.inter_point = (old_x - self.gridx_size * (mx - my), old_y)
+                            # if the number of grid sections is greater on the Y axis
+                            if mx < my:
+                                # if we move from top to down
+                                if y < old_y:
+                                    self.inter_point = (old_x, old_y - self.gridy_size * (my - mx))
+                                # if we move from down to top or at the same height
+                                else:
+                                    self.inter_point = (old_x, old_y - self.gridy_size * (mx - my))
+                    elif self.draw_app.bend_mode == 2:
+                        if x > old_x:
+                            if mx > my:
+                                self.inter_point = (old_x + self.gridx_size * my, y)
+                            if mx < my:
+                                if y < old_y:
+                                    self.inter_point = (x, old_y - self.gridy_size * mx)
+                                else:
+                                    self.inter_point = (x, old_y + self.gridy_size * mx)
+                        if x < old_x:
+                            if mx > my:
+                                self.inter_point = (old_x - self.gridx_size * my, y)
+                            if mx < my:
+                                if y < old_y:
+                                    self.inter_point = (x, old_y - self.gridy_size * mx)
+                                else:
+                                    self.inter_point = (x, old_y + self.gridy_size * mx)
+                    elif self.draw_app.bend_mode == 3:
+                        self.inter_point = (x, old_y)
+                    elif self.draw_app.bend_mode == 4:
+                        self.inter_point = (old_x, y)
+
+                    # add the intermediary point to the points storage
+                    if self.inter_point is not None:
+                        self.temp_points.append(self.inter_point)
+                    else:
+                        self.inter_point = (x, y)
+                else:
+                    self.inter_point = None
+
+            else:
+                self.inter_point = (x, y)
+
+            # add click point to the points storage
+            self.temp_points.append(
+                (x, y)
+            )
+
+            if len(self.temp_points) > 1:
+                try:
+                    geo_sol = LineString(self.temp_points)
+                    geo_sol = geo_sol.buffer(self.buf_val, int(self.steps_per_circle / 4), join_style=1)
+                    new_geo_el = {
+                        'solid': geo_sol
+                    }
+                    return DrawToolUtilityShape(new_geo_el)
+                except Exception as e:
+                    log.debug("AppGerberEditor.RegionEditorGrb.utility_geometry() --> %s" % str(e))
+            else:
+                geo_sol = Point(self.temp_points).buffer(self.buf_val, resolution=int(self.steps_per_circle / 4))
+                new_geo_el = {
+                    'solid': geo_sol
+                }
+                return DrawToolUtilityShape(new_geo_el)
+
+        elif len(self.points) > 1:
+            self.temp_points = [x for x in self.points]
+
+            # previous point coordinates
+            old_x = self.points[-1][0]
+            old_y = self.points[-1][1]
+            # how many grid sections between old point and new point
+            mx = abs(round((x - old_x) / self.gridx_size))
+            my = abs(round((y - old_y) / self.gridy_size))
+
+            if self.draw_app.app.ui.grid_snap_btn.isChecked() and mx and my:
+                # calculate intermediary point
+                if self.draw_app.bend_mode != 5:
+                    if self.draw_app.bend_mode == 1:
+                        # if we move from left to right
+                        if x > old_x:
+                            # if the number of grid sections is greater on the X axis
+                            if mx > my:
+                                self.inter_point = (old_x + self.gridx_size * (mx - my), old_y)
+                            # if the number of grid sections is greater on the Y axis
+                            elif mx < my:
+                                # if we move from top to down
+                                if y < old_y:
+                                    self.inter_point = (old_x, old_y - self.gridy_size * (my - mx))
+                                # if we move from down to top or at the same height
+                                else:
+                                    self.inter_point = (old_x, old_y + self.gridy_size * (my - mx))
+                            elif mx == my:
+                                pass
+                        # if we move from right to left
+                        if x < old_x:
+                            # if the number of grid sections is greater on the X axis
+                            if mx > my:
+                                self.inter_point = (old_x - self.gridx_size * (mx - my), old_y)
+                            # if the number of grid sections is greater on the Y axis
+                            elif mx < my:
+                                # if we move from top to down
+                                if y < old_y:
+                                    self.inter_point = (old_x, old_y - self.gridy_size * (my - mx))
+                                # if we move from down to top or at the same height
+                                else:
+                                    self.inter_point = (old_x, old_y + self.gridy_size * (my - mx))
+                            elif mx == my:
+                                pass
+                    elif self.draw_app.bend_mode == 2:
+                        if x > old_x:
+                            if mx > my:
+                                self.inter_point = (old_x + self.gridx_size * my, y)
+                            if mx < my:
+                                if y < old_y:
+                                    self.inter_point = (x, old_y - self.gridy_size * mx)
+                                else:
+                                    self.inter_point = (x, old_y + self.gridy_size * mx)
+                        if x < old_x:
+                            if mx > my:
+                                self.inter_point = (old_x - self.gridx_size * my, y)
+                            if mx < my:
+                                if y < old_y:
+                                    self.inter_point = (x, old_y - self.gridy_size * mx)
+                                else:
+                                    self.inter_point = (x, old_y + self.gridy_size * mx)
+                    elif self.draw_app.bend_mode == 3:
+                        self.inter_point = (x, old_y)
+                    elif self.draw_app.bend_mode == 4:
+                        self.inter_point = (old_x, y)
+
+                    # add the intermediary point to the points storage
+                    # self.temp_points.append(self.inter_point)
+                    if self.inter_point is not None:
+                        self.temp_points.append(self.inter_point)
+                else:
+                    self.inter_point = None
+            else:
+                self.inter_point = (x, y)
+
+            # add click point to the points storage
+            self.temp_points.append(
+                (x, y)
+            )
+
+            # create the geometry
+            geo_line = LinearRing(self.temp_points)
+            geo_sol = geo_line.buffer(self.buf_val, int(self.steps_per_circle / 4), join_style=1)
+            new_geo_el = {
+                'solid': geo_sol,
+                'follow': geo_line
+            }
+
+            return DrawToolUtilityShape(new_geo_el)
+
+        return None
+
+    def make(self):
+        # self.geometry = LinearRing(self.points)
+        if len(self.points) > 2:
+
+            # regions are added always in the '0' aperture
+            if '0' not in self.draw_app.storage_dict:
+                self.draw_app.on_aperture_add(apcode='0')
+            else:
+                self.draw_app.last_aperture_selected = '0'
+
+            new_geo_el = {
+                'solid': Polygon(self.points).buffer(self.buf_val, int(self.steps_per_circle / 4), join_style=2),
+                'follow': Polygon(self.points).exterior
+            }
+
+            self.geometry = DrawToolShape(new_geo_el)
+        self.draw_app.in_action = False
+        self.complete = True
+
+        self.draw_app.app.jump_signal.disconnect()
+
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+
+    def on_key(self, key):
+        # Jump to coords
+        if key == QtCore.Qt.Key_J or key == 'J':
+            self.draw_app.app.on_jump_to()
+
+        if key == 'Backspace' or key == QtCore.Qt.Key_Backspace:
+            if len(self.points) > 0:
+                if self.draw_app.bend_mode == 5:
+                    self.points = self.points[0:-1]
+                else:
+                    self.points = self.points[0:-2]
+                # Remove any previous utility shape
+                self.draw_app.tool_shape.clear(update=False)
+                geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+                self.draw_app.draw_utility_geometry(geo_shape=geo)
+                return _("Backtracked one point ...")
+
+        if key == 'T' or key == QtCore.Qt.Key_T:
+            if self.draw_app.bend_mode == 1:
+                self.draw_app.bend_mode = 2
+                msg = _('Corner Mode 2: Reverse 45 degrees ...')
+            elif self.draw_app.bend_mode == 2:
+                self.draw_app.bend_mode = 3
+                msg = _('Corner Mode 3: 90 degrees ...')
+            elif self.draw_app.bend_mode == 3:
+                self.draw_app.bend_mode = 4
+                msg = _('Corner Mode 4: Reverse 90 degrees ...')
+            elif self.draw_app.bend_mode == 4:
+                self.draw_app.bend_mode = 5
+                msg = _('Corner Mode 5: Free angle ...')
+            else:
+                self.draw_app.bend_mode = 1
+                msg = _('Corner Mode 1: 45 degrees ...')
+
+            # Remove any previous utility shape
+            self.draw_app.tool_shape.clear(update=False)
+            geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+            self.draw_app.draw_utility_geometry(geo_shape=geo)
+
+            return msg
+
+        if key == 'R' or key == QtCore.Qt.Key_R:
+            if self.draw_app.bend_mode == 1:
+                self.draw_app.bend_mode = 5
+                msg = _('Corner Mode 5: Free angle ...')
+            elif self.draw_app.bend_mode == 5:
+                self.draw_app.bend_mode = 4
+                msg = _('Corner Mode 4: Reverse 90 degrees ...')
+            elif self.draw_app.bend_mode == 4:
+                self.draw_app.bend_mode = 3
+                msg = _('Corner Mode 3: 90 degrees ...')
+            elif self.draw_app.bend_mode == 3:
+                self.draw_app.bend_mode = 2
+                msg = _('Corner Mode 2: Reverse 45 degrees ...')
+            else:
+                self.draw_app.bend_mode = 1
+                msg = _('Corner Mode 1: 45 degrees ...')
+
+            # Remove any previous utility shape
+            self.draw_app.tool_shape.clear(update=False)
+            geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+            self.draw_app.draw_utility_geometry(geo_shape=geo)
+
+            return msg
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class TrackEditorGrb(ShapeToolEditorGrb):
+    """
+    Resulting type: Polygon
+    """
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'track'
+        self.draw_app = draw_app
+        self.dont_execute = False
+
+        self.steps_per_circle = self.draw_app.app.defaults["gerber_circle_steps"]
+
+        try:
+            size_ap = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size'])
+        except KeyError:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                          _("You need to preselect a aperture in the Aperture Table that has a size."))
+            try:
+                QtGui.QGuiApplication.restoreOverrideCursor()
+            except Exception:
+                pass
+            self.dont_execute = True
+            self.draw_app.in_action = False
+            self.complete = True
+            self.draw_app.select_tool('select')
+            return
+
+        self.buf_val = (size_ap / 2) if size_ap > 0 else 0.0000001
+
+        self.gridx_size = float(self.draw_app.app.ui.grid_gap_x_entry.get_value())
+        self.gridy_size = float(self.draw_app.app.ui.grid_gap_y_entry.get_value())
+
+        self.temp_points = []
+
+        self.current_point = None
+
+        self.final_click = False
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception as e:
+            log.debug("AppGerberEditor.TrackEditorGrb.__init__() --> %s" % str(e))
+
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location +
+                                                  '/aero_path%s.png' % self.draw_app.bend_mode))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        self.draw_app.app.inform.emit(_('Track Mode 1: 45 degrees ...'))
+
+    def click(self, point):
+        self.draw_app.in_action = True
+
+        self.current_point = point
+
+        if not self.points or point != self.points[-1]:
+            self.points.append(point)
+        else:
+            return
+
+        if len(self.temp_points) == 1:
+            point_geo = Point(self.temp_points[0])
+            new_geo_el = {
+                'solid': point_geo.buffer(self.buf_val, int(self.steps_per_circle)),
+                'follow': point_geo
+            }
+        else:
+            line_geo = LineString(self.temp_points)
+            new_geo_el = {
+                'solid': line_geo.buffer(self.buf_val, int(self.steps_per_circle)),
+                'follow': line_geo
+            }
+
+        self.draw_app.add_gerber_shape(DrawToolShape(new_geo_el),
+                                       self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['geometry'])
+
+        self.draw_app.plot_all()
+        if len(self.points) > 0:
+            self.draw_app.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
+            return "Click on next point or hit ENTER to complete ..."
+
+        return ""
+
+    def update_grid_info(self):
+        self.gridx_size = float(self.draw_app.app.ui.grid_gap_x_entry.get_value())
+        self.gridy_size = float(self.draw_app.app.ui.grid_gap_y_entry.get_value())
+
+    def utility_geometry(self, data=None):
+        if self.dont_execute is True:
+            self.draw_app.select_tool('select')
+            return
+
+        self.update_grid_info()
+
+        if not self.points:
+            new_geo_el = {
+                'solid': Point(data).buffer(self.buf_val, int(self.steps_per_circle))
+            }
+            return DrawToolUtilityShape(new_geo_el)
+        else:
+            old_x = self.points[-1][0]
+            old_y = self.points[-1][1]
+            x = data[0]
+            y = data[1]
+
+            self.temp_points = [self.points[-1]]
+
+            mx = abs(round((x - old_x) / self.gridx_size))
+            my = abs(round((y - old_y) / self.gridy_size))
+
+            if self.draw_app.app.ui.grid_snap_btn.isChecked():
+                if self.draw_app.bend_mode == 1:
+                    if x > old_x:
+                        if mx > my:
+                            self.temp_points.append((old_x + self.gridx_size*(mx-my), old_y))
+                        if mx < my:
+                            if y < old_y:
+                                self.temp_points.append((old_x, old_y - self.gridy_size * (my-mx)))
+                            else:
+                                self.temp_points.append((old_x, old_y - self.gridy_size * (mx-my)))
+                    if x < old_x:
+                        if mx > my:
+                            self.temp_points.append((old_x - self.gridx_size*(mx-my), old_y))
+                        if mx < my:
+                            if y < old_y:
+                                self.temp_points.append((old_x, old_y - self.gridy_size * (my-mx)))
+                            else:
+                                self.temp_points.append((old_x, old_y - self.gridy_size * (mx-my)))
+                elif self.draw_app.bend_mode == 2:
+                    if x > old_x:
+                        if mx > my:
+                            self.temp_points.append((old_x + self.gridx_size*my, y))
+                        if mx < my:
+                            if y < old_y:
+                                self.temp_points.append((x, old_y - self.gridy_size * mx))
+                            else:
+                                self.temp_points.append((x, old_y + self.gridy_size * mx))
+                    if x < old_x:
+                        if mx > my:
+                            self.temp_points.append((old_x - self.gridx_size * my, y))
+                        if mx < my:
+                            if y < old_y:
+                                self.temp_points.append((x, old_y - self.gridy_size * mx))
+                            else:
+                                self.temp_points.append((x, old_y + self.gridy_size * mx))
+                elif self.draw_app.bend_mode == 3:
+                    self.temp_points.append((x, old_y))
+                elif self.draw_app.bend_mode == 4:
+                    self.temp_points.append((old_x, y))
+                else:
+                    pass
+
+            self.temp_points.append(data)
+
+            if len(self.temp_points) == 1:
+                new_geo_el = {
+                    'solid': Point(self.temp_points[0]).buffer(self.buf_val, int(self.steps_per_circle))
+                }
+            else:
+                new_geo_el = {
+                    'solid': LineString(self.temp_points).buffer(self.buf_val, int(self.steps_per_circle))
+                }
+
+            return DrawToolUtilityShape(new_geo_el)
+
+    def make(self):
+        if len(self.temp_points) == 1:
+            follow_geo = Point(self.temp_points[0])
+            solid_geo = follow_geo.buffer(self.buf_val, int(self.steps_per_circle))
+        else:
+            follow_geo = LineString(self.temp_points)
+            solid_geo = follow_geo.buffer(self.buf_val, int(self.steps_per_circle))
+            solid_geo = solid_geo.buffer(0)     # try to clean the geometry
+
+        new_geo_el = {
+            'solid': solid_geo,
+            'follow': follow_geo
+        }
+        self.geometry = DrawToolShape(new_geo_el)
+
+        self.draw_app.in_action = False
+        self.complete = True
+        self.draw_app.app.jump_signal.disconnect()
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+
+    def on_key(self, key):
+        if key == 'Backspace' or key == QtCore.Qt.Key_Backspace:
+            if len(self.points) > 0:
+                self.temp_points = self.points[0:-1]
+                # Remove any previous utility shape
+                self.draw_app.tool_shape.clear(update=False)
+                geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+                self.draw_app.draw_utility_geometry(geo_shape=geo)
+                return _("Backtracked one point ...")
+
+        # Jump to coords
+        if key == QtCore.Qt.Key_G or key == 'G':
+            self.draw_app.app.ui.grid_snap_btn.trigger()
+
+        # Jump to coords
+        if key == QtCore.Qt.Key_J or key == 'J':
+            self.draw_app.app.on_jump_to()
+
+        if key == 'T' or key == QtCore.Qt.Key_T:
+            try:
+                QtGui.QGuiApplication.restoreOverrideCursor()
+            except Exception as e:
+                log.debug("AppGerberEditor.TrackEditorGrb.on_key() --> %s" % str(e))
+
+            if self.draw_app.bend_mode == 1:
+                self.draw_app.bend_mode = 2
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path2.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 2: Reverse 45 degrees ...')
+            elif self.draw_app.bend_mode == 2:
+                self.draw_app.bend_mode = 3
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path3.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 3: 90 degrees ...')
+            elif self.draw_app.bend_mode == 3:
+                self.draw_app.bend_mode = 4
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path4.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 4: Reverse 90 degrees ...')
+            elif self.draw_app.bend_mode == 4:
+                self.draw_app.bend_mode = 5
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path5.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 5: Free angle ...')
+            else:
+                self.draw_app.bend_mode = 1
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path1.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 1: 45 degrees ...')
+
+            # Remove any previous utility shape
+            self.draw_app.tool_shape.clear(update=False)
+            geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+            self.draw_app.draw_utility_geometry(geo_shape=geo)
+
+            return msg
+
+        if key == 'R' or key == QtCore.Qt.Key_R:
+            try:
+                QtGui.QGuiApplication.restoreOverrideCursor()
+            except Exception as e:
+                log.debug("AppGerberEditor.TrackEditorGrb.on_key() --> %s" % str(e))
+
+            if self.draw_app.bend_mode == 1:
+                self.draw_app.bend_mode = 5
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path5.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 5: Free angle ...')
+            elif self.draw_app.bend_mode == 5:
+                self.draw_app.bend_mode = 4
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path4.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 4: Reverse 90 degrees ...')
+            elif self.draw_app.bend_mode == 4:
+                self.draw_app.bend_mode = 3
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path3.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 3: 90 degrees ...')
+            elif self.draw_app.bend_mode == 3:
+                self.draw_app.bend_mode = 2
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path2.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 2: Reverse 45 degrees ...')
+            else:
+                self.draw_app.bend_mode = 1
+                self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_path1.png'))
+                QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+                msg = _('Track Mode 1: 45 degrees ...')
+
+            # Remove any previous utility shape
+            self.draw_app.tool_shape.clear(update=False)
+            geo = self.utility_geometry(data=(self.draw_app.snap_x, self.draw_app.snap_y))
+            self.draw_app.draw_utility_geometry(geo_shape=geo)
+
+            return msg
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class DiscEditorGrb(ShapeToolEditorGrb):
+    """
+    Resulting type: Polygon
+    """
+
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'disc'
+        self.dont_execute = False
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception:
+            pass
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_disc.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        try:
+            size_ap = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size'])
+        except KeyError:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                          _("You need to preselect a aperture in the Aperture Table that has a size."))
+            try:
+                QtGui.QGuiApplication.restoreOverrideCursor()
+            except Exception:
+                pass
+            self.dont_execute = True
+            self.draw_app.in_action = False
+            self.complete = True
+            self.draw_app.select_tool('select')
+            return
+
+        self.buf_val = (size_ap / 2) if size_ap > 0 else 0.0000001
+
+        if '0' in self.draw_app.storage_dict:
+            self.storage_obj = self.draw_app.storage_dict['0']['geometry']
+        else:
+            self.draw_app.storage_dict['0'] = {
+                'type': 'C',
+                'size': 0.0,
+                'geometry': []
+            }
+            self.storage_obj = self.draw_app.storage_dict['0']['geometry']
+
+        self.draw_app.app.inform.emit(_("Click on Center point ..."))
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        self.steps_per_circ = self.draw_app.app.defaults["gerber_circle_steps"]
+
+    def click(self, point):
+        self.points.append(point)
+
+        if len(self.points) == 1:
+            self.draw_app.app.inform.emit(_("Click on Perimeter point to complete ..."))
+            return "Click on Perimeter to complete ..."
+
+        if len(self.points) == 2:
+            self.make()
+            return "Done."
+
+        return ""
+
+    def utility_geometry(self, data=None):
+        if self.dont_execute is True:
+            self.draw_app.select_tool('select')
+            return
+
+        new_geo_el = {}
+        if len(self.points) == 1:
+            p1 = self.points[0]
+            p2 = data
+            radius = math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
+            new_geo_el['solid'] = Point(p1).buffer((radius + self.buf_val / 2), int(self.steps_per_circ / 4))
+            return DrawToolUtilityShape(new_geo_el)
+
+        return None
+
+    def make(self):
+        new_geo_el = {}
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception as e:
+            log.debug("AppGerberEditor.DiscEditorGrb --> %s" % str(e))
+
+        self.draw_app.current_storage = self.storage_obj
+
+        p1 = self.points[0]
+        p2 = self.points[1]
+        radius = distance(p1, p2)
+
+        new_geo_el['solid'] = Point(p1).buffer((radius + self.buf_val / 2), int(self.steps_per_circ / 4))
+        new_geo_el['follow'] = Point(p1).buffer((radius + self.buf_val / 2), int(self.steps_per_circ / 4)).exterior
+        self.geometry = DrawToolShape(new_geo_el)
+
+        self.draw_app.in_action = False
+        self.complete = True
+
+        self.draw_app.app.jump_signal.disconnect()
+
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class DiscSemiEditorGrb(ShapeToolEditorGrb):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'semidisc'
+        self.dont_execute = False
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception as e:
+            log.debug("AppGerberEditor.DiscSemiEditorGrb --> %s" % str(e))
+
+        self.cursor = QtGui.QCursor(QtGui.QPixmap(self.draw_app.app.resource_location + '/aero_semidisc.png'))
+        QtGui.QGuiApplication.setOverrideCursor(self.cursor)
+
+        self.draw_app.app.inform.emit(_("Click on Center point ..."))
+
+        # Direction of rotation between point 1 and 2.
+        # 'cw' or 'ccw'. Switch direction by hitting the
+        # 'o' key.
+        self.direction = "cw"
+
+        # Mode
+        # C12 = Center, p1, p2
+        # 12C = p1, p2, Center
+        # 132 = p1, p3, p2
+        self.mode = "c12"  # Center, p1, p2
+
+        try:
+            size_ap = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size'])
+        except KeyError:
+            self.draw_app.app.inform.emit('[ERROR_NOTCL] %s' %
+                                          _("You need to preselect a aperture in the Aperture Table that has a size."))
+            try:
+                QtGui.QGuiApplication.restoreOverrideCursor()
+            except Exception:
+                pass
+            self.dont_execute = True
+            self.draw_app.in_action = False
+            self.complete = True
+            self.draw_app.select_tool('select')
+            return
+
+        self.buf_val = (size_ap / 2) if size_ap > 0 else 0.0000001
+
+        if '0' in self.draw_app.storage_dict:
+            self.storage_obj = self.draw_app.storage_dict['0']['geometry']
+        else:
+            self.draw_app.storage_dict['0'] = {
+                'type': 'C',
+                'size': 0.0,
+                'geometry': []
+            }
+            self.storage_obj = self.draw_app.storage_dict['0']['geometry']
+
+        self.steps_per_circ = self.draw_app.app.defaults["gerber_circle_steps"]
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+    def click(self, point):
+        self.points.append(point)
+
+        if len(self.points) == 1:
+            if self.mode == 'c12':
+                self.draw_app.app.inform.emit(_("Click on Start point ..."))
+            elif self.mode == '132':
+                self.draw_app.app.inform.emit(_("Click on Point3 ..."))
+            else:
+                self.draw_app.app.inform.emit(_("Click on Stop point ..."))
+            return "Click on 1st point ..."
+
+        if len(self.points) == 2:
+            if self.mode == 'c12':
+                self.draw_app.app.inform.emit(_("Click on Stop point to complete ..."))
+            elif self.mode == '132':
+                self.draw_app.app.inform.emit(_("Click on Point2 to complete ..."))
+            else:
+                self.draw_app.app.inform.emit(_("Click on Center point to complete ..."))
+            return "Click on 2nd point to complete ..."
+
+        if len(self.points) == 3:
+            self.make()
+            return "Done."
+
+        return ""
+
+    def on_key(self, key):
+        if key == 'D' or key == QtCore.Qt.Key_D:
+            self.direction = 'cw' if self.direction == 'ccw' else 'ccw'
+            return '%s: %s' % (_('Direction'), self.direction.upper())
+
+        # Jump to coords
+        if key == QtCore.Qt.Key_J or key == 'J':
+            self.draw_app.app.on_jump_to()
+
+        if key == 'M' or key == QtCore.Qt.Key_M:
+            # delete the possible points made before this action; we want to start anew
+            self.points = []
+            # and delete the utility geometry made up until this point
+            self.draw_app.delete_utility_geometry()
+
+            if self.mode == 'c12':
+                self.mode = '12c'
+                return _('Mode: Start -> Stop -> Center. Click on Start point ...')
+            elif self.mode == '12c':
+                self.mode = '132'
+                return _('Mode: Point1 -> Point3 -> Point2. Click on Point1 ...')
+            else:
+                self.mode = 'c12'
+                return _('Mode: Center -> Start -> Stop. Click on Center point ...')
+
+    def utility_geometry(self, data=None):
+        if self.dont_execute is True:
+            self.draw_app.select_tool('select')
+            return
+
+        new_geo_el = {}
+        new_geo_el_pt1 = {}
+        new_geo_el_pt2 = {}
+        new_geo_el_pt3 = {}
+
+        if len(self.points) == 1:  # Show the radius
+            center = self.points[0]
+            p1 = data
+            new_geo_el['solid'] = LineString([center, p1])
+            return DrawToolUtilityShape(new_geo_el)
+
+        if len(self.points) == 2:  # Show the arc
+
+            if self.mode == 'c12':
+                center = self.points[0]
+                p1 = self.points[1]
+                p2 = data
+
+                radius = np.sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2) + (self.buf_val / 2)
+                startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
+                stopangle = np.arctan2(p2[1] - center[1], p2[0] - center[0])
+
+                new_geo_el['solid'] = LineString(
+                    arc(center, radius, startangle, stopangle, self.direction, self.steps_per_circ))
+                new_geo_el_pt1['solid'] = Point(center)
+                return DrawToolUtilityShape([new_geo_el, new_geo_el_pt1])
+
+            elif self.mode == '132':
+                p1 = np.array(self.points[0])
+                p3 = np.array(self.points[1])
+                p2 = np.array(data)
+
+                try:
+                    center, radius, t = three_point_circle(p1, p2, p3)
+                except TypeError:
+                    return
+
+                direction = 'cw' if np.sign(t) > 0 else 'ccw'
+                radius += (self.buf_val / 2)
+
+                startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
+                stopangle = np.arctan2(p3[1] - center[1], p3[0] - center[0])
+
+                new_geo_el['solid'] = LineString(
+                    arc(center, radius, startangle, stopangle, direction, self.steps_per_circ))
+                new_geo_el_pt2['solid'] = Point(center)
+                new_geo_el_pt1['solid'] = Point(p1)
+                new_geo_el_pt3['solid'] = Point(p3)
+
+                return DrawToolUtilityShape([new_geo_el, new_geo_el_pt2, new_geo_el_pt1, new_geo_el_pt3])
+
+            else:  # '12c'
+                p1 = np.array(self.points[0])
+                p2 = np.array(self.points[1])
+                # Midpoint
+                a = (p1 + p2) / 2.0
+
+                # Parallel vector
+                c = p2 - p1
+
+                # Perpendicular vector
+                b = np.dot(c, np.array([[0, -1], [1, 0]], dtype=np.float32))
+                b /= numpy_norm(b)
+
+                # Distance
+                t = distance(data, a)
+
+                # Which side? Cross product with c.
+                # cross(M-A, B-A), where line is AB and M is test point.
+                side = (data[0] - p1[0]) * c[1] - (data[1] - p1[1]) * c[0]
+                t *= np.sign(side)
+
+                # Center = a + bt
+                center = a + b * t
+
+                radius = numpy_norm(center - p1) + (self.buf_val / 2)
+                startangle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
+                stopangle = np.arctan2(p2[1] - center[1], p2[0] - center[0])
+
+                new_geo_el['solid'] = LineString(
+                    arc(center, radius, startangle, stopangle, self.direction, self.steps_per_circ))
+                new_geo_el_pt2['solid'] = Point(center)
+
+                return DrawToolUtilityShape([new_geo_el, new_geo_el_pt2])
+
+        return None
+
+    def make(self):
+        self.draw_app.current_storage = self.storage_obj
+        new_geo_el = {}
+
+        if self.mode == 'c12':
+            center = self.points[0]
+            p1 = self.points[1]
+            p2 = self.points[2]
+
+            radius = distance(center, p1) + (self.buf_val / 2)
+            start_angle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
+            stop_angle = np.arctan2(p2[1] - center[1], p2[0] - center[0])
+            new_geo_el['solid'] = Polygon(
+                arc(center, radius, start_angle, stop_angle, self.direction, self.steps_per_circ))
+            new_geo_el['follow'] = Polygon(
+                arc(center, radius, start_angle, stop_angle, self.direction, self.steps_per_circ)).exterior
+            self.geometry = DrawToolShape(new_geo_el)
+
+        elif self.mode == '132':
+            p1 = np.array(self.points[0])
+            p3 = np.array(self.points[1])
+            p2 = np.array(self.points[2])
+
+            center, radius, t = three_point_circle(p1, p2, p3)
+            direction = 'cw' if np.sign(t) > 0 else 'ccw'
+            radius += (self.buf_val / 2)
+
+            start_angle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
+            stop_angle = np.arctan2(p3[1] - center[1], p3[0] - center[0])
+
+            new_geo_el['solid'] = Polygon(arc(center, radius, start_angle, stop_angle, direction, self.steps_per_circ))
+            new_geo_el['follow'] = Polygon(
+                arc(center, radius, start_angle, stop_angle, direction, self.steps_per_circ)).exterior
+            self.geometry = DrawToolShape(new_geo_el)
+
+        else:  # self.mode == '12c'
+            p1 = np.array(self.points[0])
+            p2 = np.array(self.points[1])
+            pc = np.array(self.points[2])
+
+            # Midpoint
+            a = (p1 + p2) / 2.0
+
+            # Parallel vector
+            c = p2 - p1
+
+            # Perpendicular vector
+            b = np.dot(c, np.array([[0, -1], [1, 0]], dtype=np.float32))
+            b /= numpy_norm(b)
+
+            # Distance
+            t = distance(pc, a)
+
+            # Which side? Cross product with c.
+            # cross(M-A, B-A), where line is AB and M is test point.
+            side = (pc[0] - p1[0]) * c[1] - (pc[1] - p1[1]) * c[0]
+            t *= np.sign(side)
+
+            # Center = a + bt
+            center = a + b * t
+
+            radius = numpy_norm(center - p1) + (self.buf_val / 2)
+            start_angle = np.arctan2(p1[1] - center[1], p1[0] - center[0])
+            stop_angle = np.arctan2(p2[1] - center[1], p2[0] - center[0])
+
+            new_geo_el['solid'] = Polygon(
+                arc(center, radius, start_angle, stop_angle, self.direction, self.steps_per_circ))
+            new_geo_el['follow'] = Polygon(
+                arc(center, radius, start_angle, stop_angle, self.direction, self.steps_per_circ)).exterior
+            self.geometry = DrawToolShape(new_geo_el)
+
+        self.draw_app.in_action = False
+        self.complete = True
+
+        self.draw_app.app.jump_signal.disconnect()
+
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+
+class ScaleEditorGrb(ShapeToolEditorGrb):
+    def __init__(self, draw_app):
+        ShapeToolEditorGrb.__init__(self, draw_app)
+        self.name = 'scale'
+
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.draw_app = draw_app
+        self.app = draw_app.app
+
+        self.draw_app.app.inform.emit(_("Scale the selected Gerber apertures ..."))
+        self.origin = (0, 0)
+
+        if self.draw_app.app.ui.splitter.sizes()[0] == 0:
+            self.draw_app.app.ui.splitter.setSizes([1, 1])
+        self.activate_scale()
+
+    def activate_scale(self):
+        self.draw_app.hide_tool('all')
+        self.draw_app.ui.scale_tool_frame.show()
+
+        try:
+            self.draw_app.ui.scale_button.clicked.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        self.draw_app.ui.scale_button.clicked.connect(self.on_scale_click)
+
+    def deactivate_scale(self):
+        self.draw_app.ui.scale_button.clicked.disconnect()
+        self.complete = True
+        self.draw_app.select_tool("select")
+        self.draw_app.hide_tool(self.name)
+
+    def on_scale_click(self):
+        self.draw_app.on_scale()
+        self.deactivate_scale()
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+
+
+class BufferEditorGrb(ShapeToolEditorGrb):
+    def __init__(self, draw_app):
+        ShapeToolEditorGrb.__init__(self, draw_app)
+        self.name = 'buffer'
+
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.draw_app = draw_app
+        self.app = draw_app.app
+
+        self.draw_app.app.inform.emit(_("Buffer the selected apertures ..."))
+        self.origin = (0, 0)
+
+        if self.draw_app.app.ui.splitter.sizes()[0] == 0:
+            self.draw_app.app.ui.splitter.setSizes([1, 1])
+        self.activate_buffer()
+
+    def activate_buffer(self):
+        self.draw_app.hide_tool('all')
+        self.draw_app.ui.buffer_tool_frame.show()
+
+        try:
+            self.draw_app.ui.buffer_button.clicked.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        self.draw_app.ui.buffer_button.clicked.connect(self.on_buffer_click)
+
+    def deactivate_buffer(self):
+        self.draw_app.ui.buffer_button.clicked.disconnect()
+        self.complete = True
+        self.draw_app.select_tool("select")
+        self.draw_app.hide_tool(self.name)
+
+    def on_buffer_click(self):
+        self.draw_app.on_buffer()
+        self.deactivate_buffer()
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+
+
+class MarkEditorGrb(ShapeToolEditorGrb):
+    def __init__(self, draw_app):
+        ShapeToolEditorGrb.__init__(self, draw_app)
+        self.name = 'markarea'
+
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.draw_app = draw_app
+        self.app = draw_app.app
+
+        self.draw_app.app.inform.emit(_("Mark polygon areas in the edited Gerber ..."))
+        self.origin = (0, 0)
+
+        if self.draw_app.app.ui.splitter.sizes()[0] == 0:
+            self.draw_app.app.ui.splitter.setSizes([1, 1])
+        self.activate_markarea()
+
+    def activate_markarea(self):
+        self.draw_app.ui.ma_tool_frame.show()
+
+        # clear previous marking
+        self.draw_app.ma_annotation.clear(update=True)
+
+        try:
+            self.draw_app.ui.ma_threshold_button.clicked.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        self.draw_app.ui.ma_threshold_button.clicked.connect(self.on_markarea_click)
+
+        try:
+            self.draw_app.ui.ma_delete_button.clicked.disconnect()
+        except TypeError:
+            pass
+        self.draw_app.ui.ma_delete_button.clicked.connect(self.on_markarea_delete)
+
+        try:
+            self.draw_app.ui.ma_clear_button.clicked.disconnect()
+        except TypeError:
+            pass
+        self.draw_app.ui.ma_clear_button.clicked.connect(self.on_markarea_clear)
+
+    def deactivate_markarea(self):
+        self.draw_app.ui.ma_threshold_button.clicked.disconnect()
+        self.complete = True
+        self.draw_app.select_tool("select")
+        self.draw_app.hide_tool(self.name)
+
+    def on_markarea_click(self):
+        self.draw_app.on_markarea()
+
+    def on_markarea_clear(self):
+        self.draw_app.ma_annotation.clear(update=True)
+        self.deactivate_markarea()
+
+    def on_markarea_delete(self):
+        self.draw_app.delete_marked_polygons()
+        self.on_markarea_clear()
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+
+
+class MoveEditorGrb(ShapeToolEditorGrb):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'move'
+
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.origin = None
+        self.destination = None
+        self.selected_apertures = []
+
+        if len(self.draw_app.get_selected()) == 0:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s...' %
+                                          _("Nothing selected to move"))
+            self.complete = True
+            self.draw_app.select_tool("select")
+            return
+
+        if self.draw_app.launched_from_shortcuts is True:
+            self.draw_app.launched_from_shortcuts = False
+            self.draw_app.app.inform.emit(_("Click on target location ..."))
+        else:
+            self.draw_app.app.inform.emit(_("Click on reference location ..."))
+
+        self.current_storage = None
+        self.geometry = []
+
+        for index in self.draw_app.ui.apertures_table.selectedIndexes():
+            row = index.row()
+            # on column 1 in tool tables we hold the aperture codes, and we retrieve them as strings
+            aperture_on_row = self.draw_app.ui.apertures_table.item(row, 1).text()
+            self.selected_apertures.append(aperture_on_row)
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        self.sel_limit = self.draw_app.app.defaults["gerber_editor_sel_limit"]
+        self.selection_shape = self.selection_bbox()
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def click(self, point):
+        if len(self.draw_app.get_selected()) == 0:
+            return "Nothing to move."
+
+        if self.origin is None:
+            self.set_origin(point)
+            self.draw_app.app.inform.emit(_("Click on target location ..."))
+            return
+        else:
+            self.destination = point
+            self.make()
+
+            # MS: always return to the Select Tool
+            self.draw_app.select_tool("select")
+            return
+
+    # def create_png(self):
+    #     """
+    #     Create a PNG file out of a list of Shapely polygons
+    #     :return:
+    #     """
+    #     if len(self.draw_app.get_selected()) == 0:
+    #         return None
+    #
+    #     geo_list = [geoms.geo for geoms in self.draw_app.get_selected()]
+    #     xmin, ymin, xmax, ymax = get_shapely_list_bounds(geo_list)
+    #
+    #     iwidth = (xmax - xmin)
+    #     iwidth = int(round(iwidth))
+    #     iheight = (ymax - ymin)
+    #     iheight = int(round(iheight))
+    #     c = pngcanvas.PNGCanvas(iwidth, iheight)
+    #
+    #     pixels = []
+    #     for geom in self.draw_app.get_selected():
+    #         m = mapping(geom.geo.exterior)
+    #         pixels += [[coord[0], coord[1]] for coord in m['coordinates']]
+    #         for g in geom.geo.interiors:
+    #             m = mapping(g)
+    #             pixels += [[coord[0], coord[1]] for coord in m['coordinates']]
+    #         c.polyline(pixels)
+    #         pixels = []
+    #
+    #     f = open("%s.png" % 'D:\\shapely_image', "wb")
+    #     f.write(c.dump())
+    #     f.close()
+
+    def selection_bbox(self):
+        geo_list = []
+
+        for select_shape in self.draw_app.get_selected():
+            geometric_data = select_shape.geo
+            geo_list.append(geometric_data['solid'])
+
+        xmin, ymin, xmax, ymax = get_shapely_list_bounds(geo_list)
+
+        pt1 = (xmin, ymin)
+        pt2 = (xmax, ymin)
+        pt3 = (xmax, ymax)
+        pt4 = (xmin, ymax)
+
+        return Polygon([pt1, pt2, pt3, pt4])
+
+    def make(self):
+        # Create new geometry
+        dx = self.destination[0] - self.origin[0]
+        dy = self.destination[1] - self.origin[1]
+        sel_shapes_to_be_deleted = []
+
+        for sel_dia in self.selected_apertures:
+            self.current_storage = self.draw_app.storage_dict[sel_dia]['geometry']
+            for select_shape in self.draw_app.get_selected():
+                if select_shape in self.current_storage:
+                    geometric_data = select_shape.geo
+                    new_geo_el = {}
+                    if 'solid' in geometric_data:
+                        new_geo_el['solid'] = affinity.translate(geometric_data['solid'], xoff=dx, yoff=dy)
+                    if 'follow' in geometric_data:
+                        new_geo_el['follow'] = affinity.translate(geometric_data['follow'], xoff=dx, yoff=dy)
+                    if 'clear' in geometric_data:
+                        new_geo_el['clear'] = affinity.translate(geometric_data['clear'], xoff=dx, yoff=dy)
+
+                    self.geometry.append(DrawToolShape(new_geo_el))
+                    self.current_storage.remove(select_shape)
+                    sel_shapes_to_be_deleted.append(select_shape)
+                    self.draw_app.on_grb_shape_complete(self.current_storage, no_plot=True)
+                    self.geometry = []
+
+            for shp in sel_shapes_to_be_deleted:
+                self.draw_app.selected.remove(shp)
+            sel_shapes_to_be_deleted = []
+
+        self.draw_app.plot_all()
+        self.draw_app.build_ui()
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        self.draw_app.app.jump_signal.disconnect()
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+    def utility_geometry(self, data=None):
+        """
+        Temporary geometry on screen while using this tool.
+
+        :param data:
+        :return:
+        """
+        geo_list = []
+
+        if self.origin is None:
+            return None
+
+        if len(self.draw_app.get_selected()) == 0:
+            return None
+
+        dx = data[0] - self.origin[0]
+        dy = data[1] - self.origin[1]
+
+        if len(self.draw_app.get_selected()) <= self.sel_limit:
+            for geom in self.draw_app.get_selected():
+                new_geo_el = {}
+                if 'solid' in geom.geo:
+                    new_geo_el['solid'] = affinity.translate(geom.geo['solid'], xoff=dx, yoff=dy)
+                if 'follow' in geom.geo:
+                    new_geo_el['follow'] = affinity.translate(geom.geo['follow'], xoff=dx, yoff=dy)
+                if 'clear' in geom.geo:
+                    new_geo_el['clear'] = affinity.translate(geom.geo['clear'], xoff=dx, yoff=dy)
+                geo_list.append(deepcopy(new_geo_el))
+            return DrawToolUtilityShape(geo_list)
+        else:
+            ss_el = {'solid': affinity.translate(self.selection_shape, xoff=dx, yoff=dy)}
+            return DrawToolUtilityShape(ss_el)
+
+
+class CopyEditorGrb(MoveEditorGrb):
+    def __init__(self, draw_app):
+        MoveEditorGrb.__init__(self, draw_app)
+        self.name = 'copy'
+
+    def make(self):
+        # Create new geometry
+        dx = self.destination[0] - self.origin[0]
+        dy = self.destination[1] - self.origin[1]
+        sel_shapes_to_be_deleted = []
+
+        for sel_dia in self.selected_apertures:
+            self.current_storage = self.draw_app.storage_dict[sel_dia]['geometry']
+            for select_shape in self.draw_app.get_selected():
+                if select_shape in self.current_storage:
+                    geometric_data = select_shape.geo
+                    new_geo_el = {}
+                    if 'solid' in geometric_data:
+                        new_geo_el['solid'] = affinity.translate(geometric_data['solid'], xoff=dx, yoff=dy)
+                    if 'follow' in geometric_data:
+                        new_geo_el['follow'] = affinity.translate(geometric_data['follow'], xoff=dx, yoff=dy)
+                    if 'clear' in geometric_data:
+                        new_geo_el['clear'] = affinity.translate(geometric_data['clear'], xoff=dx, yoff=dy)
+                    self.geometry.append(DrawToolShape(new_geo_el))
+
+                    sel_shapes_to_be_deleted.append(select_shape)
+                    self.draw_app.on_grb_shape_complete(self.current_storage)
+                    self.geometry = []
+
+            for shp in sel_shapes_to_be_deleted:
+                self.draw_app.selected.remove(shp)
+            sel_shapes_to_be_deleted = []
+
+        self.draw_app.build_ui()
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        self.draw_app.app.jump_signal.disconnect()
+
+
+class EraserEditorGrb(ShapeToolEditorGrb):
+    def __init__(self, draw_app):
+        DrawTool.__init__(self, draw_app)
+        self.name = 'eraser'
+
+        self.origin = None
+        self.destination = None
+        self.selected_apertures = []
+
+        if len(self.draw_app.get_selected()) == 0:
+            if self.draw_app.launched_from_shortcuts is True:
+                self.draw_app.launched_from_shortcuts = False
+                self.draw_app.app.inform.emit(_("Select a shape to act as deletion area ..."))
+        else:
+            self.draw_app.app.inform.emit(_("Click to pick-up the erase shape..."))
+
+        self.current_storage = None
+        self.geometry = []
+
+        for index in self.draw_app.ui.apertures_table.selectedIndexes():
+            row = index.row()
+            # on column 1 in tool tables we hold the aperture codes, and we retrieve them as strings
+            aperture_on_row = self.draw_app.ui.apertures_table.item(row, 1).text()
+            self.selected_apertures.append(aperture_on_row)
+
+        # Switch notebook to Properties page
+        self.draw_app.app.ui.notebook.setCurrentWidget(self.draw_app.app.ui.properties_tab)
+
+        self.draw_app.app.jump_signal.connect(lambda x: self.draw_app.update_utility_geometry(data=x))
+
+        self.sel_limit = self.draw_app.app.defaults["gerber_editor_sel_limit"]
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def click(self, point):
+        if len(self.draw_app.get_selected()) == 0:
+            self.draw_app.ui.apertures_table.clearSelection()
+            sel_aperture = set()
+
+            for storage in self.draw_app.storage_dict:
+                try:
+                    for geo_el in self.draw_app.storage_dict[storage]['geometry']:
+                        if 'solid' in geo_el.geo:
+                            geometric_data = geo_el.geo['solid']
+                            if Point(point).within(geometric_data):
+                                self.draw_app.selected = []
+                                self.draw_app.selected.append(geo_el)
+                                sel_aperture.add(storage)
+                except KeyError:
+                    pass
+
+            # select the aperture in the Apertures Table that is associated with the selected shape
+            try:
+                self.draw_app.ui.apertures_table.cellPressed.disconnect()
+            except Exception as e:
+                log.debug("AppGerberEditor.EraserEditorGrb.click_release() --> %s" % str(e))
+
+            self.draw_app.ui.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
+            for aper in sel_aperture:
+                for row in range(self.draw_app.ui.apertures_table.rowCount()):
+                    if str(aper) == self.draw_app.ui.apertures_table.item(row, 1).text():
+                        self.draw_app.ui.apertures_table.selectRow(row)
+                        self.draw_app.last_aperture_selected = aper
+            self.draw_app.ui.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+
+            self.draw_app.ui.apertures_table.cellPressed.connect(self.draw_app.on_row_selected)
+
+        if len(self.draw_app.get_selected()) == 0:
+            return "Nothing to ersase."
+
+        if self.origin is None:
+            self.set_origin(point)
+            self.draw_app.app.inform.emit(_("Click to erase ..."))
+            return
+        else:
+            self.destination = point
+            self.make()
+
+            # self.draw_app.select_tool("select")
+            return
+
+    def make(self):
+        eraser_sel_shapes = []
+
+        # create the eraser shape from selection
+        for eraser_shape in self.utility_geometry(data=self.destination).geo:
+            temp_shape = eraser_shape['solid'].buffer(0.0000001)
+            temp_shape = Polygon(temp_shape.exterior)
+            eraser_sel_shapes.append(temp_shape)
+        eraser_sel_shapes = unary_union(eraser_sel_shapes)
+
+        for storage in self.draw_app.storage_dict:
+            try:
+                for geo_el in self.draw_app.storage_dict[storage]['geometry']:
+                    if 'solid' in geo_el.geo:
+                        geometric_data = geo_el.geo['solid']
+                        if eraser_sel_shapes.within(geometric_data) or eraser_sel_shapes.intersects(geometric_data):
+                            geos = geometric_data.difference(eraser_sel_shapes)
+                            geos = geos.buffer(0)
+                            geo_el.geo['solid'] = deepcopy(geos)
+            except KeyError:
+                pass
+
+        self.draw_app.delete_utility_geometry()
+        self.draw_app.plot_all()
+        self.draw_app.app.inform.emit('[success] %s' % _("Done."))
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except TypeError:
+            pass
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.ui.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+    def utility_geometry(self, data=None):
+        """
+        Temporary geometry on screen while using this tool.
+
+        :param data:
+        :return:
+        """
+        geo_list = []
+
+        if self.origin is None:
+            return None
+
+        if len(self.draw_app.get_selected()) == 0:
+            return None
+
+        dx = data[0] - self.origin[0]
+        dy = data[1] - self.origin[1]
+
+        for geom in self.draw_app.get_selected():
+            new_geo_el = {}
+            if 'solid' in geom.geo:
+                new_geo_el['solid'] = affinity.translate(geom.geo['solid'], xoff=dx, yoff=dy)
+            if 'follow' in geom.geo:
+                new_geo_el['follow'] = affinity.translate(geom.geo['follow'], xoff=dx, yoff=dy)
+            if 'clear' in geom.geo:
+                new_geo_el['clear'] = affinity.translate(geom.geo['clear'], xoff=dx, yoff=dy)
+            geo_list.append(deepcopy(new_geo_el))
+        return DrawToolUtilityShape(geo_list)
+
+
+class SelectEditorGrb(QtCore.QObject, DrawTool):
+    selection_triggered = QtCore.pyqtSignal(object)
+
+    def __init__(self, draw_app):
+        super().__init__(draw_app=draw_app)
+        # DrawTool.__init__(self, draw_app)
+        self.name = 'select'
+        self.origin = None
+
+        self.draw_app = draw_app
+        self.storage = self.draw_app.storage_dict
+        # self.selected = self.draw_app.selected
+
+        # here we store all shapes that were selected
+        self.sel_storage = []
+
+        # since SelectEditorGrb tool is activated whenever a tool is exited I place here the reinitialization of the
+        # bending modes using in RegionEditorGrb and TrackEditorGrb
+        self.draw_app.bend_mode = 1
+
+        # here store the selected apertures
+        self.sel_aperture = []
+
+        # multiprocessing results
+        self.results = []
+
+        try:
+            self.draw_app.ui.apertures_table.clearSelection()
+        except Exception as e:
+            log.error("FlatCAMGerbEditor.SelectEditorGrb.__init__() --> %s" % str(e))
+
+        self.draw_app.hide_tool('all')
+        self.draw_app.hide_tool('select')
+        self.draw_app.ui.array_frame.hide()
+
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception as e:
+            log.debug("AppGerberEditor.SelectEditorGrb --> %s" % str(e))
+
+        try:
+            self.selection_triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        self.selection_triggered.connect(self.selection_worker)
+
+        try:
+            self.draw_app.plot_object.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        self.draw_app.plot_object.connect(self.after_selection)
+
+    def set_origin(self, origin):
+        self.origin = origin
+
+    def click(self, point):
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if key_modifier == QtCore.Qt.ShiftModifier:
+            mod_key = 'Shift'
+        elif key_modifier == QtCore.Qt.ControlModifier:
+            mod_key = 'Control'
+        else:
+            mod_key = None
+
+        if mod_key == self.draw_app.app.defaults["global_mselect_key"]:
+            pass
+        else:
+            self.draw_app.selected = []
+
+    def click_release(self, point):
+        self.draw_app.ui.apertures_table.clearSelection()
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if key_modifier == QtCore.Qt.ShiftModifier:
+            mod_key = 'Shift'
+        elif key_modifier == QtCore.Qt.ControlModifier:
+            mod_key = 'Control'
+        else:
+            mod_key = None
+
+        if mod_key != self.draw_app.app.defaults["global_mselect_key"]:
+            self.draw_app.selected.clear()
+            self.sel_aperture.clear()
+
+        self.selection_triggered.emit(point)
+
+    def selection_worker(self, point):
+        def job_thread(editor_obj):
+            self.results = []
+            with editor_obj.app.proc_container.new('%s' % _("Working ...")):
+
+                def divide_chunks(lst, n):
+                    # looping till length of lst
+                    for i in range(0, len(lst), n):
+                        yield lst[i:i + n]
+
+                # divide in chunks of 77 elements
+                n_chunks = 77
+
+                for ap_key, storage_val in editor_obj.storage_dict.items():
+                    # divide in chunks of 77 elements
+                    geo_list = list(divide_chunks(storage_val['geometry'], n_chunks))
+                    for chunk, list30 in enumerate(geo_list):
+                        self.results.append(
+                            editor_obj.pool.apply_async(
+                                self.check_intersection, args=(ap_key, chunk, list30, point))
+                        )
+
+                output = []
+                for p in self.results:
+                    output.append(p.get())
+
+                for ret_val in output:
+                    if ret_val:
+                        k = ret_val[0]
+                        part = ret_val[1]
+                        idx = ret_val[2] + (part * n_chunks)
+                        shape_stored = editor_obj.storage_dict[k]['geometry'][idx]
+
+                        if shape_stored in editor_obj.selected:
+                            editor_obj.selected.remove(shape_stored)
+                        else:
+                            # add the object to the selected shapes
+                            editor_obj.selected.append(shape_stored)
+
+                editor_obj.plot_object.emit(None)
+
+        self.draw_app.app.worker_task.emit({'fcn': job_thread, 'params': [self.draw_app]})
+
+    @staticmethod
+    def check_intersection(ap_key, chunk, geo_storage, point):
+        for idx, shape_stored in enumerate(geo_storage):
+            if 'solid' in shape_stored.geo:
+                geometric_data = shape_stored.geo['solid']
+                if Point(point).intersects(geometric_data):
+                    return ap_key, chunk, idx
+
+    def after_selection(self):
+        # ######################################################################################################
+        # select the aperture in the Apertures Table that is associated with the selected shape
+        # ######################################################################################################
+        self.sel_aperture.clear()
+        self.draw_app.ui.apertures_table.clearSelection()
+
+        # disconnect signal when clicking in the table
+        try:
+            self.draw_app.ui.apertures_table.cellPressed.disconnect()
+        except Exception as e:
+            log.debug("AppGerberEditor.SelectEditorGrb.click_release() --> %s" % str(e))
+
+        brake_flag = False
+        for shape_s in self.draw_app.selected:
+            for storage in self.draw_app.storage_dict:
+                if shape_s in self.draw_app.storage_dict[storage]['geometry']:
+                    self.sel_aperture.append(storage)
+                    brake_flag = True
+                    break
+            if brake_flag is True:
+                break
+
+        # actual row selection is done here
+        for aper in self.sel_aperture:
+            for row in range(self.draw_app.ui.apertures_table.rowCount()):
+                if str(aper) == self.draw_app.ui.apertures_table.item(row, 1).text():
+                    if not self.draw_app.ui.apertures_table.item(row, 0).isSelected():
+                        self.draw_app.ui.apertures_table.selectRow(row)
+                        self.draw_app.last_aperture_selected = aper
+
+        # reconnect signal when clicking in the table
+        self.draw_app.ui.apertures_table.cellPressed.connect(self.draw_app.on_row_selected)
+
+        # and plot all
+        self.draw_app.plot_all()
+
+    def clean_up(self):
+        self.draw_app.plot_all()
+
+
+class TransformEditorGrb(ShapeToolEditorGrb):
+    def __init__(self, draw_app):
+        ShapeToolEditorGrb.__init__(self, draw_app)
+        self.name = 'transformation'
+
+        # self.shape_buffer = self.draw_app.shape_buffer
+        self.draw_app = draw_app
+        self.app = draw_app.app
+
+        self.start_msg = _("Shape transformations ...")
+        self.origin = (0, 0)
+        self.draw_app.transform_tool.run()
+
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+
+
+class AppGerberEditor(QtCore.QObject):
+
+    draw_shape_idx = -1
+    # plot_finished = QtCore.pyqtSignal()
+    mp_finished = QtCore.pyqtSignal(list)
+
+    plot_object = QtCore.pyqtSignal(object)
+
+    def __init__(self, app):
+        # assert isinstance(app, FlatCAMApp.App), \
+        #     "Expected the app to be a FlatCAMApp.App, got %s" % type(app)
+
+        super(AppGerberEditor, self).__init__()
+
+        self.app = app
+        self.canvas = self.app.plotcanvas
+        self.decimals = self.app.decimals
+
+        # Current application units in Upper Case
+        self.units = self.app.defaults['units'].upper()
+
+        self.ui = AppGerberEditorUI(self.app)
+
+        # Toolbar events and properties
+        self.tools_gerber = {}
+
+        # # ## Data
+        self.active_tool = None
+
+        self.storage_dict = {}
+        self.current_storage = []
+
+        self.sorted_apcode = []
+
+        self.new_apertures = {}
+        self.new_aperture_macros = {}
+
+        # store here the plot promises, if empty the delayed plot will be activated
+        self.grb_plot_promises = []
+
+        # dictionary to store the tool_row and aperture codes in Tool_table
+        # it will be updated everytime self.build_ui() is called
+        self.oldapcode_newapcode = {}
+
+        self.tid2apcode = {}
+
+        # this will store the value for the last selected tool, for use after clicking on canvas when the selection
+        # is cleared but as a side effect also the selected tool is cleared
+        self.last_aperture_selected = None
+        self.utility = []
+
+        # this will store the polygons marked by mark are to be perhaps deleted
+        self.geo_to_delete = []
+
+        # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
+        self.launched_from_shortcuts = False
+
+        # this var will store the state of the toolbar before starting the editor
+        self.toolbar_old_state = False
+
+        # #############################################################################################################
+        # ######################### Init appGUI #######################################################################
+        # #############################################################################################################
+        self.ui.apdim_lbl.hide()
+        self.ui.apdim_entry.hide()
+
+        self.gerber_obj = None
+        self.gerber_obj_options = {}
+
+        # VisPy Visuals
+        if self.app.is_legacy is False:
+            self.shapes = self.canvas.new_shape_collection(layers=1)
+            self.tool_shape = self.canvas.new_shape_collection(layers=1)
+            self.ma_annotation = self.canvas.new_text_group()
+        else:
+            from appGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+            self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_grb_editor')
+            self.tool_shape = ShapeCollectionLegacy(obj=self, app=self.app, name='tool_shapes_grb_editor')
+            self.ma_annotation = ShapeCollectionLegacy(
+                obj=self,
+                app=self.app,
+                name='ma_anno_grb_editor',
+                annotation_job=True)
+
+        # Event signals disconnect id holders
+        self.mp = None
+        self.mm = None
+        self.mr = None
+
+        # Remove from scene
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
+
+        # List of selected geometric elements.
+        self.selected = []
+
+        self.key = None  # Currently pressed key
+        self.modifiers = None
+        self.x = None  # Current mouse cursor pos
+        self.y = None
+        # Current snapped mouse pos
+        self.snap_x = None
+        self.snap_y = None
+        self.pos = None
+
+        # used in RegionEditorGrb and TrackEditorGrb. Will store the bending mode
+        self.bend_mode = 1
+
+        # signal that there is an action active like polygon or path
+        self.in_action = False
+        # this will flag if the Editor "tools" are launched from key shortcuts (True) or from menu toolbar (False)
+        self.launched_from_shortcuts = False
+
+        def_tol_val = float(self.app.defaults["global_tolerance"])
+        self.tolerance = def_tol_val if self.units == 'MM'else def_tol_val / 20
+
+        # options of this widget (AppGerberEditor class is a widget)
+        self.options = {
+            "global_gridx":     0.1,
+            "global_gridy":     0.1,
+            "snap_max":         0.05,
+            "grid_snap":        True,
+            "corner_snap":      False,
+            "grid_gap_link":    True
+        }
+        # fill it with the application options (application preferences)
+        self.options.update(self.app.options)
+
+        for option in self.options:
+            if option in self.app.options:
+                self.options[option] = self.app.options[option]
+
+        # flag to show if the object was modified
+        self.is_modified = False
+        self.edited_obj_name = ""
+        self.tool_row = 0
+
+        # Multiprocessing pool
+        self.pool = self.app.pool
+
+        # Multiprocessing results
+        self.results = []
+
+        # A QTimer
+        self.plot_thread = None
+
+        # a QThread for the edit process
+        self.thread = QtCore.QThread()
+
+        # def entry2option(option, entry):
+        #     self.options[option] = float(entry.text())
+
+        self.transform_tool = TransformEditorTool(self.app, self)
+
+        # #############################################################################################################
+        # ######################### Gerber Editor Signals #############################################################
+        # #############################################################################################################
+        self.app.pool_recreated.connect(self.pool_recreated)
+        self.mp_finished.connect(self.on_multiprocessing_finished)
+
+        # connect the toolbar signals
+        self.connect_grb_toolbar_signals()
+
+        self.app.ui.grb_add_pad_menuitem.triggered.connect(self.on_pad_add)
+        self.app.ui.grb_add_pad_array_menuitem.triggered.connect(self.on_pad_add_array)
+
+        self.app.ui.grb_add_track_menuitem.triggered.connect(self.on_track_add)
+        self.app.ui.grb_add_region_menuitem.triggered.connect(self.on_region_add)
+
+        self.app.ui.grb_convert_poly_menuitem.triggered.connect(self.on_poligonize)
+        self.app.ui.grb_add_semidisc_menuitem.triggered.connect(self.on_add_semidisc)
+        self.app.ui.grb_add_disc_menuitem.triggered.connect(self.on_disc_add)
+        self.app.ui.grb_add_buffer_menuitem.triggered.connect(self.on_buffer)
+        self.app.ui.grb_add_scale_menuitem.triggered.connect(self.on_scale)
+        self.app.ui.grb_add_eraser_menuitem.triggered.connect(self.on_eraser)
+        self.app.ui.grb_add_markarea_menuitem.triggered.connect(self.on_markarea)
+
+        self.app.ui.grb_transform_menuitem.triggered.connect(self.transform_tool.run)
+
+        self.app.ui.grb_copy_menuitem.triggered.connect(self.on_copy_button)
+        self.app.ui.grb_delete_menuitem.triggered.connect(self.on_delete_btn)
+
+        self.app.ui.grb_move_menuitem.triggered.connect(self.on_move_button)
+
+        self.ui.buffer_button.clicked.connect(self.on_buffer)
+        self.ui.scale_button.clicked.connect(self.on_scale)
+
+        self.app.ui.aperture_delete_btn.triggered.connect(self.on_delete_btn)
+        self.ui.name_entry.returnPressed.connect(self.on_name_activate)
+
+        self.ui.aptype_cb.currentIndexChanged[str].connect(self.on_aptype_changed)
+
+        self.ui.addaperture_btn.clicked.connect(self.on_aperture_add)
+        self.ui.apsize_entry.returnPressed.connect(self.on_aperture_add)
+        self.ui.apdim_entry.returnPressed.connect(self.on_aperture_add)
+
+        self.ui.delaperture_btn.clicked.connect(self.on_aperture_delete)
+        self.ui.apertures_table.cellPressed.connect(self.on_row_selected)
+
+        self.ui.array_type_radio.activated_custom.connect(self.on_array_type_radio)
+        self.ui.pad_axis_radio.activated_custom.connect(self.on_linear_angle_radio)
+
+        self.ui.exit_editor_button.clicked.connect(lambda: self.app.editor2object())
+
+        self.conversion_factor = 1
+
+        self.apertures_row = 0
+
+        self.complete = True
+
+        self.set_ui()
+        log.debug("Initialization of the Gerber Editor is finished ...")
+
+    def make_callback(self, the_tool):
+        def f():
+            self.on_tool_select(the_tool)
+
+        return f
+
+    def connect_grb_toolbar_signals(self):
+        self.tools_gerber.update({
+            "select": {"button": self.app.ui.grb_select_btn,            "constructor": SelectEditorGrb},
+            "pad": {"button": self.app.ui.grb_add_pad_btn,              "constructor": PadEditorGrb},
+            "array": {"button": self.app.ui.add_pad_ar_btn,             "constructor": PadArrayEditorGrb},
+            "track": {"button": self.app.ui.grb_add_track_btn,          "constructor": TrackEditorGrb},
+            "region": {"button": self.app.ui.grb_add_region_btn,        "constructor": RegionEditorGrb},
+            "poligonize": {"button": self.app.ui.grb_convert_poly_btn,  "constructor": PoligonizeEditorGrb},
+            "semidisc": {"button": self.app.ui.grb_add_semidisc_btn,    "constructor": DiscSemiEditorGrb},
+            "disc": {"button": self.app.ui.grb_add_disc_btn,            "constructor": DiscEditorGrb},
+            "buffer": {"button": self.app.ui.aperture_buffer_btn,       "constructor": BufferEditorGrb},
+            "scale": {"button": self.app.ui.aperture_scale_btn,         "constructor": ScaleEditorGrb},
+            "markarea": {"button": self.app.ui.aperture_markarea_btn,   "constructor": MarkEditorGrb},
+            "eraser": {"button": self.app.ui.aperture_eraser_btn,       "constructor": EraserEditorGrb},
+            "copy": {"button": self.app.ui.aperture_copy_btn,           "constructor": CopyEditorGrb},
+            "transform": {"button": self.app.ui.grb_transform_btn,      "constructor": TransformEditorGrb},
+            "move": {"button": self.app.ui.aperture_move_btn,           "constructor": MoveEditorGrb},
+        })
+
+        for tool in self.tools_gerber:
+            self.tools_gerber[tool]["button"].triggered.connect(self.make_callback(tool))  # Events
+            self.tools_gerber[tool]["button"].setCheckable(True)
+
+    def pool_recreated(self, pool):
+        self.shapes.pool = pool
+        self.tool_shape.pool = pool
+        self.pool = pool
+
+    def set_ui(self):
+        # updated units
+        self.units = self.app.defaults['units'].upper()
+        self.decimals = self.app.decimals
+
+        self.oldapcode_newapcode.clear()
+        self.tid2apcode.clear()
+
+        # update the oldapcode_newapcode dict to make sure we have an updated state of the tool_table
+        for key in self.storage_dict:
+            self.oldapcode_newapcode[key] = key
+
+        sort_temp = []
+        for aperture in self.oldapcode_newapcode:
+            sort_temp.append(int(aperture))
+        self.sorted_apcode = sorted(sort_temp)
+
+        # populate self.intial_table_rows dict with the tool number as keys and aperture codes as values
+        for i in range(len(self.sorted_apcode)):
+            tt_aperture = self.sorted_apcode[i]
+            self.tid2apcode[i + 1] = tt_aperture
+
+        # #############################################################################################################
+        # Init appGUI
+        # #############################################################################################################
+        self.ui.buffer_distance_entry.set_value(self.app.defaults["gerber_editor_buff_f"])
+        self.ui.scale_factor_entry.set_value(self.app.defaults["gerber_editor_scale_f"])
+        self.ui.ma_upper_threshold_entry.set_value(self.app.defaults["gerber_editor_ma_high"])
+        self.ui.ma_lower_threshold_entry.set_value(self.app.defaults["gerber_editor_ma_low"])
+
+        self.ui.apsize_entry.set_value(self.app.defaults["gerber_editor_newsize"])
+        self.ui.aptype_cb.set_value(self.app.defaults["gerber_editor_newtype"])
+        self.ui.apdim_entry.set_value(self.app.defaults["gerber_editor_newdim"])
+
+        # PAD Array
+        self.ui.array_type_radio.set_value('linear')   # Linear
+        self.on_array_type_radio(val=self.ui.array_type_radio.get_value())
+        self.ui.pad_array_size_entry.set_value(int(self.app.defaults["gerber_editor_array_size"]))
+
+        # linear array
+        self.ui.pad_axis_radio.set_value('X')
+        self.on_linear_angle_radio(val=self.ui.pad_axis_radio.get_value())
+        self.ui.pad_axis_radio.set_value(self.app.defaults["gerber_editor_lin_axis"])
+        self.ui.pad_pitch_entry.set_value(float(self.app.defaults["gerber_editor_lin_pitch"]))
+        self.ui.linear_angle_spinner.set_value(self.app.defaults["gerber_editor_lin_angle"])
+
+        # circular array
+        self.ui.pad_direction_radio.set_value('CW')
+        self.ui.pad_direction_radio.set_value(self.app.defaults["gerber_editor_circ_dir"])
+        self.ui.pad_angle_entry.set_value(float(self.app.defaults["gerber_editor_circ_angle"]))
+
+    def build_ui(self, first_run=None):
+
+        try:
+            # if connected, disconnect the signal from the slot on item_changed as it creates issues
+            self.ui.apertures_table.itemChanged.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.ui.apertures_table.cellPressed.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        # updated units
+        self.units = self.app.defaults['units'].upper()
+
+        # make a new name for the new Excellon object (the one with edited content)
+        self.edited_obj_name = self.gerber_obj.options['name']
+        self.ui.name_entry.set_value(self.edited_obj_name)
+
+        self.apertures_row = 0
+        # aper_no = self.apertures_row + 1
+
+        sort = []
+        for k, v in list(self.storage_dict.items()):
+            sort.append(int(k))
+
+        sorted_apertures = sorted(sort)
+
+        # sort = []
+        # for k, v in list(self.gerber_obj.aperture_macros.items()):
+        #     sort.append(k)
+        # sorted_macros = sorted(sort)
+
+        # n = len(sorted_apertures) + len(sorted_macros)
+        n = len(sorted_apertures)
+        self.ui.apertures_table.setRowCount(n)
+
+        for ap_code in sorted_apertures:
+            ap_code = str(ap_code)
+
+            ap_code_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
+            ap_code_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.apertures_table.setItem(self.apertures_row, 0, ap_code_item)  # Tool name/id
+
+            ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
+            ap_code_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+            ap_type_item = QtWidgets.QTableWidgetItem(str(self.storage_dict[ap_code]['type']))
+            ap_type_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+            if str(self.storage_dict[ap_code]['type']) == 'R' or str(self.storage_dict[ap_code]['type']) == 'O':
+                ap_dim_item = QtWidgets.QTableWidgetItem(
+                    '%.*f, %.*f' % (self.decimals, self.storage_dict[ap_code]['width'],
+                                    self.decimals, self.storage_dict[ap_code]['height']
+                                    )
+                )
+                ap_dim_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
+            elif str(self.storage_dict[ap_code]['type']) == 'P':
+                ap_dim_item = QtWidgets.QTableWidgetItem(
+                    '%.*f, %.*f' % (self.decimals, self.storage_dict[ap_code]['diam'],
+                                    self.decimals, self.storage_dict[ap_code]['nVertices'])
+                )
+                ap_dim_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
+            else:
+                ap_dim_item = QtWidgets.QTableWidgetItem('')
+                ap_dim_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+            try:
+                if self.storage_dict[ap_code]['size'] is not None:
+                    ap_size_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals,
+                                                                        float(self.storage_dict[ap_code]['size'])))
+                else:
+                    ap_size_item = QtWidgets.QTableWidgetItem('')
+            except KeyError:
+                ap_size_item = QtWidgets.QTableWidgetItem('')
+
+            if str(self.storage_dict[ap_code]['type']) == 'C':
+                ap_size_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable)
+            else:
+                ap_size_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+            self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
+            self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
+            self.ui.apertures_table.setItem(self.apertures_row, 3, ap_size_item)  # Aperture Size
+            self.ui.apertures_table.setItem(self.apertures_row, 4, ap_dim_item)  # Aperture Dimensions
+
+            self.apertures_row += 1
+            if first_run is True:
+                # set now the last aperture selected
+                self.last_aperture_selected = ap_code
+
+        # for ap_code in sorted_macros:
+        #     ap_code = str(ap_code)
+        #
+        #     ap_code_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
+        #     ap_code_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        #     self.ui.apertures_table.setItem(self.apertures_row, 0, ap_code_item)  # Tool name/id
+        #
+        #     ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
+        #
+        #     ap_type_item = QtWidgets.QTableWidgetItem('AM')
+        #     ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
+        #
+        #     self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item)  # Aperture Code
+        #     self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item)  # Aperture Type
+        #
+        #     self.apertures_row += 1
+        #     if first_run is True:
+        #         # set now the last aperture selected
+        #         self.last_aperture_selected = ap_code
+
+        self.ui.apertures_table.selectColumn(0)
+        self.ui.apertures_table.resizeColumnsToContents()
+        self.ui.apertures_table.resizeRowsToContents()
+
+        vertical_header = self.ui.apertures_table.verticalHeader()
+        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        vertical_header.hide()
+        self.ui.apertures_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.apertures_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 27)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
+
+        self.ui.apertures_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.ui.apertures_table.setSortingEnabled(False)
+        self.ui.apertures_table.setMinimumHeight(self.ui.apertures_table.getHeight())
+        self.ui.apertures_table.setMaximumHeight(self.ui.apertures_table.getHeight())
+
+        # make sure no rows are selected so the user have to click the correct row, meaning selecting the correct tool
+        self.ui.apertures_table.clearSelection()
+
+        # Remove anything else in the GUI Properties Tab
+        self.app.ui.properties_scroll_area.takeWidget()
+        # Put ourselves in the GUI Properties Tab
+        self.app.ui.properties_scroll_area.setWidget(self.ui.grb_edit_widget)
+        # Switch notebook to Properties page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
+
+        # we reactivate the signals after the after the tool adding as we don't need to see the tool been populated
+        self.ui.apertures_table.itemChanged.connect(self.on_tool_edit)
+        self.ui.apertures_table.cellPressed.connect(self.on_row_selected)
+
+        # for convenience set the next aperture code in the apcode field
+        try:
+            self.ui.apcode_entry.set_value(max(self.tid2apcode.values()) + 1)
+        except ValueError:
+            # this means that the edited object has no apertures so we start with 10 (Gerber specifications)
+            self.ui.apcode_entry.set_value(self.app.defaults["gerber_editor_newcode"])
+
+    def on_aperture_add(self, apcode=None):
+        self.is_modified = True
+        if apcode:
+            ap_code = apcode
+        else:
+            try:
+                ap_code = str(self.ui.apcode_entry.get_value())
+            except ValueError:
+                self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                     _("Aperture code value is missing or wrong format. Add it and retry."))
+                return
+            if ap_code == '':
+                self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                     _("Aperture code value is missing or wrong format. Add it and retry."))
+                return
+
+        if ap_code == '0':
+            if ap_code not in self.tid2apcode:
+                self.storage_dict[ap_code] = {
+                    'type': 'REG',
+                    'size': 0.0,
+                    'geometry': []
+                }
+                self.ui.apsize_entry.set_value(0.0)
+
+                # self.oldapcode_newapcode dict keeps the evidence on current aperture codes as keys and
+                # gets updated on values each time a aperture code is edited or added
+                self.oldapcode_newapcode[ap_code] = ap_code
+        else:
+            if ap_code not in self.oldapcode_newapcode:
+                type_val = self.ui.aptype_cb.currentText()
+                if type_val == 'R' or type_val == 'O':
+                    try:
+                        dims = self.ui.apdim_entry.get_value()
+                        size_val = np.sqrt((dims[0] ** 2) + (dims[1] ** 2))
+
+                        self.storage_dict[ap_code] = {
+                            'type': type_val,
+                            'size': size_val,
+                            'width': dims[0],
+                            'height': dims[1],
+                            'geometry': []
+                        }
+
+                        self.ui.apsize_entry.set_value(size_val)
+
+                    except Exception as e:
+                        log.error("AppGerberEditor.on_aperture_add() --> the R or O aperture dims has to be in a "
+                                  "tuple format (x,y)\nError: %s" % str(e))
+                        self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                             _("Aperture dimensions value is missing or wrong format. "
+                                               "Add it in format (width, height) and retry."))
+                        return
+                else:
+                    try:
+                        size_val = float(self.ui.apsize_entry.get_value())
+                    except ValueError:
+                        # try to convert comma to decimal point. if it's still not working error message and return
+                        try:
+                            size_val = float(self.ui.apsize_entry.get_value().replace(',', '.'))
+                            self.ui.apsize_entry.set_value(size_val)
+                        except ValueError:
+                            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                                 _("Aperture size value is missing or wrong format. Add it and retry."))
+                            return
+
+                    self.storage_dict[ap_code] = {
+                        'type': type_val,
+                        'size': size_val,
+                        'geometry': []
+                    }
+
+                # self.oldapcode_newapcode dict keeps the evidence on current aperture codes as keys and gets updated on
+                # values  each time a aperture code is edited or added
+                self.oldapcode_newapcode[ap_code] = ap_code
+            else:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Aperture already in the aperture table."))
+                return
+
+        # since we add a new tool, we update also the initial state of the tool_table through it's dictionary
+        # we add a new entry in the tid2apcode dict
+        self.tid2apcode[len(self.oldapcode_newapcode)] = int(ap_code)
+
+        self.app.inform.emit('[success] %s: %s' % (_("Added new aperture with code"), str(ap_code)))
+
+        self.build_ui()
+
+        self.last_aperture_selected = ap_code
+
+        # make a quick sort through the tid2apcode dict so we find which row to select
+        row_to_be_selected = None
+        for key in sorted(self.tid2apcode):
+            if self.tid2apcode[key] == int(ap_code):
+                row_to_be_selected = int(key) - 1
+                break
+        self.ui.apertures_table.selectRow(row_to_be_selected)
+
+    def on_aperture_delete(self, ap_code=None):
+        """
+        Called for aperture deletion.
+
+        :param ap_code:     An Aperture code; String
+        :return:
+        """
+        self.is_modified = True
+
+        try:
+            if ap_code:
+                try:
+                    deleted_apcode_list = [dd for dd in ap_code]
+                except TypeError:
+                    deleted_apcode_list = [ap_code]
+            else:
+                # deleted_tool_dia = float(self.ui.apertures_table.item(self.ui.apertures_table.currentRow(), 1).text())
+                if len(self.ui.apertures_table.selectionModel().selectedRows()) == 0:
+                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("Select an aperture in Aperture Table"))
+                    return
+
+                deleted_apcode_list = []
+                for index in self.ui.apertures_table.selectionModel().selectedRows():
+                    row = index.row()
+                    deleted_apcode_list.append(self.ui.apertures_table.item(row, 1).text())
+        except Exception as exc:
+            self.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Select an aperture in Aperture Table -->", str(exc))))
+            return
+
+        if deleted_apcode_list:
+            for deleted_aperture in deleted_apcode_list:
+                # delete the storage used for that tool
+                self.storage_dict.pop(deleted_aperture, None)
+
+                for deleted_tool in list(self.tid2apcode.keys()):
+                    if self.tid2apcode[deleted_tool] == deleted_aperture:
+                        # delete the tool
+                        self.tid2apcode.pop(deleted_tool, None)
+
+                self.oldapcode_newapcode.pop(deleted_aperture, None)
+                self.app.inform.emit('[success] %s: %s' % (_("Deleted aperture with code"), str(deleted_aperture)))
+
+        self.plot_all()
+        self.build_ui()
+
+        # if last aperture selected was in the apertures deleted than make sure to select a
+        # 'new' last aperture selected because there are tools who depend on it.
+        # if there is no aperture left, then add a default one :)
+        if self.last_aperture_selected in deleted_apcode_list:
+            if self.ui.apertures_table.rowCount() == 0:
+                self.on_aperture_add('10')
+                self.last_aperture_selected = '10'
+            else:
+                self.last_aperture_selected = self.ui.apertures_table.item(0, 1).text()
+
+    def on_tool_edit(self):
+        if self.ui.apertures_table.currentItem() is None:
+            return
+
+        # if connected, disconnect the signal from the slot on item_changed as it creates issues
+        self.ui.apertures_table.itemChanged.disconnect()
+        # self.ui.apertures_table.cellPressed.disconnect()
+
+        self.is_modified = True
+        val_edited = None
+
+        row_of_item_changed = self.ui.apertures_table.currentRow()
+        col_of_item_changed = self.ui.apertures_table.currentColumn()
+
+        # rows start with 0, tools start with 1 so we adjust the value by 1
+        key_in_tid2apcode = row_of_item_changed + 1
+        ap_code_old = str(self.tid2apcode[key_in_tid2apcode])
+
+        ap_code_new = self.ui.apertures_table.item(row_of_item_changed, 1).text()
+
+        if col_of_item_changed == 1:
+            # we edited the Aperture Code column (int)
+            try:
+                val_edited = int(self.ui.apertures_table.currentItem().text())
+            except ValueError as e:
+                log.debug("AppGerberEditor.on_tool_edit() --> %s" % str(e))
+                # self.ui.apertures_table.setCurrentItem(None)
+                # we reactivate the signals after the after the tool editing
+                self.ui.apertures_table.itemChanged.connect(self.on_tool_edit)
+                return
+        elif col_of_item_changed == 3:
+            # we edited the Size column (float)
+            try:
+                val_edited = float(self.ui.apertures_table.currentItem().text())
+            except ValueError as e:
+                log.debug("AppGerberEditor.on_tool_edit() --> %s" % str(e))
+                # self.ui.apertures_table.setCurrentItem(None)
+                # we reactivate the signals after the after the tool editing
+                self.ui.apertures_table.itemChanged.connect(self.on_tool_edit)
+                return
+        elif col_of_item_changed == 4:
+            # we edit the Dimensions column (tuple)
+            try:
+                val_edited = [
+                    float(x.strip()) for x in self.ui.apertures_table.currentItem().text().split(",") if x != ''
+                ]
+            except ValueError as e:
+                log.debug("AppGerberEditor.on_tool_edit() --> %s" % str(e))
+                # we reactivate the signals after the after the tool editing
+                self.ui.apertures_table.itemChanged.connect(self.on_tool_edit)
+                return
+
+            if len(val_edited) != 2:
+                self.app.inform.emit("[WARNING_NOTCL] %s" % _("Dimensions need two float values separated by comma."))
+                old_dims_txt = '%s, %s' % (str(self.storage_dict[ap_code_new]['width']),
+                                           str(self.storage_dict[ap_code_new]['height']))
+
+                self.ui.apertures_table.currentItem().setText(old_dims_txt)
+                # we reactivate the signals after the after the tool editing
+                self.ui.apertures_table.itemChanged.connect(self.on_tool_edit)
+                return
+            else:
+                self.app.inform.emit("[success] %s" % _("Dimensions edited."))
+
+        # In case we edited the Aperture Code therefore the val_edited holds a new Aperture Code
+        # TODO Edit of the Aperture Code is not active yet
+        if col_of_item_changed == 1:
+            # aperture code is not used so we create a new Aperture with the desired Aperture Code
+            if val_edited not in self.oldapcode_newapcode.values():
+                # update the dict that holds as keys old Aperture Codes and as values the new Aperture Codes
+                self.oldapcode_newapcode[ap_code_old] = val_edited
+                # update the dict that holds tool_no as key and tool_dia as value
+                self.tid2apcode[key_in_tid2apcode] = val_edited
+
+                old_aperture_val = self.storage_dict.pop(ap_code_old)
+                self.storage_dict[val_edited] = old_aperture_val
+
+            else:
+                # aperture code is already in use so we move the pads from the prior tool to the new tool
+                # but only if they are of the same type
+
+                if self.storage_dict[ap_code_old]['type'] == self.storage_dict[ap_code_new]['type']:
+                    # TODO I have to work here; if type == 'R' or 'O' have t otake care of all attributes ...
+                    factor = val_edited / float(ap_code_old)
+                    geometry = []
+                    for geo_el in self.storage_dict[ap_code_old]:
+                        geometric_data = geo_el.geo
+                        new_geo_el = {}
+                        if 'solid' in geometric_data:
+                            new_geo_el['solid'] = deepcopy(affinity.scale(geometric_data['solid'],
+                                                                          xfact=factor, yfact=factor))
+                        if 'follow' in geometric_data:
+                            new_geo_el['follow'] = deepcopy(affinity.scale(geometric_data['follow'],
+                                                                           xfact=factor, yfact=factor))
+                        if 'clear' in geometric_data:
+                            new_geo_el['clear'] = deepcopy(affinity.scale(geometric_data['clear'],
+                                                                          xfact=factor, yfact=factor))
+                        geometry.append(new_geo_el)
+
+                    self.add_gerber_shape(geometry, self.storage_dict[val_edited])
+
+                    self.on_aperture_delete(apcode=ap_code_old)
+
+        # In case we edited the Size of the Aperture therefore the val_edited holds the new Aperture Size
+        # It will happen only for the Aperture Type == 'C' - I make sure of that in the self.build_ui()
+        elif col_of_item_changed == 3:
+            old_size = float(self.storage_dict[ap_code_old]['size'])
+            new_size = float(val_edited)
+            adjust_size = (new_size - old_size) / 2
+            geometry = []
+            for geo_el in self.storage_dict[ap_code_old]['geometry']:
+                g_data = geo_el.geo
+                new_geo_el = {}
+                if 'solid' in g_data:
+                    if 'follow' in g_data:
+                        if isinstance(g_data['follow'], Point):
+                            new_geo_el['solid'] = deepcopy(g_data['solid'].buffer(adjust_size))
+                        else:
+                            new_geo_el['solid'] = deepcopy(g_data['solid'].buffer(adjust_size, join_style=2))
+                if 'follow' in g_data:
+                    new_geo_el['follow'] = deepcopy(g_data['follow'])
+                if 'clear' in g_data:
+                    new_geo_el['clear'] = deepcopy(g_data['clear'].buffer(adjust_size, join_style=2))
+                geometry.append(DrawToolShape(new_geo_el))
+
+            self.storage_dict[ap_code_old]['geometry'].clear()
+            self.add_gerber_shape(geometry, self.storage_dict[ap_code_old]['geometry'])
+            # self.storage_dict[ap_code_old]['geometry'] = geometry
+
+        # In case we edited the Dims of the Aperture therefore the val_edited holds a list with the dimensions
+        # in the format [width, height]
+        # It will happen only for the Aperture Type in ['R', 'O'] - I make sure of that in the self.build_ui()
+        # and below
+        elif col_of_item_changed == 4:
+            if str(self.storage_dict[ap_code_old]['type']) == 'R' or str(self.storage_dict[ap_code_old]['type']) == 'O':
+                # use the biggest from them
+                buff_val_lines = max(val_edited)
+                new_width = val_edited[0]
+                new_height = val_edited[1]
+
+                geometry = []
+                for geo_el in self.storage_dict[ap_code_old]['geometry']:
+                    g_data = geo_el.geo
+                    new_geo_el = {}
+                    if 'solid' in g_data:
+                        if 'follow' in g_data:
+                            if isinstance(g_data['follow'], Point):
+                                x = g_data['follow'].x
+                                y = g_data['follow'].y
+                                minx = x - (new_width / 2)
+                                miny = y - (new_height / 2)
+                                maxx = x + (new_width / 2)
+                                maxy = y + (new_height / 2)
+                                geo = box(minx=minx, miny=miny, maxx=maxx, maxy=maxy)
+                                new_geo_el['solid'] = deepcopy(geo)
+                            else:
+                                new_geo_el['solid'] = deepcopy(g_data['solid'].buffer(buff_val_lines))
+                    if 'follow' in g_data:
+                        new_geo_el['follow'] = deepcopy(g_data['follow'])
+                    if 'clear' in g_data:
+                        if 'follow' in g_data:
+                            if isinstance(g_data['follow'], Point):
+                                x = g_data['follow'].x
+                                y = g_data['follow'].y
+                                minx = x - (new_width / 2)
+                                miny = y - (new_height / 2)
+                                maxx = x + (new_width / 2)
+                                maxy = y + (new_height / 2)
+                                geo = box(minx=minx, miny=miny, maxx=maxx, maxy=maxy)
+                                new_geo_el['clear'] = deepcopy(geo)
+                            else:
+                                new_geo_el['clear'] = deepcopy(g_data['clear'].buffer(buff_val_lines, join_style=2))
+                    geometry.append(DrawToolShape(new_geo_el))
+
+                self.storage_dict[ap_code_old]['geometry'].clear()
+                self.add_gerber_shape(geometry, self.storage_dict[ap_code_old]['geometry'])
+
+        self.plot_all()
+
+        # we reactivate the signals after the after the tool editing
+        self.ui.apertures_table.itemChanged.connect(self.on_tool_edit)
+        # self.ui.apertures_table.cellPressed.connect(self.on_row_selected)
+
+    def on_name_activate(self):
+        self.edited_obj_name = self.ui.name_entry.get_value()
+
+    def on_aptype_changed(self, current_text):
+        # 'O' is letter O not zero.
+        if current_text == 'R' or current_text == 'O':
+            self.ui.apdim_lbl.show()
+            self.ui.apdim_entry.show()
+            self.ui.apsize_entry.setDisabled(True)
+        else:
+            self.ui.apdim_lbl.hide()
+            self.ui.apdim_entry.hide()
+            self.ui.apsize_entry.setDisabled(False)
+
+    def activate_grb_editor(self):
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(True)
+        self.app.ui.menueditok.setDisabled(False)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(False)
+        self.app.ui.popmenu_save.setVisible(True)
+
+        self.connect_canvas_event_handlers()
+
+        # init working objects
+        self.storage_dict = {}
+        self.current_storage = []
+        self.sorted_apcode = []
+        self.new_apertures = {}
+        self.new_aperture_macros = {}
+        self.grb_plot_promises = []
+        self.oldapcode_newapcode = {}
+        self.tid2apcode = {}
+
+        self.shapes.enabled = True
+        self.tool_shape.enabled = True
+
+        self.app.ui.corner_snap_btn.setVisible(True)
+        self.app.ui.snap_magnet.setVisible(True)
+
+        self.app.ui.grb_editor_menu.setDisabled(False)
+        self.app.ui.grb_editor_menu.menuAction().setVisible(True)
+
+        self.app.ui.update_obj_btn.setEnabled(True)
+        self.app.ui.grb_editor_cmenu.setEnabled(True)
+
+        self.app.ui.grb_edit_toolbar.setDisabled(False)
+        self.app.ui.grb_edit_toolbar.setVisible(True)
+        # self.app.ui.grid_toolbar.setDisabled(False)
+
+        # start with GRID toolbar activated
+        if self.app.ui.grid_snap_btn.isChecked() is False:
+            self.app.ui.grid_snap_btn.trigger()
+
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(False)
+        self.app.ui.popmenu_save.setVisible(True)
+
+        self.app.ui.popmenu_disable.setVisible(False)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(False)
+        self.app.ui.popmenu_properties.setVisible(False)
+        self.app.ui.grb_editor_cmenu.menuAction().setVisible(True)
+
+    def deactivate_grb_editor(self):
+        try:
+            QtGui.QGuiApplication.restoreOverrideCursor()
+        except Exception as e:
+            log.debug("AppGerberEditor.deactivate_grb_editor() --> %s" % str(e))
+
+        self.clear()
+
+        # adjust the status of the menu entries related to the editor
+        self.app.ui.menueditedit.setDisabled(False)
+        self.app.ui.menueditok.setDisabled(True)
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(True)
+        self.app.ui.popmenu_save.setVisible(False)
+
+        self.disconnect_canvas_event_handlers()
+        self.app.ui.grb_edit_toolbar.setDisabled(True)
+
+        self.app.ui.corner_snap_btn.setVisible(False)
+        self.app.ui.snap_magnet.setVisible(False)
+
+        # set the Editor Toolbar visibility to what was before entering in the Editor
+        self.app.ui.grb_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \
+            else self.app.ui.grb_edit_toolbar.setVisible(True)
+
+        # Disable visuals
+        self.shapes.enabled = False
+        self.tool_shape.enabled = False
+        # self.app.app_cursor.enabled = False
+
+        self.app.ui.grb_editor_menu.setDisabled(True)
+        self.app.ui.grb_editor_menu.menuAction().setVisible(False)
+
+        self.app.ui.update_obj_btn.setEnabled(False)
+
+        # adjust the visibility of some of the canvas context menu
+        self.app.ui.popmenu_edit.setVisible(True)
+        self.app.ui.popmenu_save.setVisible(False)
+
+        self.app.ui.popmenu_disable.setVisible(True)
+        self.app.ui.cmenu_newmenu.menuAction().setVisible(True)
+        self.app.ui.popmenu_properties.setVisible(True)
+        self.app.ui.g_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.e_editor_cmenu.menuAction().setVisible(False)
+        self.app.ui.grb_editor_cmenu.menuAction().setVisible(False)
+
+        # Show original geometry
+        if self.gerber_obj:
+            self.gerber_obj.visible = True
+
+    def connect_canvas_event_handlers(self):
+        # Canvas events
+
+        # make sure that the shortcuts key and mouse events will no longer be linked to the methods from FlatCAMApp
+        # but those from AppGeoEditor
+
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
+        self.mp = self.canvas.graph_event_connect('mouse_press', self.on_canvas_click)
+        self.mm = self.canvas.graph_event_connect('mouse_move', self.on_canvas_move)
+        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_grb_click_release)
+
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
+            self.canvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
+            self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+            self.canvas.graph_event_disconnect('mouse_double_click', self.app.on_mouse_double_click_over_plot)
+        else:
+            self.canvas.graph_event_disconnect(self.app.mp)
+            self.canvas.graph_event_disconnect(self.app.mm)
+            self.canvas.graph_event_disconnect(self.app.mr)
+            self.canvas.graph_event_disconnect(self.app.mdc)
+
+        self.app.collection.view.clicked.disconnect()
+
+        self.app.ui.popmenu_copy.triggered.disconnect()
+        self.app.ui.popmenu_delete.triggered.disconnect()
+        self.app.ui.popmenu_move.triggered.disconnect()
+
+        self.app.ui.popmenu_copy.triggered.connect(self.on_copy_button)
+        self.app.ui.popmenu_delete.triggered.connect(self.on_delete_btn)
+        self.app.ui.popmenu_move.triggered.connect(self.on_move_button)
+
+        # Gerber Editor
+        self.app.ui.grb_draw_pad.triggered.connect(self.on_pad_add)
+        self.app.ui.grb_draw_pad_array.triggered.connect(self.on_pad_add_array)
+        self.app.ui.grb_draw_track.triggered.connect(self.on_track_add)
+        self.app.ui.grb_draw_region.triggered.connect(self.on_region_add)
+
+        self.app.ui.grb_draw_poligonize.triggered.connect(self.on_poligonize)
+        self.app.ui.grb_draw_semidisc.triggered.connect(self.on_add_semidisc)
+        self.app.ui.grb_draw_disc.triggered.connect(self.on_disc_add)
+        self.app.ui.grb_draw_buffer.triggered.connect(lambda: self.select_tool("buffer"))
+        self.app.ui.grb_draw_scale.triggered.connect(lambda: self.select_tool("scale"))
+        self.app.ui.grb_draw_markarea.triggered.connect(lambda: self.select_tool("markarea"))
+        self.app.ui.grb_draw_eraser.triggered.connect(self.on_eraser)
+        self.app.ui.grb_draw_transformations.triggered.connect(self.on_transform)
+
+    def disconnect_canvas_event_handlers(self):
+
+        # we restore the key and mouse control to FlatCAMApp method
+        # first connect to new, then disconnect the old handlers
+        # don't ask why but if there is nothing connected I've seen issues
+        self.app.mp = self.canvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
+        self.app.mm = self.canvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
+        self.app.mr = self.canvas.graph_event_connect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        self.app.mdc = self.canvas.graph_event_connect('mouse_double_click', self.app.on_mouse_double_click_over_plot)
+        self.app.collection.view.clicked.connect(self.app.collection.on_mouse_down)
+
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_press', self.on_canvas_click)
+            self.canvas.graph_event_disconnect('mouse_move', self.on_canvas_move)
+            self.canvas.graph_event_disconnect('mouse_release', self.on_grb_click_release)
+        else:
+            self.canvas.graph_event_disconnect(self.mp)
+            self.canvas.graph_event_disconnect(self.mm)
+            self.canvas.graph_event_disconnect(self.mr)
+
+        try:
+            self.app.ui.popmenu_copy.triggered.disconnect(self.on_copy_button)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.popmenu_delete.triggered.disconnect(self.on_delete_btn)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.popmenu_move.triggered.disconnect(self.on_move_button)
+        except (TypeError, AttributeError):
+            pass
+
+        self.app.ui.popmenu_copy.triggered.connect(self.app.on_copy_command)
+        self.app.ui.popmenu_delete.triggered.connect(self.app.on_delete)
+        self.app.ui.popmenu_move.triggered.connect(self.app.obj_move)
+
+        # Gerber Editor
+
+        try:
+            self.app.ui.grb_draw_pad.triggered.disconnect(self.on_pad_add)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.grb_draw_pad_array.triggered.disconnect(self.on_pad_add_array)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.grb_draw_track.triggered.disconnect(self.on_track_add)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.grb_draw_region.triggered.disconnect(self.on_region_add)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.ui.grb_draw_poligonize.triggered.disconnect(self.on_poligonize)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_semidisc.triggered.diconnect(self.on_add_semidisc)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_disc.triggered.disconnect(self.on_disc_add)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_buffer.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_scale.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_markarea.triggered.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_eraser.triggered.disconnect(self.on_eraser)
+        except (TypeError, AttributeError):
+            pass
+        try:
+            self.app.ui.grb_draw_transformations.triggered.disconnect(self.on_transform)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+    def clear(self):
+        self.thread.quit()
+
+        self.active_tool = None
+        self.selected = []
+        self.storage_dict.clear()
+        self.results.clear()
+
+        self.shapes.clear(update=True)
+        self.tool_shape.clear(update=True)
+        self.ma_annotation.clear(update=True)
+
+    def edit_fcgerber(self, orig_grb_obj):
+        """
+        Imports the geometry found in self.apertures from the given FlatCAM Gerber object
+        into the editor.
+
+        :param orig_grb_obj: ExcellonObject
+        :return: None
+        """
+
+        self.deactivate_grb_editor()
+        self.activate_grb_editor()
+
+        # reset the tool table
+        self.ui.apertures_table.clear()
+
+        self.ui.apertures_table.setHorizontalHeaderLabels(['#', _('Code'), _('Type'), _('Size'), _('Dim')])
+        self.last_aperture_selected = None
+
+        # create a reference to the source object
+        self.gerber_obj = orig_grb_obj
+        self.gerber_obj_options = orig_grb_obj.options
+
+        file_units = self.gerber_obj.units if self.gerber_obj.units else 'IN'
+        app_units = self.app.defaults['units']
+        # self.conversion_factor = 25.4 if file_units == 'IN' else (1 / 25.4) if file_units != app_units else 1
+
+        if file_units == app_units:
+            self.conversion_factor = 1
+        else:
+            if file_units == 'IN':
+                self.conversion_factor = 25.4
+            else:
+                self.conversion_factor = 0.0393700787401575
+
+        # Hide original geometry
+        orig_grb_obj.visible = False
+
+        # Set selection tolerance
+        # DrawToolShape.tolerance = fc_excellon.drawing_tolerance * 10
+
+        self.select_tool("select")
+
+        try:
+            # we activate this after the initial build as we don't need to see the tool been populated
+            self.ui.apertures_table.itemChanged.connect(self.on_tool_edit)
+        except Exception as e:
+            log.debug("AppGerberEditor.edit_fcgerber() --> %s" % str(e))
+
+        # apply the conversion factor on the obj.apertures
+        conv_apertures = deepcopy(self.gerber_obj.apertures)
+        for apcode in self.gerber_obj.apertures:
+            for key in self.gerber_obj.apertures[apcode]:
+                if key == 'width':
+                    conv_apertures[apcode]['width'] = self.gerber_obj.apertures[apcode]['width'] * \
+                                                      self.conversion_factor
+                elif key == 'height':
+                    conv_apertures[apcode]['height'] = self.gerber_obj.apertures[apcode]['height'] * \
+                                                       self.conversion_factor
+                elif key == 'diam':
+                    conv_apertures[apcode]['diam'] = self.gerber_obj.apertures[apcode]['diam'] * self.conversion_factor
+                elif key == 'size':
+                    conv_apertures[apcode]['size'] = self.gerber_obj.apertures[apcode]['size'] * self.conversion_factor
+                else:
+                    conv_apertures[apcode][key] = self.gerber_obj.apertures[apcode][key]
+
+        self.gerber_obj.apertures = conv_apertures
+        self.gerber_obj.units = app_units
+
+        # # and then add it to the storage elements (each storage elements is a member of a list
+        # def job_thread(aperture_id):
+        #     with self.app.proc_container.new('%s: %s ...' %
+        #                                      (_("Adding geometry for aperture"),  str(aperture_id))):
+        #         storage_elem = []
+        #         self.storage_dict[aperture_id] = {}
+        #
+        #         # add the Gerber geometry to editor storage
+        #         for k, v in self.gerber_obj.apertures[aperture_id].items():
+        #             try:
+        #                 if k == 'geometry':
+        #                     for geo_el in v:
+        #                         if geo_el:
+        #                             self.add_gerber_shape(DrawToolShape(geo_el), storage_elem)
+        #                     self.storage_dict[aperture_id][k] = storage_elem
+        #                 else:
+        #                     self.storage_dict[aperture_id][k] = self.gerber_obj.apertures[aperture_id][k]
+        #             except Exception as e:
+        #                 log.debug("AppGerberEditor.edit_fcgerber().job_thread() --> %s" % str(e))
+        #
+        #         # Check promises and clear if exists
+        #         while True:
+        #             try:
+        #                 self.grb_plot_promises.remove(aperture_id)
+        #                 time.sleep(0.5)
+        #             except ValueError:
+        #                 break
+        #
+        # # we create a job work each aperture, job that work in a threaded way to store the geometry in local storage
+        # # as DrawToolShapes
+        # for ap_code in self.gerber_obj.apertures:
+        #     self.grb_plot_promises.append(ap_code)
+        #     self.app.worker_task.emit({'fcn': job_thread, 'params': [ap_code]})
+        #
+        # self.set_ui()
+        #
+        # # do the delayed plot only if there is something to plot (the gerber is not empty)
+        # try:
+        #     if bool(self.gerber_obj.apertures):
+        #         self.start_delayed_plot(check_period=1000)
+        #     else:
+        #         raise AttributeError
+        # except AttributeError:
+        #     # now that we have data (empty data actually), create the GUI interface and add it to the Tool Tab
+        #     self.build_ui(first_run=True)
+        #     # and add the first aperture to have something to play with
+        #     self.on_aperture_add('10')
+
+        # self.app.worker_task.emit({'fcn': worker_job, 'params': [self]})
+
+        class Execute_Edit(QtCore.QObject):
+
+            start = QtCore.pyqtSignal(str)
+
+            def __init__(self, app):
+                super(Execute_Edit, self).__init__()
+                self.app = app
+                self.start.connect(self.run)
+
+            @staticmethod
+            def worker_job(app_obj):
+                with app_obj.app.proc_container.new('%s ...' % _("Loading")):
+                    # ###############################################################
+                    # APPLY CLEAR_GEOMETRY on the SOLID_GEOMETRY
+                    # ###############################################################
+
+                    # list of clear geos that are to be applied to the entire file
+                    global_clear_geo = []
+
+                    # create one big geometry made out of all 'negative' (clear) polygons
+                    for aper_id in app_obj.gerber_obj.apertures:
+                        # first check if we have any clear_geometry (LPC) and if yes added it to the global_clear_geo
+                        if 'geometry' in app_obj.gerber_obj.apertures[aper_id]:
+                            for elem in app_obj.gerber_obj.apertures[aper_id]['geometry']:
+                                if 'clear' in elem:
+                                    global_clear_geo.append(elem['clear'])
+                    log.warning("Found %d clear polygons." % len(global_clear_geo))
+
+                    if global_clear_geo:
+                        global_clear_geo = unary_union(global_clear_geo)
+                        if isinstance(global_clear_geo, Polygon):
+                            global_clear_geo = [global_clear_geo]
+
+                    # we subtract the big "negative" (clear) geometry from each solid polygon but only the part of
+                    # clear geometry that fits inside the solid. otherwise we may loose the solid
+                    for ap_code in app_obj.gerber_obj.apertures:
+                        temp_solid_geometry = []
+                        if 'geometry' in app_obj.gerber_obj.apertures[ap_code]:
+                            # for elem in self.gerber_obj.apertures[apcode]['geometry']:
+                            #     if 'solid' in elem:
+                            #         solid_geo = elem['solid']
+                            #         for clear_geo in global_clear_geo:
+                            #             # Make sure that the clear_geo is within the solid_geo otherwise we loose
+                            #             # the solid_geometry. We want for clear_geometry just to cut
+                            #             # into solid_geometry not to delete it
+                            #             if clear_geo.within(solid_geo):
+                            #                 solid_geo = solid_geo.difference(clear_geo)
+                            #         try:
+                            #             for poly in solid_geo:
+                            #                 new_elem = {}
+                            #
+                            #                 new_elem['solid'] = poly
+                            #                 if 'clear' in elem:
+                            #                     new_elem['clear'] = poly
+                            #                 if 'follow' in elem:
+                            #                     new_elem['follow'] = poly
+                            #                 temp_elem.append(deepcopy(new_elem))
+                            #         except TypeError:
+                            #             new_elem = {}
+                            #             new_elem['solid'] = solid_geo
+                            #             if 'clear' in elem:
+                            #                 new_elem['clear'] = solid_geo
+                            #             if 'follow' in elem:
+                            #                 new_elem['follow'] = solid_geo
+                            #             temp_elem.append(deepcopy(new_elem))
+                            for elem in app_obj.gerber_obj.apertures[ap_code]['geometry']:
+                                new_elem = {}
+                                if 'solid' in elem:
+                                    solid_geo = elem['solid']
+                                    if not global_clear_geo or global_clear_geo.is_empty:
+                                        pass
+                                    else:
+                                        for clear_geo in global_clear_geo:
+                                            # Make sure that the clear_geo is within the solid_geo otherwise we loose
+                                            # Make sure that the clear_geo is within the solid_geo otherwise we loose
+                                            # the solid_geometry. We want for clear_geometry just to cut into
+                                            # solid_geometry not to delete it
+                                            if clear_geo.within(solid_geo):
+                                                solid_geo = solid_geo.difference(clear_geo)
+
+                                    new_elem['solid'] = solid_geo
+                                if 'clear' in elem:
+                                    new_elem['clear'] = elem['clear']
+                                if 'follow' in elem:
+                                    new_elem['follow'] = elem['follow']
+                                temp_solid_geometry.append(deepcopy(new_elem))
+
+                            app_obj.gerber_obj.apertures[ap_code]['geometry'] = deepcopy(temp_solid_geometry)
+
+                    log.warning("Polygon difference done for %d apertures." % len(app_obj.gerber_obj.apertures))
+
+                    try:
+                        # Loading the Geometry into Editor Storage
+                        for ap_code, ap_dict in app_obj.gerber_obj.apertures.items():
+                            app_obj.results.append(
+                                app_obj.pool.apply_async(app_obj.add_apertures, args=(ap_code, ap_dict))
+                            )
+                    except Exception as ee:
+                        log.debug(
+                            "AppGerberEditor.edit_fcgerber.worker_job() Adding processes to pool --> %s" % str(ee))
+                        traceback.print_exc()
+
+                    output = []
+                    for p in app_obj.results:
+                        output.append(p.get())
+
+                    for elem in output:
+                        app_obj.storage_dict[elem[0]] = deepcopy(elem[1])
+
+                    app_obj.mp_finished.emit(output)
+
+            def run(self):
+                self.worker_job(self.app)
+
+        # self.thread.start(QtCore.QThread.NormalPriority)
+
+        executable_edit = Execute_Edit(app=self)
+        # executable_edit.moveToThread(self.thread)
+        # executable_edit.start.emit("Started")
+
+        self.app.worker_task.emit({'fcn': executable_edit.run, 'params': []})
+
+    @staticmethod
+    def add_apertures(aperture_id, aperture_dict):
+        storage_elem = []
+        storage_dict = {}
+
+        for k, v in list(aperture_dict.items()):
+            try:
+                if k == 'geometry':
+                    for geo_el in v:
+                        if geo_el:
+                            storage_elem.append(DrawToolShape(geo_el))
+                    storage_dict[k] = storage_elem
+                else:
+                    storage_dict[k] = aperture_dict[k]
+            except Exception as e:
+                log.debug("AppGerberEditor.edit_fcgerber().job_thread() --> %s" % str(e))
+
+        return [aperture_id, storage_dict]
+
+    def on_multiprocessing_finished(self):
+        self.app.proc_container.update_view_text(' %s' % _("Setting up the UI"))
+        self.app.inform.emit('[success] %s.' % _("Adding geometry finished. Preparing the GUI"))
+        self.set_ui()
+        self.build_ui(first_run=True)
+        self.plot_all()
+
+        # HACK: enabling/disabling the cursor seams to somehow update the shapes making them more 'solid'
+        # - perhaps is a bug in VisPy implementation
+        self.app.app_cursor.enabled = False
+        self.app.app_cursor.enabled = True
+        self.app.inform.emit('[success] %s' % _("Finished loading the Gerber object into the editor."))
+
+    def update_fcgerber(self):
+        """
+        Create a new Gerber object that contain the edited content of the source Gerber object
+
+        :return: None
+        """
+        new_grb_name = self.edited_obj_name
+
+        # if the 'delayed plot' malfunctioned stop the QTimer
+        try:
+            self.plot_thread.stop()
+        except Exception as e:
+            log.debug("AppGerberEditor.update_fcgerber() --> %s" % str(e))
+
+        if "_edit" in self.edited_obj_name:
+            try:
+                _id = int(self.edited_obj_name[-1]) + 1
+                new_grb_name = self.edited_obj_name[:-1] + str(_id)
+            except ValueError:
+                new_grb_name += "_1"
+        else:
+            new_grb_name = self.edited_obj_name + "_edit"
+
+        self.app.worker_task.emit({'fcn': self.new_edited_gerber, 'params': [new_grb_name, self.storage_dict]})
+        # self.new_edited_gerber(new_grb_name, self.storage_dict)
+
+    @staticmethod
+    def update_options(obj):
+        try:
+            if not obj.options:
+                obj.options = {'xmin': 0, 'ymin': 0, 'xmax': 0, 'ymax': 0}
+                return True
+            else:
+                return False
+        except AttributeError:
+            obj.options = {}
+            return True
+
+    def new_edited_gerber(self, outname, aperture_storage):
+        """
+        Creates a new Gerber object for the edited Gerber. Thread-safe.
+
+        :param outname:             Name of the resulting object. None causes the name to be that of the file.
+        :type outname:              str
+        :param aperture_storage:    a dictionary that holds all the objects geometry
+        :type aperture_storage:     dict
+        :return: None
+        """
+
+        self.app.log.debug("Update the Gerber object with edited content. Source is: %s" %
+                           self.gerber_obj.options['name'].upper())
+
+        out_name = outname
+        storage_dict = aperture_storage
+
+        local_storage_dict = {}
+        for aperture in storage_dict:
+            if 'geometry' in storage_dict[aperture]:
+                # add aperture only if it has geometry
+                if len(storage_dict[aperture]['geometry']) > 0:
+                    local_storage_dict[aperture] = deepcopy(storage_dict[aperture])
+
+        # How the object should be initialized
+        def obj_init(grb_obj, app_obj):
+
+            poly_buffer = []
+            follow_buffer = []
+
+            for storage_apcode, storage_val in local_storage_dict.items():
+                grb_obj.apertures[storage_apcode] = {}
+
+                for k, val in storage_val.items():
+                    if k == 'geometry':
+                        grb_obj.apertures[storage_apcode][k] = []
+                        for geo_el in val:
+                            geometric_data = geo_el.geo
+                            new_geo_el = {}
+                            if 'solid' in geometric_data:
+                                new_geo_el['solid'] = geometric_data['solid']
+                                poly_buffer.append(deepcopy(new_geo_el['solid']))
+
+                            if 'follow' in geometric_data:
+                                # if isinstance(geometric_data['follow'], Polygon):
+                                #     buff_val = -(int(storage_val['size']) / 2)
+                                #     geo_f = (geometric_data['follow'].buffer(buff_val)).exterior
+                                #     new_geo_el['follow'] = geo_f
+                                # else:
+                                #     new_geo_el['follow'] = geometric_data['follow']
+                                new_geo_el['follow'] = geometric_data['follow']
+                                follow_buffer.append(deepcopy(new_geo_el['follow']))
+                            else:
+                                if 'solid' in geometric_data:
+                                    geo_f = geometric_data['solid'].exterior
+                                    new_geo_el['follow'] = geo_f
+                                    follow_buffer.append(deepcopy(new_geo_el['follow']))
+
+                            if 'clear' in geometric_data:
+                                new_geo_el['clear'] = geometric_data['clear']
+
+                            if new_geo_el:
+                                grb_obj.apertures[storage_apcode][k].append(deepcopy(new_geo_el))
+                    else:
+                        grb_obj.apertures[storage_apcode][k] = val
+
+            grb_obj.aperture_macros = deepcopy(self.gerber_obj.aperture_macros)
+
+            new_poly = MultiPolygon(poly_buffer)
+            new_poly = new_poly.buffer(0.00000001)
+            new_poly = new_poly.buffer(-0.00000001)
+
+            # for ad in grb_obj.apertures:
+            #     print(ad, grb_obj.apertures[ad])
+
+            try:
+                __ = iter(new_poly)
+            except TypeError:
+                new_poly = [new_poly]
+
+            grb_obj.solid_geometry = deepcopy(new_poly)
+            grb_obj.follow_geometry = deepcopy(follow_buffer)
+
+            for k, v in self.gerber_obj_options.items():
+                if k == 'name':
+                    grb_obj.options[k] = out_name
+                else:
+                    grb_obj.options[k] = deepcopy(v)
+
+            grb_obj.multigeo = False
+            grb_obj.follow = False
+            grb_obj.units = app_obj.defaults['units']
+
+            try:
+                grb_obj.create_geometry()
+            except KeyError:
+                self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                     _("There are no Aperture definitions in the file. Aborting Gerber creation."))
+            except Exception:
+                msg = '[ERROR] %s' % _("An internal error has occurred. See shell.\n")
+                msg += traceback.format_exc()
+                app_obj.inform.emit(msg)
+                raise
+
+            grb_obj.source_file = self.app.f_handlers.export_gerber(obj_name=out_name, filename=None,
+                                                                    local_use=grb_obj, use_thread=False)
+
+        with self.app.proc_container.new(_("Working ...")):
+            try:
+                self.app.app_obj.new_object("gerber", outname, obj_init)
+            except Exception as e:
+                log.error("Error on Edited object creation: %s" % str(e))
+                # make sure to clean the previous results
+                self.results = []
+                return
+
+            # make sure to clean the previous results
+            self.results = []
+            self.deactivate_grb_editor()
+            self.app.inform.emit('[success] %s' % _("Done."))
+
+    def on_tool_select(self, tool):
+        """
+        Behavior of the toolbar. Tool initialization.
+
+        :rtype : None
+        """
+        current_tool = tool
+
+        self.app.log.debug("on_tool_select('%s')" % tool)
+
+        if self.last_aperture_selected is None and current_tool != 'select':
+            # self.draw_app.select_tool('select')
+            self.complete = True
+            current_tool = 'select'
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. No aperture is selected"))
+
+        # This is to make the group behave as radio group
+        if current_tool in self.tools_gerber:
+            if self.tools_gerber[current_tool]["button"].isChecked():
+                self.app.log.debug("%s is checked." % current_tool)
+                for t in self.tools_gerber:
+                    if t != current_tool:
+                        self.tools_gerber[t]["button"].setChecked(False)
+
+                # this is where the Editor toolbar classes (button's) are instantiated
+                self.active_tool = self.tools_gerber[current_tool]["constructor"](self)
+                # self.app.inform.emit(self.active_tool.start_msg)
+            else:
+                self.app.log.debug("%s is NOT checked." % current_tool)
+                for t in self.tools_gerber:
+                    self.tools_gerber[t]["button"].setChecked(False)
+
+                self.select_tool('select')
+                self.active_tool = SelectEditorGrb(self)
+
+    def on_row_selected(self, row, col):
+        # if col == 0:
+        key_modifier = QtWidgets.QApplication.keyboardModifiers()
+        if self.app.defaults["global_mselect_key"] == 'Control':
+            modifier_to_use = Qt.ControlModifier
+        else:
+            modifier_to_use = Qt.ShiftModifier
+
+        if key_modifier == modifier_to_use:
+            pass
+        else:
+            self.selected = []
+
+        try:
+            selected_ap_code = self.ui.apertures_table.item(row, 1).text()
+            self.last_aperture_selected = copy(selected_ap_code)
+
+            for obj in self.storage_dict[selected_ap_code]['geometry']:
+                self.selected.append(obj)
+        except Exception as e:
+            self.app.log.debug(str(e))
+
+        self.plot_all()
+
+    # def toolbar_tool_toggle(self, key):
+    #     """
+    #
+    #     :param key: key to update in self.options dictionary
+    #     :return:
+    #     """
+    #     self.options[key] = self.sender().isChecked()
+    #     return self.options[key]
+
+    def on_grb_shape_complete(self, storage=None, specific_shape=None, no_plot=False):
+        """
+
+        :param storage: where to store the shape
+        :param specific_shape: optional, the shape to be stored
+        :param no_plot: use this if you want the added shape not plotted
+        :return:
+        """
+        self.app.log.debug("on_grb_shape_complete()")
+
+        if specific_shape:
+            geo = specific_shape
+        else:
+            geo = deepcopy(self.active_tool.geometry)
+            if geo is None:
+                return
+
+        if storage is not None:
+            # Add shape
+            self.add_gerber_shape(geo, storage)
+        else:
+            stora = self.storage_dict[self.last_aperture_selected]['geometry']
+            self.add_gerber_shape(geo, storage=stora)
+
+        # Remove any utility shapes
+        self.delete_utility_geometry()
+        self.tool_shape.clear(update=True)
+
+        if no_plot is False:
+            # Re-plot and reset tool.
+            self.plot_all()
+
+    def add_gerber_shape(self, shape_element, storage):
+        """
+        Adds a shape to the shape storage.
+
+        :param shape_element: Shape to be added.
+        :type shape_element: DrawToolShape or DrawToolUtilityShape Geometry is stored as a dict with keys: solid,
+        follow, clear, each value being a list of Shapely objects. The dict can have at least one of the mentioned keys
+        :param storage: Where to store the shape
+        :return: None
+        """
+        # List of DrawToolShape?
+
+        if isinstance(shape_element, list):
+            for subshape in shape_element:
+                self.add_gerber_shape(subshape, storage)
+            return
+
+        assert isinstance(shape_element, DrawToolShape), \
+            "Expected a DrawToolShape, got %s" % str(type(shape_element))
+
+        assert shape_element.geo is not None, \
+            "Shape object has empty geometry (None)"
+
+        assert(isinstance(shape_element.geo, list) and len(shape_element.geo) > 0) or not \
+            isinstance(shape_element.geo, list), "Shape objects has empty geometry ([])"
+
+        if isinstance(shape_element, DrawToolUtilityShape):
+            self.utility.append(shape_element)
+        else:
+            storage.append(shape_element)
+
+    def on_canvas_click(self, event):
+        """
+        event.x and .y have canvas coordinates
+        event.xdata and .ydata have plot coordinates
+
+        :param event: Event object dispatched by VisPy
+        :return: None
+        """
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            # right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            # right_button = 3
+
+        self.pos = self.canvas.translate_coords(event_pos)
+
+        if self.app.grid_status():
+            self.pos = self.app.geo_editor.snap(self.pos[0], self.pos[1])
+        else:
+            self.pos = (self.pos[0], self.pos[1])
+
+        if event.button == 1:
+            self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                                   "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
+
+            # Selection with left mouse button
+            if self.active_tool is not None:
+                modifiers = QtWidgets.QApplication.keyboardModifiers()
+
+                # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard
+                if modifiers == QtCore.Qt.ShiftModifier:
+                    self.app.clipboard.setText(
+                        self.app.defaults["global_point_clipboard_format"] %
+                        (self.decimals, self.pos[0], self.decimals, self.pos[1])
+                    )
+                    self.app.inform.emit('[success] %s' % _("Coordinates copied to clipboard."))
+                    return
+
+                # Dispatch event to active_tool
+                self.active_tool.click(self.app.geo_editor.snap(self.pos[0], self.pos[1]))
+
+                # If it is a shape generating tool
+                if isinstance(self.active_tool, ShapeToolEditorGrb) and self.active_tool.complete:
+                    if self.current_storage is not None:
+                        self.on_grb_shape_complete(self.current_storage)
+                        self.build_ui()
+
+                    # MS: always return to the Select Tool if modifier key is not pressed
+                    # else return to the current tool
+                    key_modifier = QtWidgets.QApplication.keyboardModifiers()
+                    if self.app.defaults["global_mselect_key"] == 'Control':
+                        modifier_to_use = Qt.ControlModifier
+                    else:
+                        modifier_to_use = Qt.ShiftModifier
+
+                    # if modifier key is pressed then we add to the selected list the current shape but if it's already
+                    # in the selected list, we removed it. Therefore first click selects, second deselects.
+                    if key_modifier == modifier_to_use:
+                        self.select_tool(self.active_tool.name)
+                    else:
+                        # return to Select tool but not for PadEditorGrb
+                        if isinstance(self.active_tool, PadEditorGrb):
+                            self.select_tool(self.active_tool.name)
+                        else:
+                            self.select_tool("select")
+                        return
+
+                # if isinstance(self.active_tool, SelectEditorGrb):
+                #     self.plot_all()
+            else:
+                self.app.log.debug("No active tool to respond to click!")
+
+    def on_grb_click_release(self, event):
+        self.modifiers = QtWidgets.QApplication.keyboardModifiers()
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            # event_is_dragging = event.is_dragging
+            right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            # event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
+
+        pos_canvas = self.canvas.translate_coords(event_pos)
+        if self.app.grid_status():
+            pos = self.app.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+        else:
+            pos = (pos_canvas[0], pos_canvas[1])
+
+        # if the released mouse button was RMB then test if it was a panning motion or not, if not it was a context
+        # canvas menu
+        try:
+            if event.button == right_button:  # right click
+                if self.app.ui.popMenu.mouse_is_panning is False:
+                    if self.in_action is False:
+                        try:
+                            QtGui.QGuiApplication.restoreOverrideCursor()
+                        except Exception as e:
+                            log.debug("AppGerberEditor.on_grb_click_release() --> %s" % str(e))
+
+                        if self.active_tool.complete is False and not isinstance(self.active_tool, SelectEditorGrb):
+                            self.active_tool.complete = True
+                            self.in_action = False
+                            self.delete_utility_geometry()
+                            self.app.inform.emit('[success] %s' %
+                                                 _("Done."))
+                            self.select_tool('select')
+                        else:
+                            self.app.cursor = QtGui.QCursor()
+                            self.app.populate_cmenu_grids()
+                            self.app.ui.popMenu.popup(self.app.cursor.pos())
+                    else:
+                        # if right click on canvas and the active tool need to be finished (like Path or Polygon)
+                        # right mouse click will finish the action
+                        if isinstance(self.active_tool, ShapeToolEditorGrb):
+                            if isinstance(self.active_tool, TrackEditorGrb):
+                                self.active_tool.make()
+                            else:
+                                self.active_tool.click(self.app.geo_editor.snap(self.x, self.y))
+                                self.active_tool.make()
+
+                            if self.active_tool.complete:
+                                self.on_grb_shape_complete()
+                                self.app.inform.emit('[success] %s' % _("Done."))
+
+                                # MS: always return to the Select Tool if modifier key is not pressed
+                                # else return to the current tool but not for TrackEditorGrb
+
+                                if isinstance(self.active_tool, TrackEditorGrb):
+                                    self.select_tool(self.active_tool.name)
+                                else:
+                                    key_modifier = QtWidgets.QApplication.keyboardModifiers()
+                                    if (self.app.defaults["global_mselect_key"] == 'Control' and
+                                        key_modifier == Qt.ControlModifier) or \
+                                            (self.app.defaults["global_mselect_key"] == 'Shift' and
+                                             key_modifier == Qt.ShiftModifier):
+
+                                        self.select_tool(self.active_tool.name)
+                                    else:
+                                        self.select_tool("select")
+        except Exception as e:
+            log.warning("AppGerberEditor.on_grb_click_release() RMB click --> Error: %s" % str(e))
+            raise
+
+        # if the released mouse button was LMB then test if we had a right-to-left selection or a left-to-right
+        # selection and then select a type of selection ("enclosing" or "touching")
+        try:
+            if event.button == 1:  # left click
+                if self.app.selection_type is not None:
+                    self.draw_selection_area_handler(self.pos, pos, self.app.selection_type)
+                    self.app.selection_type = None
+
+                elif isinstance(self.active_tool, SelectEditorGrb):
+                    self.active_tool.click_release((self.pos[0], self.pos[1]))
+
+                    # # if there are selected objects then plot them
+                    # if self.selected:
+                    #     self.plot_all()
+        except Exception as e:
+            log.warning("AppGerberEditor.on_grb_click_release() LMB click --> Error: %s" % str(e))
+            raise
+
+    def draw_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:
+        """
+
+        poly_selection = Polygon([start_pos, (end_pos[0], start_pos[1]), end_pos, (start_pos[0], end_pos[1])])
+        sel_aperture = set()
+        self.ui.apertures_table.clearSelection()
+
+        self.app.delete_selection_shape()
+        for storage in self.storage_dict:
+            for obj in self.storage_dict[storage]['geometry']:
+                if 'solid' in obj.geo:
+                    geometric_data = obj.geo['solid']
+                    if (sel_type is True and poly_selection.contains(geometric_data)) or \
+                            (sel_type is False and poly_selection.intersects(geometric_data)):
+                        if self.key == self.app.defaults["global_mselect_key"]:
+                            if obj in self.selected:
+                                self.selected.remove(obj)
+                            else:
+                                # add the object to the selected shapes
+                                self.selected.append(obj)
+                                sel_aperture.add(storage)
+                        else:
+                            self.selected.append(obj)
+                            sel_aperture.add(storage)
+
+        try:
+            self.ui.apertures_table.cellPressed.disconnect()
+        except Exception as e:
+            log.debug("AppGerberEditor.draw_selection_Area_handler() --> %s" % str(e))
+        # select the aperture code of the selected geometry, in the tool table
+        self.ui.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
+        for aper in sel_aperture:
+            for row_to_sel in range(self.ui.apertures_table.rowCount()):
+                if str(aper) == self.ui.apertures_table.item(row_to_sel, 1).text():
+                    if row_to_sel not in set(index.row() for index in self.ui.apertures_table.selectedIndexes()):
+                        self.ui.apertures_table.selectRow(row_to_sel)
+                    self.last_aperture_selected = aper
+        self.ui.apertures_table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+
+        self.ui.apertures_table.cellPressed.connect(self.on_row_selected)
+        self.plot_all()
+
+    def on_canvas_move(self, event):
+        """
+        Called on 'mouse_move' event
+
+        event.pos have canvas screen coordinates
+
+        :param event: Event object dispatched by VisPy SceneCavas
+        :return: None
+        """
+
+        if not self.app.plotcanvas.native.hasFocus():
+            self.app.plotcanvas.native.setFocus()
+
+        if self.app.is_legacy is False:
+            event_pos = event.pos
+            event_is_dragging = event.is_dragging
+            right_button = 2
+        else:
+            event_pos = (event.xdata, event.ydata)
+            event_is_dragging = self.app.plotcanvas.is_dragging
+            right_button = 3
+
+        pos_canvas = self.canvas.translate_coords(event_pos)
+        event.xdata, event.ydata = pos_canvas[0], pos_canvas[1]
+
+        self.x = event.xdata
+        self.y = event.ydata
+
+        self.app.ui.popMenu.mouse_is_panning = False
+
+        # if the RMB is clicked and mouse is moving over plot then 'panning_action' is True
+        if event.button == right_button and event_is_dragging == 1:
+            self.app.ui.popMenu.mouse_is_panning = True
+            return
+
+        try:
+            x = float(event.xdata)
+            y = float(event.ydata)
+        except TypeError:
+            return
+
+        if self.active_tool is None:
+            return
+
+        # # ## Snap coordinates
+        if self.app.grid_status():
+            x, y = self.app.geo_editor.snap(x, y)
+
+            # Update cursor
+            self.app.app_cursor.set_data(np.asarray([(x, y)]), symbol='++', edge_color=self.app.cursor_color_3D,
+                                         edge_width=self.app.defaults["global_cursor_width"],
+                                         size=self.app.defaults["global_cursor_size"])
+
+        self.snap_x = x
+        self.snap_y = y
+
+        self.app.mouse = [x, y]
+
+        if self.pos is None:
+            self.pos = (0, 0)
+        self.app.dx = x - self.pos[0]
+        self.app.dy = y - self.pos[1]
+
+        # # update the position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.position_label.setText("&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
+                                           "<b>Y</b>: %.4f&nbsp;" % (x, y))
+
+        # update the reference position label in the infobar since the APP mouse event handlers are disconnected
+        self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
+
+        units = self.app.defaults["units"].lower()
+        self.app.plotcanvas.text_hud.text = \
+            'Dx:\t{:<.4f} [{:s}]\nDy:\t{:<.4f} [{:s}]\n\nX:  \t{:<.4f} [{:s}]\nY:  \t{:<.4f} [{:s}]'.format(
+                self.app.dx, units, self.app.dy, units, x, units, y, units)
+
+        self.update_utility_geometry(data=(x, y))
+
+        # # ## Selection area on canvas section # ##
+        if event_is_dragging == 1 and event.button == 1:
+            # I make an exception for RegionEditorGrb and TrackEditorGrb because clicking and dragging while making 
+            # regions can create strange issues like missing a point in a track/region
+            if isinstance(self.active_tool, RegionEditorGrb) or isinstance(self.active_tool, TrackEditorGrb):
+                pass
+            else:
+                dx = pos_canvas[0] - self.pos[0]
+                self.app.delete_selection_shape()
+                if dx < 0:
+                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y),
+                                                         color=self.app.defaults["global_alt_sel_line"],
+                                                         face_color=self.app.defaults['global_alt_sel_fill'])
+                    self.app.selection_type = False
+                else:
+                    self.app.draw_moving_selection_shape((self.pos[0], self.pos[1]), (x, y))
+                    self.app.selection_type = True
+        else:
+            self.app.selection_type = None
+
+    def update_utility_geometry(self, data):
+        # # ## Utility geometry (animated)
+        geo = self.active_tool.utility_geometry(data=data)
+
+        if isinstance(geo, DrawToolShape) and geo.geo is not None:
+            # Remove any previous utility shape
+            self.tool_shape.clear(update=True)
+            self.draw_utility_geometry(geo_shape=geo)
+
+    def draw_utility_geometry(self, geo_shape):
+        # it's a DrawToolShape therefore it stores his geometry in the geo attribute
+        geometry = geo_shape.geo
+
+        try:
+            for el in geometry:
+                geometric_data = el['solid']
+                # Add the new utility shape
+                self.tool_shape.add(
+                    shape=geometric_data, color=(self.app.defaults["global_draw_color"] + '80'),
+                    # face_color=self.app.defaults['global_alt_sel_fill'],
+                    update=False, layer=0, tolerance=None
+                )
+        except TypeError:
+            geometric_data = geometry['solid']
+            # Add the new utility shape
+            self.tool_shape.add(
+                shape=geometric_data,
+                color=(self.app.defaults["global_draw_color"] + '80'),
+                # face_color=self.app.defaults['global_alt_sel_fill'],
+                update=False, layer=0, tolerance=None
+            )
+
+        self.tool_shape.redraw()
+
+    def plot_all(self):
+        """
+        Plots all shapes in the editor.
+
+        :return: None
+        :rtype: None
+        """
+        with self.app.proc_container.new('%s ...' % _("Plotting")):
+            self.shapes.clear(update=True)
+
+            for storage in self.storage_dict:
+                # fix for apertures with no geometry inside
+                if 'geometry' in self.storage_dict[storage]:
+                    for elem in self.storage_dict[storage]['geometry']:
+                        if 'solid' in elem.geo:
+                            geometric_data = elem.geo['solid']
+                            if geometric_data is None:
+                                continue
+
+                            if elem in self.selected:
+                                self.plot_shape(geometry=geometric_data,
+                                                color=self.app.defaults['global_sel_draw_color'] + 'FF',
+                                                linewidth=2)
+                            else:
+                                self.plot_shape(geometry=geometric_data,
+                                                color=self.app.defaults['global_draw_color'] + 'FF')
+
+            if self.utility:
+                for elem in self.utility:
+                    geometric_data = elem.geo['solid']
+                    self.plot_shape(geometry=geometric_data, linewidth=1)
+                    continue
+
+            self.shapes.redraw()
+
+    def plot_shape(self, geometry=None, color='#000000FF', linewidth=1):
+        """
+        Plots a geometric object or list of objects without rendering. Plotted objects
+        are returned as a list. This allows for efficient/animated rendering.
+
+        :param geometry:    Geometry to be plotted (Any Shapely.geom kind or list of such)
+        :param color:       Shape color
+        :param linewidth:   Width of lines in # of pixels.
+        :return:            List of plotted elements.
+        """
+
+        if geometry is None:
+            geometry = self.active_tool.geometry
+
+        try:
+            self.shapes.add(shape=geometry.geo, color=color, face_color=color, layer=0, tolerance=self.tolerance)
+        except AttributeError:
+            if type(geometry) == Point:
+                return
+            if len(color) == 9:
+                color = color[:7] + 'AF'
+            self.shapes.add(shape=geometry, color=color, face_color=color, layer=0, tolerance=self.tolerance)
+
+    # def start_delayed_plot(self, check_period):
+    #     """
+    #     This function starts an QTImer and it will periodically check if all the workers finish the plotting functions
+    #
+    #     :param check_period: time at which to check periodically if all plots finished to be plotted
+    #     :return:
+    #     """
+    #
+    #     # self.plot_thread = threading.Thread(target=lambda: self.check_plot_finished(check_period))
+    #     # self.plot_thread.start()
+    #     log.debug("AppGerberEditor --> Delayed Plot started.")
+    #     self.plot_thread = QtCore.QTimer()
+    #     self.plot_thread.setInterval(check_period)
+    #     self.plot_finished.connect(self.setup_ui_after_delayed_plot)
+    #     self.plot_thread.timeout.connect(self.check_plot_finished)
+    #     self.plot_thread.start()
+    #
+    # def check_plot_finished(self):
+    #     """
+    #     If all the promises made are finished then all the shapes are in shapes_storage and can be plotted safely and
+    #     then the UI is rebuilt accordingly.
+    #     :return:
+    #     """
+    #
+    #     try:
+    #         if not self.grb_plot_promises:
+    #             self.plot_thread.stop()
+    #             self.plot_finished.emit()
+    #             log.debug("AppGerberEditor --> delayed_plot finished")
+    #     except Exception as e:
+    #         traceback.print_exc()
+    #
+    # def setup_ui_after_delayed_plot(self):
+    #     self.plot_finished.disconnect()
+    #
+    #     # now that we have data, create the GUI interface and add it to the Tool Tab
+    #     self.build_ui(first_run=True)
+    #     self.plot_all()
+    #
+    #     # HACK: enabling/disabling the cursor seams to somehow update the shapes making them more 'solid'
+    #     # - perhaps is a bug in VisPy implementation
+    #     self.app.app_cursor.enabled = False
+    #     self.app.app_cursor.enabled = True
+
+    def on_zoom_fit(self):
+        """
+        Callback for zoom-fit request in Gerber Editor
+
+        :return:        None
+        """
+        log.debug("AppGerberEditor.on_zoom_fit()")
+
+        # calculate all the geometry in the edited Gerber object
+        edit_geo = []
+        for ap_code in self.storage_dict:
+            for geo_el in self.storage_dict[ap_code]['geometry']:
+                actual_geo = geo_el.geo
+                if 'solid' in actual_geo:
+                    edit_geo.append(actual_geo['solid'])
+
+        all_geo = unary_union(edit_geo)
+
+        # calculate the bounds values for the edited Gerber object
+        xmin, ymin, xmax, ymax = all_geo.bounds
+
+        if self.app.is_legacy is False:
+            new_rect = Rect(xmin, ymin, xmax, ymax)
+            self.app.plotcanvas.fit_view(rect=new_rect)
+        else:
+            width = xmax - xmin
+            height = ymax - ymin
+            xmin -= 0.05 * width
+            xmax += 0.05 * width
+            ymin -= 0.05 * height
+            ymax += 0.05 * height
+            self.app.plotcanvas.adjust_axes(xmin, ymin, xmax, ymax)
+
+    def get_selected(self):
+        """
+        Returns list of shapes that are selected in the editor.
+
+        :return: List of shapes.
+        """
+        # return [shape for shape in self.shape_buffer if shape["selected"]]
+        return self.selected
+
+    def delete_selected(self):
+        temp_ref = [s for s in self.selected]
+
+        if len(temp_ref) == 0:
+            self.app.inform.emit('[ERROR_NOTCL] %s' %
+                                 _("Failed. No aperture geometry is selected."))
+            return
+
+        for shape_sel in temp_ref:
+            self.delete_shape(shape_sel)
+
+        self.selected = []
+        self.build_ui()
+        self.app.inform.emit('[success] %s' % _("Done."))
+
+    def delete_shape(self, geo_el):
+        self.is_modified = True
+
+        if geo_el in self.utility:
+            self.utility.remove(geo_el)
+            return
+
+        for storage in self.storage_dict:
+            try:
+                if geo_el in self.storage_dict[storage]['geometry']:
+                    self.storage_dict[storage]['geometry'].remove(geo_el)
+            except KeyError:
+                pass
+        if geo_el in self.selected:
+            self.selected.remove(geo_el)
+
+    def delete_utility_geometry(self):
+        # for_deletion = [shape for shape in self.shape_buffer if shape.utility]
+        # for_deletion = [shape for shape in self.storage.get_objects() if shape.utility]
+        for_deletion = [geo_el for geo_el in self.utility]
+        for geo_el in for_deletion:
+            self.delete_shape(geo_el)
+
+        self.tool_shape.clear(update=True)
+        self.tool_shape.redraw()
+
+    def on_delete_btn(self):
+        self.delete_selected()
+        self.plot_all()
+
+    def select_tool(self, toolname):
+        """
+        Selects a drawing tool. Impacts the object and appGUI.
+
+        :param toolname: Name of the tool.
+        :return: None
+        """
+        self.tools_gerber[toolname]["button"].setChecked(True)
+        self.on_tool_select(toolname)
+
+    def set_selected(self, geo_el):
+
+        # Remove and add to the end.
+        if geo_el in self.selected:
+            self.selected.remove(geo_el)
+
+        self.selected.append(geo_el)
+
+    def set_unselected(self, geo_el):
+        if geo_el in self.selected:
+            self.selected.remove(geo_el)
+
+    def on_array_type_radio(self, val):
+        if val == 'linear':
+            self.ui.pad_axis_label.show()
+            self.ui.pad_axis_radio.show()
+            self.ui.pad_pitch_label.show()
+            self.ui.pad_pitch_entry.show()
+            self.ui.linear_angle_label.show()
+            self.ui.linear_angle_spinner.show()
+            self.ui.lin_separator_line.show()
+
+            self.ui.pad_direction_label.hide()
+            self.ui.pad_direction_radio.hide()
+            self.ui.pad_angle_label.hide()
+            self.ui.pad_angle_entry.hide()
+            self.ui.circ_separator_line.hide()
+        else:
+            self.delete_utility_geometry()
+
+            self.ui.pad_axis_label.hide()
+            self.ui.pad_axis_radio.hide()
+            self.ui.pad_pitch_label.hide()
+            self.ui.pad_pitch_entry.hide()
+            self.ui.linear_angle_label.hide()
+            self.ui.linear_angle_spinner.hide()
+            self.ui.lin_separator_line.hide()
+
+            self.ui.pad_direction_label.show()
+            self.ui.pad_direction_radio.show()
+            self.ui.pad_angle_label.show()
+            self.ui.pad_angle_entry.show()
+            self.ui.circ_separator_line.show()
+
+            self.app.inform.emit(_("Click on the circular array Center position"))
+
+    def on_linear_angle_radio(self, val):
+        if val == 'A':
+            self.ui.linear_angle_spinner.show()
+            self.ui.linear_angle_label.show()
+        else:
+            self.ui.linear_angle_spinner.hide()
+            self.ui.linear_angle_label.hide()
+
+    def on_copy_button(self):
+        self.select_tool('copy')
+        return
+
+    def on_move_button(self):
+        self.select_tool('move')
+        return
+
+    def on_pad_add(self):
+        self.select_tool('pad')
+
+    def on_pad_add_array(self):
+        self.select_tool('array')
+
+    def on_track_add(self):
+        self.select_tool('track')
+
+    def on_region_add(self):
+        self.select_tool('region')
+
+    def on_poligonize(self):
+        self.select_tool('poligonize')
+
+    def on_disc_add(self):
+        self.select_tool('disc')
+
+    def on_add_semidisc(self):
+        self.select_tool('semidisc')
+
+    def on_buffer(self):
+        buff_value = 0.01
+        log.debug("AppGerberEditor.on_buffer()")
+
+        try:
+            buff_value = float(self.ui.buffer_distance_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                buff_value = float(self.ui.buffer_distance_entry.get_value().replace(',', '.'))
+                self.ui.buffer_distance_entry.set_value(buff_value)
+            except ValueError:
+                self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                     _("Buffer distance value is missing or wrong format. Add it and retry."))
+                return
+
+        # the cb index start from 0 but the join styles for the buffer start from 1 therefore the adjustment
+        # I populated the combobox such that the index coincide with the join styles value (which is really an INT)
+        join_style = self.ui.buffer_corner_cb.currentIndex() + 1
+
+        def buffer_recursion(geom_el, selection):
+            if type(geom_el) == list:
+                geoms = []
+                for local_geom in geom_el:
+                    geoms.append(buffer_recursion(local_geom, selection=selection))
+                return geoms
+            else:
+                if geom_el in selection:
+                    geometric_data = geom_el.geo
+                    buffered_geom_el = {}
+                    if 'solid' in geometric_data:
+                        buffered_geom_el['solid'] = geometric_data['solid'].buffer(buff_value, join_style=join_style)
+                    if 'follow' in geometric_data:
+                        buffered_geom_el['follow'] = geometric_data['follow'].buffer(buff_value, join_style=join_style)
+                    if 'clear' in geometric_data:
+                        buffered_geom_el['clear'] = geometric_data['clear'].buffer(buff_value, join_style=join_style)
+                    return DrawToolShape(buffered_geom_el)
+                else:
+                    return geom_el
+
+        if not self.ui.apertures_table.selectedItems():
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("No aperture to buffer. Select at least one aperture and try again."))
+            return
+
+        for x in self.ui.apertures_table.selectedItems():
+            try:
+                apcode = self.ui.apertures_table.item(x.row(), 1).text()
+
+                temp_storage = deepcopy(buffer_recursion(self.storage_dict[apcode]['geometry'], self.selected))
+                self.storage_dict[apcode]['geometry'] = []
+                self.storage_dict[apcode]['geometry'] = temp_storage
+            except Exception as e:
+                log.debug("AppGerberEditor.buffer() --> %s" % str(e))
+                self.app.inform.emit('[ERROR_NOTCL] %s\n%s' % (_("Failed."), str(traceback.print_exc())))
+                return
+
+        self.plot_all()
+        self.app.inform.emit('[success] %s' % _("Done."))
+
+    def on_scale(self):
+        scale_factor = 1.0
+        log.debug("AppGerberEditor.on_scale()")
+
+        try:
+            scale_factor = float(self.ui.scale_factor_entry.get_value())
+        except ValueError:
+            # try to convert comma to decimal point. if it's still not working error message and return
+            try:
+                scale_factor = float(self.ui.scale_factor_entry.get_value().replace(',', '.'))
+                self.ui.scale_factor_entry.set_value(scale_factor)
+            except ValueError:
+                self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                     _("Scale factor value is missing or wrong format. Add it and retry."))
+                return
+
+        def scale_recursion(geom_el, selection):
+            if type(geom_el) == list:
+                geoms = []
+                for local_geom in geom_el:
+                    geoms.append(scale_recursion(local_geom, selection=selection))
+                return geoms
+            else:
+                if geom_el in selection:
+                    geometric_data = geom_el.geo
+                    scaled_geom_el = {}
+                    if 'solid' in geometric_data:
+                        scaled_geom_el['solid'] = affinity.scale(
+                            geometric_data['solid'], scale_factor, scale_factor, origin='center'
+                        )
+                    if 'follow' in geometric_data:
+                        scaled_geom_el['follow'] = affinity.scale(
+                            geometric_data['follow'], scale_factor, scale_factor, origin='center'
+                        )
+                    if 'clear' in geometric_data:
+                        scaled_geom_el['clear'] = affinity.scale(
+                            geometric_data['clear'], scale_factor, scale_factor, origin='center'
+                        )
+
+                    return DrawToolShape(scaled_geom_el)
+                else:
+                    return geom_el
+
+        if not self.ui.apertures_table.selectedItems():
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("No aperture to scale. Select at least one aperture and try again."))
+            return
+
+        for x in self.ui.apertures_table.selectedItems():
+            try:
+                apcode = self.ui.apertures_table.item(x.row(), 1).text()
+
+                temp_storage = deepcopy(scale_recursion(self.storage_dict[apcode]['geometry'], self.selected))
+                self.storage_dict[apcode]['geometry'] = []
+                self.storage_dict[apcode]['geometry'] = temp_storage
+
+            except Exception as e:
+                log.debug("AppGerberEditor.on_scale() --> %s" % str(e))
+
+        self.plot_all()
+        self.app.inform.emit('[success] %s' % _("Done."))
+
+    def on_markarea(self):
+        # clear previous marking
+        self.ma_annotation.clear(update=True)
+
+        self.units = self.app.defaults['units'].upper()
+
+        text = []
+        position = []
+
+        for apcode in self.storage_dict:
+            if 'geometry' in self.storage_dict[apcode]:
+                for geo_el in self.storage_dict[apcode]['geometry']:
+                    if 'solid' in geo_el.geo:
+                        area = geo_el.geo['solid'].area
+                        try:
+                            upper_threshold_val = self.ui.ma_upper_threshold_entry.get_value()
+                        except Exception:
+                            return
+
+                        try:
+                            lower_threshold_val = self.ui.ma_lower_threshold_entry.get_value()
+                        except Exception:
+                            lower_threshold_val = 0.0
+
+                        if float(upper_threshold_val) > area > float(lower_threshold_val):
+                            current_pos = geo_el.geo['solid'].exterior.coords[-1]
+                            text_elem = '%.*f' % (self.decimals, area)
+                            text.append(text_elem)
+                            position.append(current_pos)
+                            self.geo_to_delete.append(geo_el)
+
+        if text:
+            self.ma_annotation.set(text=text, pos=position, visible=True,
+                                   font_size=self.app.defaults["cncjob_annotation_fontsize"],
+                                   color='#000000FF')
+            self.app.inform.emit('[success] %s' % _("Polygons marked."))
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No polygons were marked. None fit within the limits."))
+
+    def delete_marked_polygons(self):
+        for shape_sel in self.geo_to_delete:
+            self.delete_shape(shape_sel)
+
+        self.build_ui()
+        self.plot_all()
+        self.app.inform.emit('[success] %s' % _("Done."))
+
+    def on_eraser(self):
+        self.select_tool('eraser')
+
+    def on_transform(self):
+        if type(self.active_tool) == TransformEditorGrb:
+            self.select_tool('select')
+        else:
+            self.select_tool('transform')
+
+    def hide_tool(self, tool_name):
+        # self.app.ui.notebook.setTabText(2, _("Tools"))
+        try:
+            if tool_name == 'all':
+                self.ui.apertures_frame.hide()
+            if tool_name == 'select':
+                self.ui.apertures_frame.show()
+            if tool_name == 'buffer' or tool_name == 'all':
+                self.ui.buffer_tool_frame.hide()
+            if tool_name == 'scale' or tool_name == 'all':
+                self.ui.scale_tool_frame.hide()
+            if tool_name == 'markarea' or tool_name == 'all':
+                self.ui.ma_tool_frame.hide()
+        except Exception as e:
+            log.debug("AppGerberEditor.hide_tool() --> %s" % str(e))
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
+
+
+class AppGerberEditorUI:
+    def __init__(self, app):
+        self.app = app
+
+        # Number of decimals used by tools in this class
+        self.decimals = self.app.decimals
+
+        # ## Current application units in Upper Case
+        self.units = self.app.defaults['units'].upper()
+
+        self.grb_edit_widget = QtWidgets.QWidget()
+
+        layout = QtWidgets.QVBoxLayout()
+        self.grb_edit_widget.setLayout(layout)
+
+        # Page Title box (spacing between children)
+        self.title_box = QtWidgets.QHBoxLayout()
+        layout.addLayout(self.title_box)
+
+        # Page Title icon
+        pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png')
+        self.icon = FCLabel()
+        self.icon.setPixmap(pixmap)
+        self.title_box.addWidget(self.icon, stretch=0)
+
+        # Title label
+        self.title_label = FCLabel("<font size=5><b>%s</b></font>" % _('Gerber Editor'))
+        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.title_label, stretch=1)
+
+        # Object name
+        self.name_box = QtWidgets.QHBoxLayout()
+        layout.addLayout(self.name_box)
+        name_label = FCLabel(_("Name:"))
+        self.name_box.addWidget(name_label)
+        self.name_entry = FCEntry()
+        self.name_box.addWidget(self.name_entry)
+
+        # Box for custom widgets
+        # This gets populated in offspring implementations.
+        self.custom_box = QtWidgets.QVBoxLayout()
+        layout.addLayout(self.custom_box)
+
+        # #############################################################################################################
+        # #################################### Gerber Apertures Table #################################################
+        # #############################################################################################################
+        self.apertures_table_label = FCLabel('<b>%s:</b>' % _('Apertures'))
+        self.apertures_table_label.setToolTip(
+            _("Apertures Table for the Gerber Object.")
+        )
+        self.custom_box.addWidget(self.apertures_table_label)
+
+        self.apertures_table = FCTable()
+        # delegate = SpinBoxDelegate(units=self.units)
+        # self.apertures_table.setItemDelegateForColumn(1, delegate)
+
+        self.custom_box.addWidget(self.apertures_table)
+
+        self.apertures_table.setColumnCount(5)
+        self.apertures_table.setHorizontalHeaderLabels(['#', _('Code'), _('Type'), _('Size'), _('Dim')])
+        self.apertures_table.setSortingEnabled(False)
+        self.apertures_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        self.apertures_table.horizontalHeaderItem(0).setToolTip(
+            _("Index"))
+        self.apertures_table.horizontalHeaderItem(1).setToolTip(
+            _("Aperture Code"))
+        self.apertures_table.horizontalHeaderItem(2).setToolTip(
+            _("Type of aperture: circular, rectangle, macros etc"))
+        self.apertures_table.horizontalHeaderItem(4).setToolTip(
+            _("Aperture Size:"))
+        self.apertures_table.horizontalHeaderItem(4).setToolTip(
+            _("Aperture Dimensions:\n"
+              " - (width, height) for R, O type.\n"
+              " - (dia, nVertices) for P type"))
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.custom_box.addWidget(separator_line)
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Apertures widgets
+        # this way I can hide/show the frame
+        self.apertures_frame = QtWidgets.QFrame()
+        self.apertures_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.apertures_frame)
+        self.apertures_box = QtWidgets.QVBoxLayout()
+        self.apertures_box.setContentsMargins(0, 0, 0, 0)
+        self.apertures_frame.setLayout(self.apertures_box)
+
+        # #############################################################################################################
+        # ############################ Add/Delete an new Aperture #####################################################
+        # #############################################################################################################
+        grid1 = QtWidgets.QGridLayout()
+        self.apertures_box.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+
+        # Title
+        apadd_del_lbl = FCLabel('<b>%s:</b>' % _('Add/Delete Aperture'))
+        apadd_del_lbl.setToolTip(
+            _("Add/Delete an aperture in the aperture table")
+        )
+        grid1.addWidget(apadd_del_lbl, 0, 0, 1, 2)
+
+        # Aperture Code
+        apcode_lbl = FCLabel('%s:' % _('Aperture Code'))
+        apcode_lbl.setToolTip(_("Code for the new aperture"))
+
+        self.apcode_entry = FCSpinner()
+        self.apcode_entry.set_range(0, 1000)
+        self.apcode_entry.setWrapping(True)
+
+        grid1.addWidget(apcode_lbl, 1, 0)
+        grid1.addWidget(self.apcode_entry, 1, 1)
+
+        # Aperture Size
+        apsize_lbl = FCLabel('%s' % _('Aperture Size:'))
+        apsize_lbl.setToolTip(
+            _("Size for the new aperture.\n"
+              "If aperture type is 'R' or 'O' then\n"
+              "this value is automatically\n"
+              "calculated as:\n"
+              "sqrt(width**2 + height**2)")
+        )
+
+        self.apsize_entry = FCDoubleSpinner()
+        self.apsize_entry.set_precision(self.decimals)
+        self.apsize_entry.set_range(0.0, 10000.0000)
+
+        grid1.addWidget(apsize_lbl, 2, 0)
+        grid1.addWidget(self.apsize_entry, 2, 1)
+
+        # Aperture Type
+        aptype_lbl = FCLabel('%s:' % _('Aperture Type'))
+        aptype_lbl.setToolTip(
+            _("Select the type of new aperture. Can be:\n"
+              "C = circular\n"
+              "R = rectangular\n"
+              "O = oblong")
+        )
+
+        self.aptype_cb = FCComboBox()
+        self.aptype_cb.addItems(['C', 'R', 'O'])
+
+        grid1.addWidget(aptype_lbl, 3, 0)
+        grid1.addWidget(self.aptype_cb, 3, 1)
+
+        # Aperture Dimensions
+        self.apdim_lbl = FCLabel('%s:' % _('Aperture Dim'))
+        self.apdim_lbl.setToolTip(
+            _("Dimensions for the new aperture.\n"
+              "Active only for rectangular apertures (type R).\n"
+              "The format is (width, height)")
+        )
+
+        self.apdim_entry = EvalEntry2()
+
+        grid1.addWidget(self.apdim_lbl, 4, 0)
+        grid1.addWidget(self.apdim_entry, 4, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 6, 0, 1, 2)
+
+        # Aperture Buttons
+        hlay_ad = QtWidgets.QHBoxLayout()
+        grid1.addLayout(hlay_ad, 8, 0, 1, 2)
+
+        self.addaperture_btn = FCButton(_('Add'))
+        self.addaperture_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png'))
+        self.addaperture_btn.setToolTip(
+            _("Add a new aperture to the aperture list.")
+        )
+
+        self.delaperture_btn = FCButton(_('Delete'))
+        self.delaperture_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/trash32.png'))
+        self.delaperture_btn.setToolTip(
+            _("Delete a aperture in the aperture list")
+        )
+        hlay_ad.addWidget(self.addaperture_btn)
+        hlay_ad.addWidget(self.delaperture_btn)
+
+        # #############################################################################################################
+        # ############################################ BUFFER TOOL ####################################################
+        # #############################################################################################################
+        self.buffer_tool_frame = QtWidgets.QFrame()
+        self.buffer_tool_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.buffer_tool_frame)
+        self.buffer_tools_box = QtWidgets.QVBoxLayout()
+        self.buffer_tools_box.setContentsMargins(0, 0, 0, 0)
+        self.buffer_tool_frame.setLayout(self.buffer_tools_box)
+        self.buffer_tool_frame.hide()
+
+        # Title
+        buf_title_lbl = FCLabel('<b>%s:</b>' % _('Buffer Aperture'))
+        buf_title_lbl.setToolTip(
+            _("Buffer a aperture in the aperture list")
+        )
+        self.buffer_tools_box.addWidget(buf_title_lbl)
+
+        # Form Layout
+        buf_form_layout = QtWidgets.QFormLayout()
+        self.buffer_tools_box.addLayout(buf_form_layout)
+
+        # Buffer distance
+        self.buffer_distance_entry = FCDoubleSpinner()
+        self.buffer_distance_entry.set_precision(self.decimals)
+        self.buffer_distance_entry.set_range(-10000.0000, 10000.0000)
+
+        buf_form_layout.addRow('%s:' % _("Buffer distance"), self.buffer_distance_entry)
+
+        # Buffer Corner
+        self.buffer_corner_lbl = FCLabel('%s:' % _("Buffer corner"))
+        self.buffer_corner_lbl.setToolTip(
+            _("There are 3 types of corners:\n"
+              " - 'Round': the corner is rounded.\n"
+              " - 'Square': the corner is met in a sharp angle.\n"
+              " - 'Beveled': the corner is a line that directly connects the features meeting in the corner")
+        )
+        self.buffer_corner_cb = FCComboBox()
+        self.buffer_corner_cb.addItem(_("Round"))
+        self.buffer_corner_cb.addItem(_("Square"))
+        self.buffer_corner_cb.addItem(_("Beveled"))
+        buf_form_layout.addRow(self.buffer_corner_lbl, self.buffer_corner_cb)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        buf_form_layout.addRow(separator_line)
+
+        # Buttons
+        hlay_buf = QtWidgets.QHBoxLayout()
+        self.buffer_tools_box.addLayout(hlay_buf)
+
+        self.buffer_button = FCButton(_("Buffer"))
+        self.buffer_button.setIcon(QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'))
+        hlay_buf.addWidget(self.buffer_button)
+
+        # #############################################################################################################
+        # ########################################### SCALE TOOL ######################################################
+        # #############################################################################################################
+        self.scale_tool_frame = QtWidgets.QFrame()
+        self.scale_tool_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.scale_tool_frame)
+        self.scale_tools_box = QtWidgets.QVBoxLayout()
+        self.scale_tools_box.setContentsMargins(0, 0, 0, 0)
+        self.scale_tool_frame.setLayout(self.scale_tools_box)
+        self.scale_tool_frame.hide()
+
+        # Title
+        scale_title_lbl = FCLabel('<b>%s:</b>' % _('Scale Aperture'))
+        scale_title_lbl.setToolTip(
+            _("Scale a aperture in the aperture list")
+        )
+        self.scale_tools_box.addWidget(scale_title_lbl)
+
+        # Form Layout
+        scale_form_layout = QtWidgets.QFormLayout()
+        self.scale_tools_box.addLayout(scale_form_layout)
+
+        self.scale_factor_lbl = FCLabel('%s:' % _("Scale factor"))
+        self.scale_factor_lbl.setToolTip(
+            _("The factor by which to scale the selected aperture.\n"
+              "Values can be between 0.0000 and 999.9999")
+        )
+        self.scale_factor_entry = FCDoubleSpinner()
+        self.scale_factor_entry.set_precision(self.decimals)
+        self.scale_factor_entry.set_range(0.0000, 10000.0000)
+
+        scale_form_layout.addRow(self.scale_factor_lbl, self.scale_factor_entry)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        scale_form_layout.addRow(separator_line)
+
+        # Buttons
+        hlay_scale = QtWidgets.QHBoxLayout()
+        self.scale_tools_box.addLayout(hlay_scale)
+
+        self.scale_button = FCButton(_("Scale"))
+        self.scale_button.setIcon(QtGui.QIcon(self.app.resource_location + '/clean32.png'))
+        hlay_scale.addWidget(self.scale_button)
+
+        # #############################################################################################################
+        # ######################################### Mark Area TOOL ####################################################
+        # #############################################################################################################
+        self.ma_tool_frame = QtWidgets.QFrame()
+        self.ma_tool_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.ma_tool_frame)
+        self.ma_tools_box = QtWidgets.QVBoxLayout()
+        self.ma_tools_box.setContentsMargins(0, 0, 0, 0)
+        self.ma_tool_frame.setLayout(self.ma_tools_box)
+        self.ma_tool_frame.hide()
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.ma_tools_box.addWidget(separator_line)
+
+        # Title
+        ma_title_lbl = FCLabel('<b>%s:</b>' % _('Mark polygons'))
+        ma_title_lbl.setToolTip(
+            _("Mark the polygon areas.")
+        )
+        self.ma_tools_box.addWidget(ma_title_lbl)
+
+        # Form Layout
+        ma_form_layout = QtWidgets.QFormLayout()
+        self.ma_tools_box.addLayout(ma_form_layout)
+
+        self.ma_upper_threshold_lbl = FCLabel('%s:' % _("Area UPPER threshold"))
+        self.ma_upper_threshold_lbl.setToolTip(
+            _("The threshold value, all areas less than this are marked.\n"
+              "Can have a value between 0.0000 and 10000.0000")
+        )
+        self.ma_upper_threshold_entry = FCDoubleSpinner()
+        self.ma_upper_threshold_entry.set_precision(self.decimals)
+        self.ma_upper_threshold_entry.set_range(0, 10000)
+
+        self.ma_lower_threshold_lbl = FCLabel('%s:' % _("Area LOWER threshold"))
+        self.ma_lower_threshold_lbl.setToolTip(
+            _("The threshold value, all areas more than this are marked.\n"
+              "Can have a value between 0.0000 and 10000.0000")
+        )
+        self.ma_lower_threshold_entry = FCDoubleSpinner()
+        self.ma_lower_threshold_entry.set_precision(self.decimals)
+        self.ma_lower_threshold_entry.set_range(0, 10000)
+
+        ma_form_layout.addRow(self.ma_lower_threshold_lbl, self.ma_lower_threshold_entry)
+        ma_form_layout.addRow(self.ma_upper_threshold_lbl, self.ma_upper_threshold_entry)
+
+        # Buttons
+        hlay_ma = QtWidgets.QHBoxLayout()
+        self.ma_tools_box.addLayout(hlay_ma)
+
+        self.ma_threshold_button = FCButton(_("Mark"))
+        self.ma_threshold_button.setIcon(QtGui.QIcon(self.app.resource_location + '/markarea32.png'))
+        self.ma_threshold_button.setToolTip(
+            _("Mark the polygons that fit within limits.")
+        )
+        hlay_ma.addWidget(self.ma_threshold_button)
+
+        self.ma_delete_button = FCButton(_("Delete"))
+        self.ma_delete_button.setIcon(QtGui.QIcon(self.app.resource_location + '/trash32.png'))
+        self.ma_delete_button.setToolTip(
+            _("Delete all the marked polygons.")
+        )
+        hlay_ma.addWidget(self.ma_delete_button)
+
+        self.ma_clear_button = FCButton(_("Clear"))
+        self.ma_clear_button.setIcon(QtGui.QIcon(self.app.resource_location + '/clean32.png'))
+        self.ma_clear_button.setToolTip(
+            _("Clear all the markings.")
+        )
+        hlay_ma.addWidget(self.ma_clear_button)
+
+        # #############################################################################################################
+        # ######################################### Add Pad Array #####################################################
+        # #############################################################################################################
+        self.array_frame = QtWidgets.QFrame()
+        self.array_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.array_frame)
+        self.array_box = QtWidgets.QVBoxLayout()
+        self.array_box.setContentsMargins(0, 0, 0, 0)
+        self.array_frame.setLayout(self.array_box)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.array_box.addWidget(separator_line)
+
+        array_grid = QtWidgets.QGridLayout()
+        array_grid.setColumnStretch(0, 0)
+        array_grid.setColumnStretch(1, 1)
+        self.array_box.addLayout(array_grid)
+
+        # Title
+        self.padarray_label = FCLabel('<b>%s</b>' % _("Add Pad Array"))
+        self.padarray_label.setToolTip(
+            _("Add an array of pads (linear or circular array)")
+        )
+        array_grid.addWidget(self.padarray_label, 0, 0, 1, 2)
+
+        # Array Type
+        array_type_lbl = FCLabel('%s:' % _("Type"))
+        array_type_lbl.setToolTip(
+            _("Select the type of pads array to create.\n"
+              "It can be Linear X(Y) or Circular")
+        )
+
+        self.array_type_radio = RadioSet([{'label': _('Linear'), 'value': 'linear'},
+                                          {'label': _('Circular'), 'value': 'circular'}])
+
+        array_grid.addWidget(array_type_lbl, 2, 0)
+        array_grid.addWidget(self.array_type_radio, 2, 1)
+
+        # Number of Pads in Array
+        pad_array_size_label = FCLabel('%s:' % _('Nr of pads'))
+        pad_array_size_label.setToolTip(
+            _("Specify how many pads to be in the array.")
+        )
+
+        self.pad_array_size_entry = FCSpinner()
+        self.pad_array_size_entry.set_range(1, 10000)
+
+        array_grid.addWidget(pad_array_size_label, 4, 0)
+        array_grid.addWidget(self.pad_array_size_entry, 4, 1)
+
+        # #############################################################################################################
+        # ############################ Linear Pad Array ###############################################################
+        # #############################################################################################################
+        self.lin_separator_line = QtWidgets.QFrame()
+        self.lin_separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        self.lin_separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        array_grid.addWidget(self.lin_separator_line, 6, 0, 1, 2)
+
+        # Linear Direction
+        self.pad_axis_label = FCLabel('%s:' % _('Direction'))
+        self.pad_axis_label.setToolTip(
+            _("Direction on which the linear array is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the array inclination")
+        )
+
+        self.pad_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                        {'label': _('Y'), 'value': 'Y'},
+                                        {'label': _('Angle'), 'value': 'A'}])
+
+        array_grid.addWidget(self.pad_axis_label, 8, 0)
+        array_grid.addWidget(self.pad_axis_radio, 8, 1)
+
+        # Linear Pitch
+        self.pad_pitch_label = FCLabel('%s:' % _('Pitch'))
+        self.pad_pitch_label.setToolTip(
+            _("Pitch = Distance between elements of the array.")
+        )
+
+        self.pad_pitch_entry = FCDoubleSpinner()
+        self.pad_pitch_entry.set_precision(self.decimals)
+        self.pad_pitch_entry.set_range(0.0000, 10000.0000)
+        self.pad_pitch_entry.setSingleStep(0.1)
+
+        array_grid.addWidget(self.pad_pitch_label, 10, 0)
+        array_grid.addWidget(self.pad_pitch_entry, 10, 1)
+
+        # Linear Angle
+        self.linear_angle_label = FCLabel('%s:' % _('Angle'))
+        self.linear_angle_label.setToolTip(
+            _("Angle at which the linear array is placed.\n"
+              "The precision is of max 2 decimals.\n"
+              "Min value is: -360.00 degrees.\n"
+              "Max value is: 360.00 degrees.")
+        )
+
+        self.linear_angle_spinner = FCDoubleSpinner()
+        self.linear_angle_spinner.set_precision(self.decimals)
+        self.linear_angle_spinner.setRange(-360.00, 360.00)
+
+        array_grid.addWidget(self.linear_angle_label, 12, 0)
+        array_grid.addWidget(self.linear_angle_spinner, 12, 1)
+
+        # #############################################################################################################
+        # ################################### Circular Pad Array ######################################################
+        # #############################################################################################################
+        self.circ_separator_line = QtWidgets.QFrame()
+        self.circ_separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        self.circ_separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        array_grid.addWidget(self.circ_separator_line, 14, 0, 1, 2)
+
+        # Circular Direction
+        self.pad_direction_label = FCLabel('%s:' % _('Direction'))
+        self.pad_direction_label.setToolTip(
+            _("Direction for circular array.\n"
+              "Can be CW = clockwise or CCW = counter clockwise.")
+        )
+
+        self.pad_direction_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                             {'label': _('CCW'), 'value': 'CCW'}])
+
+        array_grid.addWidget(self.pad_direction_label, 16, 0)
+        array_grid.addWidget(self.pad_direction_radio, 16, 1)
+
+        # Circular Angle
+        self.pad_angle_label = FCLabel('%s:' % _('Angle'))
+        self.pad_angle_label.setToolTip(
+            _("Angle at which each element in circular array is placed.")
+        )
+
+        self.pad_angle_entry = FCDoubleSpinner()
+        self.pad_angle_entry.set_precision(self.decimals)
+        self.pad_angle_entry.set_range(-360.00, 360.00)
+        self.pad_angle_entry.setSingleStep(0.1)
+
+        array_grid.addWidget(self.pad_angle_label, 18, 0)
+        array_grid.addWidget(self.pad_angle_entry, 18, 1)
+
+        self.custom_box.addStretch()
+        layout.addStretch()
+
+        # Editor
+        self.exit_editor_button = FCButton(_('Exit Editor'))
+        self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
+        self.exit_editor_button.setToolTip(
+            _("Exit from Editor.")
+        )
+        self.exit_editor_button.setStyleSheet("""
+                                              QPushButton
+                                              {
+                                                  font-weight: bold;
+                                              }
+                                              """)
+        layout.addWidget(self.exit_editor_button)
+
+
+class TransformEditorTool(AppTool):
+    """
+    Inputs to specify how to paint the selected polygons.
+    """
+
+    toolName = _("Transform Tool")
+    rotateName = _("Rotate")
+    skewName = _("Skew/Shear")
+    scaleName = _("Scale")
+    flipName = _("Mirror (Flip)")
+    offsetName = _("Offset")
+    bufferName = _("Buffer")
+
+    def __init__(self, app, draw_app):
+        AppTool.__init__(self, app)
+
+        self.app = app
+        self.draw_app = draw_app
+        self.decimals = self.app.decimals
+
+        # ## Title
+        title_label = FCLabel("%s" % self.toolName)
+        title_label.setStyleSheet("""
+                                        QLabel
+                                        {
+                                            font-size: 16px;
+                                            font-weight: bold;
+                                        }
+                                        """)
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(FCLabel(''))
+
+        # ## Layout
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        grid0.setColumnStretch(2, 0)
+
+        grid0.addWidget(FCLabel(''))
+
+        # Reference
+        ref_label = FCLabel('%s:' % _("Reference"))
+        ref_label.setToolTip(
+            _("The reference point for Rotate, Skew, Scale, Mirror.\n"
+              "Can be:\n"
+              "- Origin -> it is the 0, 0 point\n"
+              "- Selection -> the center of the bounding box of the selected objects\n"
+              "- Point -> a custom point defined by X,Y coordinates\n"
+              "- Min Selection -> the point (minx, miny) of the bounding box of the selection")
+        )
+        self.ref_combo = FCComboBox()
+        self.ref_items = [_("Origin"), _("Selection"), _("Point"), _("Minimum")]
+        self.ref_combo.addItems(self.ref_items)
+
+        grid0.addWidget(ref_label, 0, 0)
+        grid0.addWidget(self.ref_combo, 0, 1, 1, 2)
+
+        self.point_label = FCLabel('%s:' % _("Value"))
+        self.point_label.setToolTip(
+            _("A point of reference in format X,Y.")
+        )
+        self.point_entry = NumericalEvalTupleEntry()
+
+        grid0.addWidget(self.point_label, 1, 0)
+        grid0.addWidget(self.point_entry, 1, 1, 1, 2)
+
+        self.point_button = FCButton(_("Add"))
+        self.point_button.setToolTip(
+            _("Add point coordinates from clipboard.")
+        )
+        grid0.addWidget(self.point_button, 2, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 5, 0, 1, 3)
+
+        # ## Rotate Title
+        rotate_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.rotateName)
+        grid0.addWidget(rotate_title_label, 6, 0, 1, 3)
+
+        self.rotate_label = FCLabel('%s:' % _("Angle"))
+        self.rotate_label.setToolTip(
+            _("Angle, in degrees.\n"
+              "Float number between -360 and 359.\n"
+              "Positive numbers for CW motion.\n"
+              "Negative numbers for CCW motion.")
+        )
+
+        self.rotate_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.rotate_entry.set_precision(self.decimals)
+        self.rotate_entry.setSingleStep(45)
+        self.rotate_entry.setWrapping(True)
+        self.rotate_entry.set_range(-360, 360)
+
+        # self.rotate_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+
+        self.rotate_button = FCButton(_("Rotate"))
+        self.rotate_button.setToolTip(
+            _("Rotate the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects.")
+        )
+        self.rotate_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.rotate_label, 7, 0)
+        grid0.addWidget(self.rotate_entry, 7, 1)
+        grid0.addWidget(self.rotate_button, 7, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 8, 0, 1, 3)
+
+        # ## Skew Title
+        skew_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.skewName)
+        grid0.addWidget(skew_title_label, 9, 0, 1, 2)
+
+        self.skew_link_cb = FCCheckBox()
+        self.skew_link_cb.setText(_("Link"))
+        self.skew_link_cb.setToolTip(
+            _("Link the Y entry to X entry and copy its content.")
+        )
+
+        grid0.addWidget(self.skew_link_cb, 9, 2)
+
+        self.skewx_label = FCLabel('%s:' % _("X angle"))
+        self.skewx_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 360.")
+        )
+        self.skewx_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.skewx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.skewx_entry.set_precision(self.decimals)
+        self.skewx_entry.set_range(-360, 360)
+
+        self.skewx_button = FCButton(_("Skew X"))
+        self.skewx_button.setToolTip(
+            _("Skew/shear the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects."))
+        self.skewx_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.skewx_label, 10, 0)
+        grid0.addWidget(self.skewx_entry, 10, 1)
+        grid0.addWidget(self.skewx_button, 10, 2)
+
+        self.skewy_label = FCLabel('%s:' % _("Y angle"))
+        self.skewy_label.setToolTip(
+            _("Angle for Skew action, in degrees.\n"
+              "Float number between -360 and 360.")
+        )
+        self.skewy_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.skewy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.skewy_entry.set_precision(self.decimals)
+        self.skewy_entry.set_range(-360, 360)
+
+        self.skewy_button = FCButton(_("Skew Y"))
+        self.skewy_button.setToolTip(
+            _("Skew/shear the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects."))
+        self.skewy_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.skewy_label, 12, 0)
+        grid0.addWidget(self.skewy_entry, 12, 1)
+        grid0.addWidget(self.skewy_button, 12, 2)
+
+        self.ois_sk = OptionalInputSection(self.skew_link_cb, [self.skewy_label, self.skewy_entry, self.skewy_button],
+                                           logic=False)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 14, 0, 1, 3)
+
+        # ## Scale Title
+        scale_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.scaleName)
+        grid0.addWidget(scale_title_label, 15, 0, 1, 2)
+
+        self.scale_link_cb = FCCheckBox()
+        self.scale_link_cb.setText(_("Link"))
+        self.scale_link_cb.setToolTip(
+            _("Link the Y entry to X entry and copy its content.")
+        )
+
+        grid0.addWidget(self.scale_link_cb, 15, 2)
+
+        self.scalex_label = FCLabel('%s:' % _("X factor"))
+        self.scalex_label.setToolTip(
+            _("Factor for scaling on X axis.")
+        )
+        self.scalex_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.scalex_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.scalex_entry.set_precision(self.decimals)
+        self.scalex_entry.setMinimum(-1e6)
+
+        self.scalex_button = FCButton(_("Scale X"))
+        self.scalex_button.setToolTip(
+            _("Scale the selected object(s).\n"
+              "The point of reference depends on \n"
+              "the Scale reference checkbox state."))
+        self.scalex_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.scalex_label, 17, 0)
+        grid0.addWidget(self.scalex_entry, 17, 1)
+        grid0.addWidget(self.scalex_button, 17, 2)
+
+        self.scaley_label = FCLabel('%s:' % _("Y factor"))
+        self.scaley_label.setToolTip(
+            _("Factor for scaling on Y axis.")
+        )
+        self.scaley_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.scaley_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.scaley_entry.set_precision(self.decimals)
+        self.scaley_entry.setMinimum(-1e6)
+
+        self.scaley_button = FCButton(_("Scale Y"))
+        self.scaley_button.setToolTip(
+            _("Scale the selected object(s).\n"
+              "The point of reference depends on \n"
+              "the Scale reference checkbox state."))
+        self.scaley_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.scaley_label, 19, 0)
+        grid0.addWidget(self.scaley_entry, 19, 1)
+        grid0.addWidget(self.scaley_button, 19, 2)
+
+        self.ois_s = OptionalInputSection(self.scale_link_cb,
+                                          [
+                                              self.scaley_label,
+                                              self.scaley_entry,
+                                              self.scaley_button
+                                          ], logic=False)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 21, 0, 1, 3)
+
+        # ## Flip Title
+        flip_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.flipName)
+        grid0.addWidget(flip_title_label, 23, 0, 1, 3)
+
+        self.flipx_button = FCButton(_("Flip on X"))
+        self.flipx_button.setToolTip(
+            _("Flip the selected object(s) over the X axis.")
+        )
+
+        self.flipy_button = FCButton(_("Flip on Y"))
+        self.flipy_button.setToolTip(
+            _("Flip the selected object(s) over the X axis.")
+        )
+
+        hlay0 = QtWidgets.QHBoxLayout()
+        grid0.addLayout(hlay0, 25, 0, 1, 3)
+
+        hlay0.addWidget(self.flipx_button)
+        hlay0.addWidget(self.flipy_button)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 27, 0, 1, 3)
+
+        # ## Offset Title
+        offset_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.offsetName)
+        grid0.addWidget(offset_title_label, 29, 0, 1, 3)
+
+        self.offx_label = FCLabel('%s:' % _("X val"))
+        self.offx_label.setToolTip(
+            _("Distance to offset on X axis. In current units.")
+        )
+        self.offx_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.offx_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.offx_entry.set_precision(self.decimals)
+        self.offx_entry.setMinimum(-1e6)
+
+        self.offx_button = FCButton(_("Offset X"))
+        self.offx_button.setToolTip(
+            _("Offset the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects.\n"))
+        self.offx_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.offx_label, 31, 0)
+        grid0.addWidget(self.offx_entry, 31, 1)
+        grid0.addWidget(self.offx_button, 31, 2)
+
+        self.offy_label = FCLabel('%s:' % _("Y val"))
+        self.offy_label.setToolTip(
+            _("Distance to offset on Y axis. In current units.")
+        )
+        self.offy_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        # self.offy_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.offy_entry.set_precision(self.decimals)
+        self.offy_entry.setMinimum(-1e6)
+
+        self.offy_button = FCButton(_("Offset Y"))
+        self.offy_button.setToolTip(
+            _("Offset the selected object(s).\n"
+              "The point of reference is the middle of\n"
+              "the bounding box for all selected objects.\n"))
+        self.offy_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.offy_label, 32, 0)
+        grid0.addWidget(self.offy_entry, 32, 1)
+        grid0.addWidget(self.offy_button, 32, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 34, 0, 1, 3)
+
+        # ## Buffer Title
+        buffer_title_label = FCLabel("<font size=3><b>%s</b></font>" % self.bufferName)
+        grid0.addWidget(buffer_title_label, 35, 0, 1, 2)
+
+        self.buffer_rounded_cb = FCCheckBox('%s' % _("Rounded"))
+        self.buffer_rounded_cb.setToolTip(
+            _("If checked then the buffer will surround the buffered shape,\n"
+              "every corner will be rounded.\n"
+              "If not checked then the buffer will follow the exact geometry\n"
+              "of the buffered shape.")
+        )
+
+        grid0.addWidget(self.buffer_rounded_cb, 35, 2)
+
+        self.buffer_label = FCLabel('%s:' % _("Distance"))
+        self.buffer_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased with the 'distance'.")
+        )
+
+        self.buffer_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.buffer_entry.set_precision(self.decimals)
+        self.buffer_entry.setSingleStep(0.1)
+        self.buffer_entry.setWrapping(True)
+        self.buffer_entry.set_range(-10000.0000, 10000.0000)
+
+        self.buffer_button = FCButton(_("Buffer D"))
+        self.buffer_button.setToolTip(
+            _("Create the buffer effect on each geometry,\n"
+              "element from the selected object, using the distance.")
+        )
+        self.buffer_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.buffer_label, 37, 0)
+        grid0.addWidget(self.buffer_entry, 37, 1)
+        grid0.addWidget(self.buffer_button, 37, 2)
+
+        self.buffer_factor_label = FCLabel('%s:' % _("Value"))
+        self.buffer_factor_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased to fit the 'Value'. Value is a percentage\n"
+              "of the initial dimension.")
+        )
+
+        self.buffer_factor_entry = FCDoubleSpinner(callback=self.confirmation_message, suffix='%')
+        self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
+        self.buffer_factor_entry.set_precision(self.decimals)
+        self.buffer_factor_entry.setWrapping(True)
+        self.buffer_factor_entry.setSingleStep(1)
+
+        self.buffer_factor_button = FCButton(_("Buffer F"))
+        self.buffer_factor_button.setToolTip(
+            _("Create the buffer effect on each geometry,\n"
+              "element from the selected object, using the factor.")
+        )
+        self.buffer_factor_button.setMinimumWidth(90)
+
+        grid0.addWidget(self.buffer_factor_label, 38, 0)
+        grid0.addWidget(self.buffer_factor_entry, 38, 1)
+        grid0.addWidget(self.buffer_factor_button, 38, 2)
+
+        grid0.addWidget(FCLabel(''), 42, 0, 1, 3)
+
+        self.layout.addStretch()
+
+        # Signals
+        self.ref_combo.currentIndexChanged.connect(self.on_reference_changed)
+        self.point_button.clicked.connect(self.on_add_coords)
+
+        self.rotate_button.clicked.connect(self.on_rotate)
+
+        self.skewx_button.clicked.connect(self.on_skewx)
+        self.skewy_button.clicked.connect(self.on_skewy)
+
+        self.scalex_button.clicked.connect(self.on_scalex)
+        self.scaley_button.clicked.connect(self.on_scaley)
+
+        self.offx_button.clicked.connect(self.on_offx)
+        self.offy_button.clicked.connect(self.on_offy)
+
+        self.flipx_button.clicked.connect(self.on_flipx)
+        self.flipy_button.clicked.connect(self.on_flipy)
+
+        self.buffer_button.clicked.connect(self.on_buffer_by_distance)
+        self.buffer_factor_button.clicked.connect(self.on_buffer_by_factor)
+
+        # self.rotate_entry.editingFinished.connect(self.on_rotate)
+        # self.skewx_entry.editingFinished.connect(self.on_skewx)
+        # self.skewy_entry.editingFinished.connect(self.on_skewy)
+        # self.scalex_entry.editingFinished.connect(self.on_scalex)
+        # self.scaley_entry.editingFinished.connect(self.on_scaley)
+        # self.offx_entry.editingFinished.connect(self.on_offx)
+        # self.offy_entry.editingFinished.connect(self.on_offy)
+
+        self.set_tool_ui()
+
+    def run(self, toggle=True):
+        self.app.defaults.report_usage("Gerber Editor Transform Tool()")
+
+        # if the splitter is hidden, display it, else hide it but only if the current widget is the same
+        if self.app.ui.splitter.sizes()[0] == 0:
+            self.app.ui.splitter.setSizes([1, 1])
+
+        if toggle:
+            try:
+                if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
+                    self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
+                else:
+                    self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
+            except AttributeError:
+                pass
+
+        AppTool.run(self)
+        self.set_tool_ui()
+
+        self.app.ui.notebook.setTabText(2, _("Transform Tool"))
+
+    def install(self, icon=None, separator=None, **kwargs):
+        AppTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
+
+    def set_tool_ui(self):
+        # Initialize form
+        ref_val = self.app.defaults["tools_transform_reference"]
+        if ref_val == _("Object"):
+            ref_val = _("Selection")
+        self.ref_combo.set_value(ref_val)
+        self.point_entry.set_value(self.app.defaults["tools_transform_ref_point"])
+        self.rotate_entry.set_value(self.app.defaults["tools_transform_rotate"])
+
+        self.skewx_entry.set_value(self.app.defaults["tools_transform_skew_x"])
+        self.skewy_entry.set_value(self.app.defaults["tools_transform_skew_y"])
+        self.skew_link_cb.set_value(self.app.defaults["tools_transform_skew_link"])
+
+        self.scalex_entry.set_value(self.app.defaults["tools_transform_scale_x"])
+        self.scaley_entry.set_value(self.app.defaults["tools_transform_scale_y"])
+        self.scale_link_cb.set_value(self.app.defaults["tools_transform_scale_link"])
+
+        self.offx_entry.set_value(self.app.defaults["tools_transform_offset_x"])
+        self.offy_entry.set_value(self.app.defaults["tools_transform_offset_y"])
+
+        self.buffer_entry.set_value(self.app.defaults["tools_transform_buffer_dis"])
+        self.buffer_factor_entry.set_value(self.app.defaults["tools_transform_buffer_factor"])
+        self.buffer_rounded_cb.set_value(self.app.defaults["tools_transform_buffer_corner"])
+
+        # initial state is hidden
+        self.point_label.hide()
+        self.point_entry.hide()
+        self.point_button.hide()
+
+    def template(self):
+        if not self.draw_app.selected:
+            self.draw_app.app.inform.emit('[WARNING_NOTCL] %s %s' % (_("Cancelled."), _("No shape selected.")))
+            return
+
+        self.draw_app.select_tool("select")
+        self.app.ui.notebook.setTabText(2, "Tools")
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
+
+        self.app.ui.splitter.setSizes([0, 1])
+
+    def on_reference_changed(self, index):
+        if index == 0 or index == 1:  # "Origin" or "Selection" reference
+            self.point_label.hide()
+            self.point_entry.hide()
+            self.point_button.hide()
+
+        elif index == 2:    # "Point" reference
+            self.point_label.show()
+            self.point_entry.show()
+            self.point_button.show()
+
+    def on_calculate_reference(self, ref_index=None):
+        if ref_index:
+            ref_val = ref_index
+        else:
+            ref_val = self.ref_combo.currentIndex()
+
+        if ref_val == 0:    # "Origin" reference
+            return 0, 0
+        elif ref_val == 1:  # "Selection" reference
+            sel_list = self.draw_app.selected
+            if sel_list:
+                xmin, ymin, xmax, ymax = self.alt_bounds(sel_list)
+                px = (xmax + xmin) * 0.5
+                py = (ymax + ymin) * 0.5
+                return px, py
+            else:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("No shape selected."))
+                return "fail"
+        elif ref_val == 2:  # "Point" reference
+            point_val = self.point_entry.get_value()
+            try:
+                px, py = eval('{}'.format(point_val))
+                return px, py
+            except Exception:
+                self.app.inform.emit('[WARNING_NOTCL] %s' % _("Incorrect format for Point value. Needs format X,Y"))
+                return "fail"
+        else:
+            sel_list = self.draw_app.selected
+            if sel_list:
+                xmin, ymin, xmax, ymax = self.alt_bounds(sel_list)
+                if ref_val == 3:
+                    return xmin, ymin   # lower left corner
+                elif ref_val == 4:
+                    return xmax, ymin   # lower right corner
+                elif ref_val == 5:
+                    return xmax, ymax   # upper right corner
+                else:
+                    return xmin, ymax   # upper left corner
+            else:
+                self.app.inform.emit('[ERROR_NOTCL] %s' % _("No shape selected."))
+                return "fail"
+
+    def on_add_coords(self):
+        val = self.app.clipboard.text()
+        self.point_entry.set_value(val)
+
+    def on_rotate(self, sig=None, val=None, ref=None):
+        value = float(self.rotate_entry.get_value()) if val is None else val
+        if value == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Rotate transformation can not be done for a value of 0."))
+            return
+        point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref)
+        if point == 'fail':
+            return
+        self.app.worker_task.emit({'fcn': self.on_rotate_action, 'params': [value, point]})
+
+    def on_flipx(self, signal=None, ref=None):
+        axis = 'Y'
+        point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref)
+        if point == 'fail':
+            return
+        self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
+
+    def on_flipy(self, signal=None, ref=None):
+        axis = 'X'
+        point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref)
+        if point == 'fail':
+            return
+        self.app.worker_task.emit({'fcn': self.on_flip, 'params': [axis, point]})
+
+    def on_skewx(self, signal=None, val=None, ref=None):
+        xvalue = float(self.skewx_entry.get_value()) if val is None else val
+
+        if xvalue == 0:
+            return
+
+        if self.skew_link_cb.get_value():
+            yvalue = xvalue
+        else:
+            yvalue = 0
+
+        axis = 'X'
+        point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref)
+        if point == 'fail':
+            return
+
+        self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
+
+    def on_skewy(self, signal=None, val=None, ref=None):
+        xvalue = 0
+        yvalue = float(self.skewy_entry.get_value()) if val is None else val
+
+        if yvalue == 0:
+            return
+
+        axis = 'Y'
+        point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref)
+        if point == 'fail':
+            return
+
+        self.app.worker_task.emit({'fcn': self.on_skew, 'params': [axis, xvalue, yvalue, point]})
+
+    def on_scalex(self, signal=None, val=None, ref=None):
+        xvalue = float(self.scalex_entry.get_value()) if val is None else val
+
+        if xvalue == 0 or xvalue == 1:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Scale transformation can not be done for a factor of 0 or 1."))
+            return
+
+        if self.scale_link_cb.get_value():
+            yvalue = xvalue
+        else:
+            yvalue = 1
+
+        axis = 'X'
+        point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref)
+        if point == 'fail':
+            return
+
+        self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
+
+    def on_scaley(self, signal=None, val=None, ref=None):
+        xvalue = 1
+        yvalue = float(self.scaley_entry.get_value()) if val is None else val
+
+        if yvalue == 0 or yvalue == 1:
+            self.app.inform.emit('[WARNING_NOTCL] %s' %
+                                 _("Scale transformation can not be done for a factor of 0 or 1."))
+            return
+
+        axis = 'Y'
+        point = self.on_calculate_reference() if ref is None else self.on_calculate_reference(ref_index=ref)
+        if point == 'fail':
+            return
+
+        self.app.worker_task.emit({'fcn': self.on_scale, 'params': [axis, xvalue, yvalue, point]})
+
+    def on_offx(self, signal=None, val=None):
+        value = float(self.offx_entry.get_value()) if val is None else val
+        if value == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
+            return
+        axis = 'X'
+
+        self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
+
+    def on_offy(self, signal=None, val=None):
+        value = float(self.offy_entry.get_value()) if val is None else val
+        if value == 0:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Offset transformation can not be done for a value of 0."))
+            return
+        axis = 'Y'
+
+        self.app.worker_task.emit({'fcn': self.on_offset, 'params': [axis, value]})
+
+    def on_buffer_by_distance(self):
+        value = self.buffer_entry.get_value()
+        join = 1 if self.buffer_rounded_cb.get_value() else 2
+
+        self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join]})
+
+    def on_buffer_by_factor(self):
+        value = 1 + (self.buffer_factor_entry.get_value() / 100.0)
+        join = 1 if self.buffer_rounded_cb.get_value() else 2
+
+        # tell the buffer method to use the factor
+        factor = True
+
+        self.app.worker_task.emit({'fcn': self.on_buffer_action, 'params': [value, join, factor]})
+
+    def on_rotate_action(self, val, point):
+        """
+        Rotate geometry
+
+        :param val:     Rotate with a known angle value, val
+        :param point:   Reference point for rotation: tuple
+        :return:
+        """
+
+        elem_list = self.draw_app.selected
+        px, py = point
+
+        if not elem_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected."))
+            return
+
+        with self.app.proc_container.new(_("Appying Rotate")):
+            try:
+                for sel_el_shape in elem_list:
+                    sel_el = sel_el_shape.geo
+                    if 'solid' in sel_el:
+                        sel_el['solid'] = affinity.rotate(sel_el['solid'], angle=-val, origin=(px, py))
+                    if 'follow' in sel_el:
+                        sel_el['follow'] = affinity.rotate(sel_el['follow'], angle=-val, origin=(px, py))
+                    if 'clear' in sel_el:
+                        sel_el['clear'] = affinity.rotate(sel_el['clear'], angle=-val, origin=(px, py))
+                self.draw_app.plot_all()
+
+                self.app.inform.emit('[success] %s' % _("Done."))
+            except Exception as e:
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Action was not executed"), str(e)))
+                return
+
+    def on_flip(self, axis, point):
+        """
+        Mirror (flip) geometry
+
+        :param axis:    Mirror on a known axis given by the axis parameter
+        :param point:   Mirror reference point
+        :return:
+        """
+
+        elem_list = self.draw_app.selected
+        px, py = point
+
+        if not elem_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected."))
+            return
+
+        with self.app.proc_container.new(_("Applying Flip")):
+            try:
+                # execute mirroring
+                for sel_el_shape in elem_list:
+                    sel_el = sel_el_shape.geo
+                    if axis == 'X':
+                        if 'solid' in sel_el:
+                            sel_el['solid'] = affinity.scale(sel_el['solid'], xfact=1, yfact=-1, origin=(px, py))
+                        if 'follow' in sel_el:
+                            sel_el['follow'] = affinity.scale(sel_el['follow'], xfact=1, yfact=-1, origin=(px, py))
+                        if 'clear' in sel_el:
+                            sel_el['clear'] = affinity.scale(sel_el['clear'], xfact=1, yfact=-1, origin=(px, py))
+                        self.app.inform.emit('[success] %s...' % _('Flip on Y axis done'))
+                    elif axis == 'Y':
+                        if 'solid' in sel_el:
+                            sel_el['solid'] = affinity.scale(sel_el['solid'], xfact=-1, yfact=1, origin=(px, py))
+                        if 'follow' in sel_el:
+                            sel_el['follow'] = affinity.scale(sel_el['follow'], xfact=-1, yfact=1, origin=(px, py))
+                        if 'clear' in sel_el:
+                            sel_el['clear'] = affinity.scale(sel_el['clear'], xfact=-1, yfact=1, origin=(px, py))
+                        self.app.inform.emit('[success] %s...' % _('Flip on X axis done'))
+                self.draw_app.plot_all()
+            except Exception as e:
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Action was not executed"), str(e)))
+                return
+
+    def on_skew(self, axis, xval, yval, point):
+        """
+        Skew geometry
+
+        :param axis:    Axis on which to deform, skew
+        :param xval:    Skew value on X axis
+        :param yval:    Skew value on Y axis
+        :param point:   Point of reference for deformation: tuple
+        :return:
+        """
+        elem_list = self.draw_app.selected
+        px, py = point
+
+        if not elem_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected."))
+            return
+
+        with self.app.proc_container.new(_("Applying Skew")):
+            try:
+
+                for sel_el_shape in elem_list:
+                    sel_el = sel_el_shape.geo
+
+                    if 'solid' in sel_el:
+                        sel_el['solid'] = affinity.skew(sel_el['solid'], xval, yval, origin=(px, py))
+                    if 'follow' in sel_el:
+                        sel_el['follow'] = affinity.skew(sel_el['follow'], xval, yval, origin=(px, py))
+                    if 'clear' in sel_el:
+                        sel_el['clear'] = affinity.skew(sel_el['clear'], xval, yval, origin=(px, py))
+
+                self.draw_app.plot_all()
+
+                if str(axis) == 'X':
+                    self.app.inform.emit('[success] %s...' % _('Skew on the X axis done'))
+                else:
+                    self.app.inform.emit('[success] %s...' % _('Skew on the Y axis done'))
+            except Exception as e:
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Action was not executed"), str(e)))
+                return
+
+    def on_scale(self, axis, xfactor, yfactor, point=None):
+        """
+        Scale geometry
+
+        :param axis:        Axis on which to scale
+        :param xfactor:     Factor for scaling on X axis
+        :param yfactor:     Factor for scaling on Y axis
+        :param point:       Point of origin for scaling
+
+        :return:
+        """
+        elem_list = self.draw_app.selected
+        px, py = point
+
+        if not elem_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected."))
+            return
+        else:
+            with self.app.proc_container.new(_("Applying Scale")):
+                try:
+                    for sel_el_shape in elem_list:
+                        sel_el = sel_el_shape.geo
+                        if 'solid' in sel_el:
+                            sel_el['solid'] = affinity.scale(sel_el['solid'], xfactor, yfactor, origin=(px, py))
+                        if 'follow' in sel_el:
+                            sel_el['follow'] = affinity.scale(sel_el['follow'], xfactor, yfactor, origin=(px, py))
+                        if 'clear' in sel_el:
+                            sel_el['clear'] = affinity.scale(sel_el['clear'], xfactor, yfactor, origin=(px, py))
+                    self.draw_app.plot_all()
+
+                    if str(axis) == 'X':
+                        self.app.inform.emit('[success] %s...' % _('Scale on the X axis done'))
+                    else:
+                        self.app.inform.emit('[success] %s...' % _('Scale on the Y axis done'))
+
+                except Exception as e:
+                    self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Action was not executed"), str(e)))
+                    return
+
+    def on_offset(self, axis, num):
+        """
+        Offset geometry
+
+        :param axis:        Axis on which to apply offset
+        :param num:         The translation factor
+
+        :return:
+        """
+        elem_list = self.draw_app.selected
+
+        if not elem_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected."))
+            return
+
+        with self.app.proc_container.new(_("Applying Offset")):
+            try:
+                for sel_el_shape in elem_list:
+                    sel_el = sel_el_shape.geo
+                    if axis == 'X':
+                        if 'solid' in sel_el:
+                            sel_el['solid'] = affinity.translate(sel_el['solid'], num, 0)
+                        if 'follow' in sel_el:
+                            sel_el['follow'] = affinity.translate(sel_el['follow'], num, 0)
+                        if 'clear' in sel_el:
+                            sel_el['clear'] = affinity.translate(sel_el['clear'], num, 0)
+                    elif axis == 'Y':
+                        if 'solid' in sel_el:
+                            sel_el['solid'] = affinity.translate(sel_el['solid'], 0, num)
+                        if 'follow' in sel_el:
+                            sel_el['follow'] = affinity.translate(sel_el['follow'], 0, num)
+                        if 'clear' in sel_el:
+                            sel_el['clear'] = affinity.translate(sel_el['clear'], 0, num)
+                    self.draw_app.plot_all()
+
+                if str(axis) == 'X':
+                    self.app.inform.emit('[success] %s...' % _('Offset on the X axis done'))
+                else:
+                    self.app.inform.emit('[success] %s...' % _('Offset on the Y axis done'))
+
+            except Exception as e:
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Action was not executed"), str(e)))
+                return
+
+    def on_buffer_action(self, value, join, factor=None):
+        elem_list = self.draw_app.selected
+
+        if not elem_list:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("No shape selected."))
+            return
+
+        with self.app.proc_container.new(_("Applying Buffer")):
+            try:
+                for sel_el_shape in elem_list:
+                    sel_el = sel_el_shape.geo
+
+                    if factor:
+                        if 'solid' in sel_el:
+                            sel_el['solid'] = affinity.scale(sel_el['solid'], value, value, origin='center')
+                        if 'follow' in sel_el:
+                            sel_el['follow'] = affinity.scale(sel_el['solid'], value, value, origin='center')
+                        if 'clear' in sel_el:
+                            sel_el['clear'] = affinity.scale(sel_el['solid'], value, value, origin='center')
+                    else:
+                        if 'solid' in sel_el:
+                            sel_el['solid'] = sel_el['solid'].buffer(
+                                value, resolution=self.app.defaults["gerber_circle_steps"], join_style=join)
+                        if 'clear' in sel_el:
+                            sel_el['clear'] = sel_el['clear'].buffer(
+                                value, resolution=self.app.defaults["gerber_circle_steps"], join_style=join)
+
+                    self.draw_app.plot_all()
+
+                self.app.inform.emit('[success] %s...' % _('Buffer done'))
+
+            except Exception as e:
+                self.app.log.debug("TransformEditorTool.on_buffer_action() --> %s" % str(e))
+                self.app.inform.emit('[ERROR_NOTCL] %s: %s.' % (_("Action was not executed"), str(e)))
+                return
+
+    def on_rotate_key(self):
+        val_box = FCInputDoubleSpinner(title=_("Rotate ..."),
+                                       text='%s:' % _('Enter an Angle Value (degrees)'),
+                                       min=-359.9999, max=360.0000, decimals=self.decimals,
+                                       init_val=float(self.app.defaults['tools_transform_rotate']),
+                                       parent=self.app.ui)
+        val_box.set_icon(QtGui.QIcon(self.app.resource_location + '/rotate.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_rotate(val=val, ref=1)
+            self.app.inform.emit('[success] %s...' % _("Rotate done"))
+            return
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Rotate cancelled"))
+
+    def on_offx_key(self):
+        units = self.app.defaults['units'].lower()
+
+        val_box = FCInputDoubleSpinner(title=_("Offset on X axis ..."),
+                                       text='%s: (%s)' % (_('Enter a distance Value'), str(units)),
+                                       min=-10000.0000, max=10000.0000, decimals=self.decimals,
+                                       init_val=float(self.app.defaults['tools_transform_offset_x']),
+                                       parent=self.app.ui)
+        val_box.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/offsetx32.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_offx(val=val)
+            self.app.inform.emit('[success] %s...' % _("Offset on the X axis done"))
+            return
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Offset X cancelled"))
+
+    def on_offy_key(self):
+        units = self.app.defaults['units'].lower()
+
+        val_box = FCInputDoubleSpinner(title=_("Offset on Y axis ..."),
+                                       text='%s: (%s)' % (_('Enter a distance Value'), str(units)),
+                                       min=-10000.0000, max=10000.0000, decimals=self.decimals,
+                                       init_val=float(self.app.defaults['tools_transform_offset_y']),
+                                       parent=self.app.ui)
+        val_box.set_icon(QtGui.QIcon(self.app.resource_location + '/offsety32.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_offx(val=val)
+            self.app.inform.emit('[success] %s...' % _("Offset on Y axis done"))
+            return
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Offset Y cancelled"))
+
+    def on_skewx_key(self):
+        val_box = FCInputDoubleSpinner(title=_("Skew on X axis ..."),
+                                       text='%s:' % _('Enter an Angle Value (degrees)'),
+                                       min=-359.9999, max=360.0000, decimals=self.decimals,
+                                       init_val=float(self.app.defaults['tools_transform_skew_x']),
+                                       parent=self.app.ui)
+        val_box.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/skewX.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_skewx(val=val, ref=3)
+            self.app.inform.emit('[success] %s...' % _("Skew on X axis done"))
+            return
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Skew X cancelled"))
+
+    def on_skewy_key(self):
+        val_box = FCInputDoubleSpinner(title=_("Skew on Y axis ..."),
+                                       text='%s:' % _('Enter an Angle Value (degrees)'),
+                                       min=-359.9999, max=360.0000, decimals=self.decimals,
+                                       init_val=float(self.app.defaults['tools_transform_skew_y']),
+                                       parent=self.app.ui)
+        val_box.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/skewY.png'))
+
+        val, ok = val_box.get_value()
+        if ok:
+            self.on_skewx(val=val, ref=3)
+            self.app.inform.emit('[success] %s...' % _("Skew on Y axis done"))
+            return
+        else:
+            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Skew Y cancelled"))
+
+    @staticmethod
+    def alt_bounds(shapelist):
+        """
+        Returns coordinates of rectangular bounds of a selection of shapes
+        """
+
+        def bounds_rec(lst):
+            minx = np.Inf
+            miny = np.Inf
+            maxx = -np.Inf
+            maxy = -np.Inf
+
+            try:
+                for shape in lst:
+                    el = shape.geo
+                    if 'solid' in el:
+                        minx_, miny_, maxx_, maxy_ = bounds_rec(el['solid'])
+                        minx = min(minx, minx_)
+                        miny = min(miny, miny_)
+                        maxx = max(maxx, maxx_)
+                        maxy = max(maxy, maxy_)
+                return minx, miny, maxx, maxy
+            except TypeError:
+                # it's an object, return it's bounds
+                return lst.bounds
+
+        return bounds_rec(shapelist)
+
+
+def get_shapely_list_bounds(geometry_list):
+    xmin = np.Inf
+    ymin = np.Inf
+    xmax = -np.Inf
+    ymax = -np.Inf
+
+    for gs in geometry_list:
+        try:
+            gxmin, gymin, gxmax, gymax = gs.bounds
+            xmin = min([xmin, gxmin])
+            ymin = min([ymin, gymin])
+            xmax = max([xmax, gxmax])
+            ymax = max([ymax, gymax])
+        except Exception as e:
+            log.warning("DEVELOPMENT: Tried to get bounds of empty geometry. --> %s" % str(e))
+
+    return [xmin, ymin, xmax, ymax]

+ 373 - 0
appEditors/AppTextEditor.py

@@ -0,0 +1,373 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 10/10/2019                                         #
+# MIT Licence                                              #
+# ##########################################################
+
+from appGUI.GUIElements import FCFileSaveDialog, FCEntry, FCTextAreaExtended, FCTextAreaLineNumber, FCButton
+from PyQt5 import QtPrintSupport, QtWidgets, QtCore, QtGui
+
+from reportlab.platypus import SimpleDocTemplate, Paragraph
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.lib.units import inch, mm
+
+# from io import StringIO
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class AppTextEditor(QtWidgets.QWidget):
+
+    def __init__(self, app, text=None, plain_text=None, parent=None):
+        super().__init__(parent=parent)
+
+        self.app = app
+        self.plain_text = plain_text
+        self.callback = lambda x: None
+
+        self.setSizePolicy(
+            QtWidgets.QSizePolicy.MinimumExpanding,
+            QtWidgets.QSizePolicy.MinimumExpanding
+        )
+
+        # UI Layout
+        self.main_editor_layout = QtWidgets.QVBoxLayout(self)
+        self.main_editor_layout.setContentsMargins(0, 0, 0, 0)
+
+        self.t_frame = QtWidgets.QFrame()
+        self.t_frame.setContentsMargins(0, 0, 0, 0)
+        self.main_editor_layout.addWidget(self.t_frame)
+
+        self.work_editor_layout = QtWidgets.QGridLayout(self.t_frame)
+        self.work_editor_layout.setContentsMargins(2, 2, 2, 2)
+        self.t_frame.setLayout(self.work_editor_layout)
+
+        # CODE Editor
+        if self.plain_text:
+            self.editor_class = FCTextAreaLineNumber()
+            self.code_editor = self.editor_class.edit
+
+            stylesheet = """
+                            QPlainTextEdit { selection-background-color:yellow;
+                                             selection-color:black;
+                            }
+                         """
+            self.work_editor_layout.addWidget(self.editor_class, 0, 0, 1, 5)
+        else:
+            self.code_editor = FCTextAreaExtended()
+            stylesheet = """
+                            QTextEdit { selection-background-color:yellow;
+                                        selection-color:black;
+                            }
+                         """
+            self.work_editor_layout.addWidget(self.code_editor, 0, 0, 1, 5)
+
+        self.code_editor.setStyleSheet(stylesheet)
+
+        if text:
+            self.code_editor.setPlainText(text)
+
+        # #############################################################################################################
+        # UI SETUP
+        # #############################################################################################################
+        control_lay = QtWidgets.QHBoxLayout()
+        self.work_editor_layout.addLayout(control_lay, 1, 0, 1, 5)
+
+        # FIND
+        self.buttonFind = FCButton(_('Find'))
+        self.buttonFind.setIcon(QtGui.QIcon(self.app.resource_location + '/find32.png'))
+        self.buttonFind.setToolTip(_("Will search and highlight in yellow the string in the Find box."))
+        control_lay.addWidget(self.buttonFind)
+
+        # Entry FIND
+        self.entryFind = FCEntry()
+        self.entryFind.setToolTip(_("Find box. Enter here the strings to be searched in the text."))
+        control_lay.addWidget(self.entryFind)
+
+        # REPLACE
+        self.buttonReplace = FCButton(_('Replace With'))
+        self.buttonReplace.setIcon(QtGui.QIcon(self.app.resource_location + '/replace32.png'))
+        self.buttonReplace.setToolTip(_("Will replace the string from the Find box with the one in the Replace box."))
+        control_lay.addWidget(self.buttonReplace)
+
+        # Entry REPLACE
+        self.entryReplace = FCEntry()
+        self.entryReplace.setToolTip(_("String to replace the one in the Find box throughout the text."))
+        control_lay.addWidget(self.entryReplace)
+
+        # Select All
+        self.sel_all_cb = QtWidgets.QCheckBox(_('All'))
+        self.sel_all_cb.setToolTip(_("When checked it will replace all instances in the 'Find' box\n"
+                                     "with the text in the 'Replace' box.."))
+        control_lay.addWidget(self.sel_all_cb)
+
+        # COPY All
+        # self.button_copy_all = FCButton(_('Copy All'))
+        # self.button_copy_all.setIcon(QtGui.QIcon(self.app.resource_location + '/copy_file32.png'))
+        # self.button_copy_all.setToolTip(_("Will copy all the text in the Code Editor to the clipboard."))
+        # control_lay.addWidget(self.button_copy_all)
+
+        # Update
+        self.button_update_code = QtWidgets.QToolButton()
+        self.button_update_code.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.button_update_code.setToolTip(_("Save changes internally."))
+        self.button_update_code.hide()
+        control_lay.addWidget(self.button_update_code)
+
+        # Print PREVIEW
+        self.buttonPreview = QtWidgets.QToolButton()
+        self.buttonPreview.setIcon(QtGui.QIcon(self.app.resource_location + '/preview32.png'))
+        self.buttonPreview.setToolTip(_("Open a OS standard Preview Print window."))
+        control_lay.addWidget(self.buttonPreview)
+
+        # PRINT
+        self.buttonPrint = QtWidgets.QToolButton()
+        self.buttonPrint.setIcon(QtGui.QIcon(self.app.resource_location + '/printer32.png'))
+        self.buttonPrint.setToolTip(_("Open a OS standard Print window."))
+        control_lay.addWidget(self.buttonPrint)
+
+        # OPEN
+        self.buttonOpen = QtWidgets.QToolButton()
+        self.buttonOpen.setIcon(QtGui.QIcon(self.app.resource_location + '/folder32_bis.png'))
+        self.buttonOpen.setToolTip(_("Will open a text file in the editor."))
+        control_lay.addWidget(self.buttonOpen)
+
+        # SAVE
+        self.buttonSave = QtWidgets.QToolButton()
+        self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.buttonSave.setToolTip(_("Will save the text in the editor into a file."))
+        control_lay.addWidget(self.buttonSave)
+
+        # RUN
+        self.buttonRun = FCButton(_('Run'))
+        self.buttonRun.setToolTip(_("Will run the TCL commands found in the text file, one by one."))
+        self.buttonRun.hide()
+        control_lay.addWidget(self.buttonRun)
+
+        # #############################################################################################################
+        # ################### SIGNALS #################################################################################
+        # #############################################################################################################
+        self.code_editor.textChanged.connect(self.handleTextChanged)
+        self.buttonOpen.clicked.connect(self.handleOpen)
+        self.buttonSave.clicked.connect(self.handleSaveGCode)
+        self.buttonPrint.clicked.connect(self.handlePrint)
+        self.buttonPreview.clicked.connect(self.handlePreview)
+        self.buttonFind.clicked.connect(self.handleFindGCode)
+        self.buttonReplace.clicked.connect(self.handleReplaceGCode)
+        # self.button_copy_all.clicked.connect(self.handleCopyAll)
+
+        self.code_editor.set_model_data(self.app.myKeywords)
+
+        self.code_edited = ''
+
+    def set_callback(self, callback):
+        self.callback = callback
+
+    def handlePrint(self):
+        dialog = QtPrintSupport.QPrintDialog()
+        if dialog.exec() == QtWidgets.QDialog.Accepted:
+            self.code_editor.document().print_(dialog.printer())
+
+    def handlePreview(self):
+        dialog = QtPrintSupport.QPrintPreviewDialog()
+        dialog.paintRequested.connect(self.code_editor.print)
+        dialog.exec()
+
+    def handleTextChanged(self):
+        # enable = not self.ui.code_editor.document().isEmpty()
+        # self.ui.buttonPrint.setEnabled(enable)
+        # self.ui.buttonPreview.setEnabled(enable)
+
+        self.buttonSave.setStyleSheet("QPushButton {color: red;}")
+        self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as_red.png'))
+
+    def load_text(self, text, move_to_start=False, move_to_end=False, clear_text=True, as_html=False):
+        self.code_editor.textChanged.disconnect()
+        if clear_text:
+            # first clear previous text in text editor (if any)
+            self.code_editor.clear()
+
+        self.code_editor.setReadOnly(False)
+        if as_html is False:
+            self.code_editor.setPlainText(text)
+        else:
+            self.code_editor.setHtml(text)
+        if move_to_start:
+            self.code_editor.moveCursor(QtGui.QTextCursor.Start)
+        elif move_to_end:
+            self.code_editor.moveCursor(QtGui.QTextCursor.End)
+        self.code_editor.textChanged.connect(self.handleTextChanged)
+
+    def handleOpen(self, filt=None):
+        self.app.defaults.report_usage("handleOpen()")
+
+        if filt:
+            _filter_ = filt
+        else:
+            _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
+                       "All Files (*.*)"
+
+        path, _f = QtWidgets.QFileDialog.getOpenFileName(
+            caption=_('Open file'), directory=self.app.get_last_folder(), filter=_filter_)
+
+        if path:
+            file = QtCore.QFile(path)
+            if file.open(QtCore.QIODevice.ReadOnly):
+                stream = QtCore.QTextStream(file)
+                self.code_edited = stream.readAll()
+                self.code_editor.setPlainText(self.code_edited)
+                file.close()
+
+    def handleSaveGCode(self, name=None, filt=None, callback=None):
+        self.app.defaults.report_usage("handleSaveGCode()")
+
+        if filt:
+            _filter_ = filt
+        else:
+            _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
+                       "PDF Files (*.pdf);;All Files (*.*)"
+
+        if name:
+            obj_name = name
+        else:
+            try:
+                obj_name = self.app.collection.get_active().options['name']
+            except AttributeError:
+                obj_name = 'file'
+                if filt is None:
+                    _filter_ = "FlatConfig Files (*.FlatConfig);;PDF Files (*.pdf);;All Files (*.*)"
+
+        try:
+            filename = str(FCFileSaveDialog.get_saved_filename(
+                caption=_("Export Code ..."),
+                directory=self.app.defaults["global_last_folder"] + '/' + str(obj_name),
+                ext_filter=_filter_
+            )[0])
+        except TypeError:
+            filename = str(FCFileSaveDialog.get_saved_filename(
+                caption=_("Export Code ..."),
+                ext_filter=_filter_)[0])
+
+        if filename == "":
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+            return
+        else:
+            try:
+                my_gcode = self.code_editor.toPlainText()
+                if filename.rpartition('.')[2].lower() == 'pdf':
+                    page_size = (
+                        self.app.plotcanvas.pagesize_dict[self.app.defaults['global_workspaceT']][0] * mm,
+                        self.app.plotcanvas.pagesize_dict[self.app.defaults['global_workspaceT']][1] * mm
+                    )
+
+                    # add new line after each line
+                    lined_gcode = my_gcode.replace("\n", "<br />")
+
+                    styles = getSampleStyleSheet()
+                    styleN = styles['Normal']
+                    # styleH = styles['Heading1']
+                    story = []
+
+                    if self.app.defaults['units'].lower() == 'mm':
+                        bmargin = self.app.defaults['global_tpdf_bmargin'] * mm
+                        tmargin = self.app.defaults['global_tpdf_tmargin'] * mm
+                        rmargin = self.app.defaults['global_tpdf_rmargin'] * mm
+                        lmargin = self.app.defaults['global_tpdf_lmargin'] * mm
+                    else:
+                        bmargin = self.app.defaults['global_tpdf_bmargin'] * inch
+                        tmargin = self.app.defaults['global_tpdf_tmargin'] * inch
+                        rmargin = self.app.defaults['global_tpdf_rmargin'] * inch
+                        lmargin = self.app.defaults['global_tpdf_lmargin'] * inch
+
+                    doc = SimpleDocTemplate(
+                        filename,
+                        pagesize=page_size,
+                        bottomMargin=bmargin,
+                        topMargin=tmargin,
+                        rightMargin=rmargin,
+                        leftMargin=lmargin)
+
+                    P = Paragraph(lined_gcode, styleN)
+                    story.append(P)
+
+                    doc.build(
+                        story,
+                    )
+                else:
+                    with open(filename, 'w') as f:
+                        for line in my_gcode:
+                            f.write(line)
+                self.buttonSave.setStyleSheet("")
+                self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+            except FileNotFoundError:
+                self.app.inform.emit('[WARNING] %s' % _("No such file or directory"))
+                return
+            except PermissionError:
+                self.app.inform.emit('[WARNING] %s' %
+                                     _("Permission denied, saving not possible.\n"
+                                       "Most likely another app is holding the file open and not accessible."))
+                return
+
+        # Just for adding it to the recent files list.
+        if self.app.defaults["global_open_style"] is False:
+            self.app.file_opened.emit("cncjob", filename)
+        self.app.file_saved.emit("cncjob", filename)
+        self.app.inform.emit('%s: %s' % (_("Saved to"), str(filename)))
+
+        if callback is not None:
+            callback()
+
+    def handleFindGCode(self):
+
+        flags = QtGui.QTextDocument.FindCaseSensitively
+        text_to_be_found = self.entryFind.get_value()
+
+        r = self.code_editor.find(str(text_to_be_found), flags)
+        if r is False:
+            self.code_editor.moveCursor(QtGui.QTextCursor.Start)
+            self.code_editor.find(str(text_to_be_found), flags)
+
+    def handleReplaceGCode(self):
+
+        old = self.entryFind.get_value()
+        new = self.entryReplace.get_value()
+
+        if self.sel_all_cb.isChecked():
+            while True:
+                cursor = self.code_editor.textCursor()
+                cursor.beginEditBlock()
+                flags = QtGui.QTextDocument.FindCaseSensitively
+                # self.ui.editor is the QPlainTextEdit
+                r = self.code_editor.find(str(old), flags)
+                if r:
+                    qc = self.code_editor.textCursor()
+                    if qc.hasSelection():
+                        qc.insertText(new)
+                else:
+                    self.code_editor.moveCursor(QtGui.QTextCursor.Start)
+                    break
+            # Mark end of undo block
+            cursor.endEditBlock()
+        else:
+            cursor = self.code_editor.textCursor()
+            cursor.beginEditBlock()
+            qc = self.code_editor.textCursor()
+            if qc.hasSelection():
+                qc.insertText(new)
+            # Mark end of undo block
+            cursor.endEditBlock()
+
+    # def handleCopyAll(self):
+    #     text = self.code_editor.toPlainText()
+    #     self.app.clipboard.setText(text)
+    #     self.app.inform.emit(_("Content copied to clipboard ..."))
+
+    # def closeEvent(self, QCloseEvent):
+    #     super().closeEvent(QCloseEvent)

+ 0 - 0
flatcamEditors/__init__.py → appEditors/__init__.py


+ 783 - 0
appEditors/appGCodeEditor.py

@@ -0,0 +1,783 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Date: 07/22/2020                                         #
+# MIT Licence                                              #
+# ##########################################################
+
+from appEditors.AppTextEditor import AppTextEditor
+from appObjects.FlatCAMCNCJob import CNCJobObject
+from appGUI.GUIElements import FCTextArea, FCEntry, FCButton, FCTable
+from PyQt5 import QtWidgets, QtCore, QtGui
+
+# from io import StringIO
+
+import logging
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class AppGCodeEditor(QtCore.QObject):
+
+    def __init__(self, app, parent=None):
+        super().__init__(parent=parent)
+
+        self.app = app
+        self.decimals = self.app.decimals
+        self.plain_text = ''
+        self.callback = lambda x: None
+
+        self.ui = AppGCodeEditorUI(app=self.app)
+
+        self.edited_obj_name = ""
+        self.edit_area = None
+
+        self.gcode_obj = None
+        self.code_edited = ''
+
+        # #################################################################################
+        # ################### SIGNALS #####################################################
+        # #################################################################################
+        self.ui.name_entry.returnPressed.connect(self.on_name_activate)
+        self.ui.update_gcode_button.clicked.connect(self.insert_gcode)
+        self.ui.exit_editor_button.clicked.connect(lambda: self.app.editor2object())
+
+        log.debug("Initialization of the GCode Editor is finished ...")
+
+    def set_ui(self):
+        """
+
+        :return:
+        :rtype:
+        """
+
+        self.decimals = self.app.decimals
+
+        # #############################################################################################################
+        # ############# ADD a new TAB in the PLot Tab Area
+        # #############################################################################################################
+        self.ui.gcode_editor_tab = AppTextEditor(app=self.app, plain_text=True)
+        self.edit_area = self.ui.gcode_editor_tab.code_editor
+
+        # add the tab if it was closed
+        self.app.ui.plot_tab_area.addTab(self.ui.gcode_editor_tab, '%s' % _("Code Editor"))
+        self.ui.gcode_editor_tab.setObjectName('gcode_editor_tab')
+        # protect the tab that was just added
+        for idx in range(self.app.ui.plot_tab_area.count()):
+            if self.app.ui.plot_tab_area.widget(idx).objectName() == self.ui.gcode_editor_tab.objectName():
+                self.app.ui.plot_tab_area.protectTab(idx)
+
+        # delete the absolute and relative position and messages in the infobar
+        self.app.ui.position_label.setText("")
+        self.app.ui.rel_position_label.setText("")
+
+        self.ui.gcode_editor_tab.code_editor.completer_enable = False
+        self.ui.gcode_editor_tab.buttonRun.hide()
+
+        # Switch plot_area to CNCJob tab
+        self.app.ui.plot_tab_area.setCurrentWidget(self.ui.gcode_editor_tab)
+
+        self.ui.gcode_editor_tab.t_frame.hide()
+
+        self.ui.gcode_editor_tab.t_frame.show()
+        self.app.proc_container.view.set_idle()
+        # #############################################################################################################
+        # #############################################################################################################
+
+        self.ui.append_text.set_value(self.app.defaults["cncjob_append"])
+        self.ui.prepend_text.set_value(self.app.defaults["cncjob_prepend"])
+
+        # Remove anything else in the GUI Properties Tab
+        self.app.ui.properties_scroll_area.takeWidget()
+        # Put ourselves in the GUI Properties Tab
+        self.app.ui.properties_scroll_area.setWidget(self.ui.edit_widget)
+        # Switch notebook to Properties page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.properties_tab)
+
+        # make a new name for the new Excellon object (the one with edited content)
+        self.edited_obj_name = self.gcode_obj.options['name']
+        self.ui.name_entry.set_value(self.edited_obj_name)
+
+        self.activate()
+
+    def build_ui(self):
+        """
+
+        :return:
+        :rtype:
+        """
+
+        self.ui_disconnect()
+
+        # if the FlatCAM object is Excellon don't build the CNC Tools Table but hide it
+        self.ui.cnc_tools_table.hide()
+        if self.gcode_obj.cnc_tools:
+            self.ui.cnc_tools_table.show()
+            self.build_cnc_tools_table()
+
+        self.ui.exc_cnc_tools_table.hide()
+        if self.gcode_obj.exc_cnc_tools:
+            self.ui.exc_cnc_tools_table.show()
+            self.build_excellon_cnc_tools()
+
+        self.ui_connect()
+
+    def build_cnc_tools_table(self):
+        tool_idx = 0
+        row_no = 0
+
+        n = len(self.gcode_obj.cnc_tools) + 3
+        self.ui.cnc_tools_table.setRowCount(n)
+
+        # add the All Gcode selection
+        allgcode_item = QtWidgets.QTableWidgetItem('%s' % _("All GCode"))
+        allgcode_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.cnc_tools_table.setItem(row_no, 1, allgcode_item)
+        row_no += 1
+
+        # add the Header Gcode selection
+        header_item = QtWidgets.QTableWidgetItem('%s' % _("Header GCode"))
+        header_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.cnc_tools_table.setItem(row_no, 1, header_item)
+        row_no += 1
+
+        # add the Start Gcode selection
+        start_item = QtWidgets.QTableWidgetItem('%s' % _("Start GCode"))
+        start_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.cnc_tools_table.setItem(row_no, 1, start_item)
+
+        for dia_key, dia_value in self.gcode_obj.cnc_tools.items():
+
+            tool_idx += 1
+            row_no += 1
+
+            t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
+            # id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.cnc_tools_table.setItem(row_no, 0, t_id)  # Tool name/id
+
+            dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(dia_value['tooldia'])))
+
+            offset_txt = list(str(dia_value['offset']))
+            offset_txt[0] = offset_txt[0].upper()
+            offset_item = QtWidgets.QTableWidgetItem(''.join(offset_txt))
+            type_item = QtWidgets.QTableWidgetItem(str(dia_value['type']))
+            tool_type_item = QtWidgets.QTableWidgetItem(str(dia_value['tool_type']))
+
+            t_id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            dia_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            offset_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            type_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            tool_type_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+            self.ui.cnc_tools_table.setItem(row_no, 1, dia_item)  # Diameter
+            self.ui.cnc_tools_table.setItem(row_no, 2, offset_item)  # Offset
+            self.ui.cnc_tools_table.setItem(row_no, 3, type_item)  # Toolpath Type
+            self.ui.cnc_tools_table.setItem(row_no, 4, tool_type_item)  # Tool Type
+
+            tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_key))
+            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
+            self.ui.cnc_tools_table.setItem(row_no, 5, tool_uid_item)  # Tool unique ID)
+
+        self.ui.cnc_tools_table.resizeColumnsToContents()
+        self.ui.cnc_tools_table.resizeRowsToContents()
+
+        vertical_header = self.ui.cnc_tools_table.verticalHeader()
+        # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        vertical_header.hide()
+        self.ui.cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.cnc_tools_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(4, 40)
+
+        # horizontal_header.setStretchLastSection(True)
+        self.ui.cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.cnc_tools_table.setColumnWidth(0, 20)
+        self.ui.cnc_tools_table.setColumnWidth(4, 40)
+        self.ui.cnc_tools_table.setColumnWidth(6, 17)
+
+        # self.ui.geo_tools_table.setSortingEnabled(True)
+
+        self.ui.cnc_tools_table.setMinimumHeight(self.ui.cnc_tools_table.getHeight())
+        self.ui.cnc_tools_table.setMaximumHeight(self.ui.cnc_tools_table.getHeight())
+
+    def build_excellon_cnc_tools(self):
+        """
+
+        :return:
+        :rtype:
+        """
+
+        tool_idx = 0
+        row_no = 0
+
+        n = len(self.gcode_obj.exc_cnc_tools) + 3
+        self.ui.exc_cnc_tools_table.setRowCount(n)
+
+        # add the All Gcode selection
+        allgcode_item = QtWidgets.QTableWidgetItem('%s' % _("All GCode"))
+        allgcode_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.exc_cnc_tools_table.setItem(row_no, 1, allgcode_item)
+        row_no += 1
+
+        # add the Header Gcode selection
+        header_item = QtWidgets.QTableWidgetItem('%s' % _("Header GCode"))
+        header_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.exc_cnc_tools_table.setItem(row_no, 1, header_item)
+        row_no += 1
+
+        # add the Start Gcode selection
+        start_item = QtWidgets.QTableWidgetItem('%s' % _("Start GCode"))
+        start_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+        self.ui.exc_cnc_tools_table.setItem(row_no, 1, start_item)
+
+        for tooldia_key, dia_value in self.gcode_obj.exc_cnc_tools.items():
+
+            tool_idx += 1
+            row_no += 1
+
+            t_id = QtWidgets.QTableWidgetItem('%d' % int(tool_idx))
+            dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooldia_key)))
+            nr_drills_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_drills']))
+            nr_slots_item = QtWidgets.QTableWidgetItem('%d' % int(dia_value['nr_slots']))
+            cutz_item = QtWidgets.QTableWidgetItem('%.*f' % (
+                self.decimals, float(dia_value['offset']) + float(dia_value['data']['tools_drill_cutz'])))
+
+            t_id.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            dia_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            nr_drills_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            nr_slots_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            cutz_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+
+            self.ui.exc_cnc_tools_table.setItem(row_no, 0, t_id)  # Tool name/id
+            self.ui.exc_cnc_tools_table.setItem(row_no, 1, dia_item)  # Diameter
+            self.ui.exc_cnc_tools_table.setItem(row_no, 2, nr_drills_item)  # Nr of drills
+            self.ui.exc_cnc_tools_table.setItem(row_no, 3, nr_slots_item)  # Nr of slots
+
+            tool_uid_item = QtWidgets.QTableWidgetItem(str(dia_value['tool']))
+            # ## REMEMBER: THIS COLUMN IS HIDDEN IN OBJECTUI.PY # ##
+            self.ui.exc_cnc_tools_table.setItem(row_no, 4, tool_uid_item)  # Tool unique ID)
+            self.ui.exc_cnc_tools_table.setItem(row_no, 5, cutz_item)
+
+        self.ui.exc_cnc_tools_table.resizeColumnsToContents()
+        self.ui.exc_cnc_tools_table.resizeRowsToContents()
+
+        vertical_header = self.ui.exc_cnc_tools_table.verticalHeader()
+        vertical_header.hide()
+        self.ui.exc_cnc_tools_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        horizontal_header = self.ui.exc_cnc_tools_table.horizontalHeader()
+        horizontal_header.setMinimumSectionSize(10)
+        horizontal_header.setDefaultSectionSize(70)
+        horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        horizontal_header.resizeSection(0, 20)
+        horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+        horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.ResizeToContents)
+
+        # horizontal_header.setStretchLastSection(True)
+        self.ui.exc_cnc_tools_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.exc_cnc_tools_table.setColumnWidth(0, 20)
+        self.ui.exc_cnc_tools_table.setColumnWidth(6, 17)
+
+        self.ui.exc_cnc_tools_table.setMinimumHeight(self.ui.exc_cnc_tools_table.getHeight())
+        self.ui.exc_cnc_tools_table.setMaximumHeight(self.ui.exc_cnc_tools_table.getHeight())
+
+    def ui_connect(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        # rows selected
+        if self.gcode_obj.cnc_tools:
+            self.ui.cnc_tools_table.clicked.connect(self.on_row_selection_change)
+            self.ui.cnc_tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_all_rows)
+        if self.gcode_obj.exc_cnc_tools:
+            self.ui.exc_cnc_tools_table.clicked.connect(self.on_row_selection_change)
+            self.ui.exc_cnc_tools_table.horizontalHeader().sectionClicked.connect(self.on_toggle_all_rows)
+
+    def ui_disconnect(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        # rows selected
+        if self.gcode_obj.cnc_tools:
+            try:
+                self.ui.cnc_tools_table.clicked.disconnect(self.on_row_selection_change)
+            except (TypeError, AttributeError):
+                pass
+            try:
+                self.ui.cnc_tools_table.horizontalHeader().sectionClicked.disconnect(self.on_toggle_all_rows)
+            except (TypeError, AttributeError):
+                pass
+
+        if self.gcode_obj.exc_cnc_tools:
+            try:
+                self.ui.exc_cnc_tools_table.clicked.disconnect(self.on_row_selection_change)
+            except (TypeError, AttributeError):
+                pass
+            try:
+                self.ui.exc_cnc_tools_table.horizontalHeader().sectionClicked.disconnect(self.on_toggle_all_rows)
+            except (TypeError, AttributeError):
+                pass
+
+    def on_row_selection_change(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        flags = QtGui.QTextDocument.FindCaseSensitively
+        self.edit_area.moveCursor(QtGui.QTextCursor.Start)
+
+        if self.gcode_obj.cnc_tools:
+            t_table = self.ui.cnc_tools_table
+        elif self.gcode_obj.exc_cnc_tools:
+            t_table = self.ui.exc_cnc_tools_table
+        else:
+            return
+
+        sel_model = t_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if 0 in sel_rows:
+            self.edit_area.selectAll()
+            return
+
+        if 1 in sel_rows:
+            text_to_be_found = self.gcode_obj.gc_header
+            text_list = [x for x in text_to_be_found.split("\n") if x != '']
+
+            self.edit_area.find(str(text_list[0]), flags)
+            my_text_cursor = self.edit_area.textCursor()
+            start_sel = my_text_cursor.selectionStart()
+
+            end_sel = 0
+            while True:
+                f = self.edit_area.find(str(text_list[-1]), flags)
+                if f is False:
+                    break
+                my_text_cursor = self.edit_area.textCursor()
+                end_sel = my_text_cursor.selectionEnd()
+
+            my_text_cursor.setPosition(start_sel)
+            my_text_cursor.setPosition(end_sel, QtGui.QTextCursor.KeepAnchor)
+            self.edit_area.setTextCursor(my_text_cursor)
+
+        if 2 in sel_rows:
+            text_to_be_found = self.gcode_obj.gc_start
+            text_list = [x for x in text_to_be_found.split("\n") if x != '']
+
+            self.edit_area.find(str(text_list[0]), flags)
+            my_text_cursor = self.edit_area.textCursor()
+            start_sel = my_text_cursor.selectionStart()
+
+            end_sel = 0
+            while True:
+                f = self.edit_area.find(str(text_list[-1]), flags)
+                if f is False:
+                    break
+                my_text_cursor = self.edit_area.textCursor()
+                end_sel = my_text_cursor.selectionEnd()
+
+            my_text_cursor.setPosition(start_sel)
+            my_text_cursor.setPosition(end_sel, QtGui.QTextCursor.KeepAnchor)
+            self.edit_area.setTextCursor(my_text_cursor)
+
+        sel_list = []
+        for row in sel_rows:
+            # those are special rows treated before so we except them
+            if row not in [0, 1, 2]:
+                tool_no = int(t_table.item(row, 0).text())
+
+                text_to_be_found = None
+                if self.gcode_obj.cnc_tools:
+                    text_to_be_found = self.gcode_obj.cnc_tools[tool_no]['gcode']
+                elif self.gcode_obj.exc_cnc_tools:
+                    tool_dia = self.app.dec_format(float(t_table.item(row, 1).text()), dec=self.decimals)
+                    for tool_d in self.gcode_obj.exc_cnc_tools:
+                        if self.app.dec_format(tool_d, dec=self.decimals) == tool_dia:
+                            text_to_be_found = self.gcode_obj.exc_cnc_tools[tool_d]['gcode']
+                    if text_to_be_found is None:
+                        continue
+                else:
+                    continue
+
+                text_list = [x for x in text_to_be_found.split("\n") if x != '']
+
+                # self.edit_area.find(str(text_list[0]), flags)
+                # my_text_cursor = self.edit_area.textCursor()
+                # start_sel = my_text_cursor.selectionStart()
+
+                # first I search for the tool
+                found_tool = self.edit_area.find('T%d' % tool_no, flags)
+                if found_tool is False:
+                    continue
+
+                # once the tool found then I set the text Cursor position to the tool Tx position
+                my_text_cursor = self.edit_area.textCursor()
+                tool_pos = my_text_cursor.selectionStart()
+                my_text_cursor.setPosition(tool_pos)
+
+                # I search for the first finding of the first line in the Tool GCode
+                f = self.edit_area.find(str(text_list[0]), flags)
+                if f is False:
+                    continue
+
+                # once found I set the text Cursor position here
+                my_text_cursor = self.edit_area.textCursor()
+                start_sel = my_text_cursor.selectionStart()
+
+                # I search for the next find of M6 (which belong to the next tool
+                m6 = self.edit_area.find('M6', flags)
+                if m6 is False:
+                    # this mean that we are in the last tool, we take all to the end
+                    self.edit_area.moveCursor(QtGui.QTextCursor.End)
+                    my_text_cursor = self.edit_area.textCursor()
+                    end_sel = my_text_cursor.selectionEnd()
+                else:
+                    pos_list = []
+
+                    my_text_cursor = self.edit_area.textCursor()
+                    m6_pos = my_text_cursor.selectionEnd()
+
+                    # move cursor back to the start of the tool gcode so the find method will work on the tool gcode
+                    t_curs = self.edit_area.textCursor()
+                    t_curs.setPosition(start_sel)
+                    self.edit_area.setTextCursor(t_curs)
+
+                    # search for all findings of the last line in the tool gcode
+                    # yet, we may find in multiple locations or in the gcode that belong to other tools
+                    while True:
+                        f = self.edit_area.find(str(text_list[-1]), flags)
+                        if f is False:
+                            break
+                        my_text_cursor = self.edit_area.textCursor()
+                        pos_list.append(my_text_cursor.selectionEnd())
+
+                    # now we find a position that is less than the m6_pos but also the closest (maximum)
+                    belong_to_tool_list = []
+                    for last_line_pos in pos_list:
+                        if last_line_pos < m6_pos:
+                            belong_to_tool_list.append(last_line_pos)
+                    if belong_to_tool_list:
+                        end_sel = max(belong_to_tool_list)
+                    else:
+                        # this mean that we are in the last tool, we take all to the end
+                        self.edit_area.moveCursor(QtGui.QTextCursor.End)
+                        my_text_cursor = self.edit_area.textCursor()
+                        end_sel = my_text_cursor.selectionEnd()
+
+                my_text_cursor.setPosition(start_sel)
+                my_text_cursor.setPosition(end_sel, QtGui.QTextCursor.KeepAnchor)
+                self.edit_area.setTextCursor(my_text_cursor)
+
+                tool_selection = QtWidgets.QTextEdit.ExtraSelection()
+                tool_selection.cursor = self.edit_area.textCursor()
+                tool_selection.format.setFontUnderline(True)
+                sel_list.append(tool_selection)
+
+        self.edit_area.setExtraSelections(sel_list)
+
+    def on_toggle_all_rows(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        if self.gcode_obj.cnc_tools:
+            t_table = self.ui.cnc_tools_table
+        elif self.gcode_obj.exc_cnc_tools:
+            t_table = self.ui.exc_cnc_tools_table
+        else:
+            return
+
+        sel_model = t_table.selectionModel()
+        sel_indexes = sel_model.selectedIndexes()
+
+        # it will iterate over all indexes which means all items in all columns too but I'm interested only on rows
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if len(sel_rows) == t_table.rowCount():
+            t_table.clearSelection()
+            my_text_cursor = self.edit_area.textCursor()
+            my_text_cursor.clearSelection()
+        else:
+            t_table.selectAll()
+
+    def handleTextChanged(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        # enable = not self.ui.code_editor.document().isEmpty()
+        # self.ui.buttonPrint.setEnabled(enable)
+        # self.ui.buttonPreview.setEnabled(enable)
+
+        self.buttonSave.setStyleSheet("QPushButton {color: red;}")
+        self.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as_red.png'))
+
+    def insert_gcode(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        pass
+
+    def edit_fcgcode(self, cnc_obj):
+        """
+
+        :param cnc_obj:
+        :type cnc_obj:
+        :return:
+        :rtype:
+        """
+        assert isinstance(cnc_obj, CNCJobObject)
+        self.gcode_obj = cnc_obj
+
+        gcode_text = self.gcode_obj.source_file
+
+        self.set_ui()
+        self.build_ui()
+
+        # then append the text from GCode to the text editor
+        self.ui.gcode_editor_tab.load_text(gcode_text, move_to_start=True, clear_text=True)
+        self.app.inform.emit('[success] %s...' % _('Loaded Machine Code into Code Editor'))
+
+    def update_fcgcode(self, edited_obj):
+        """
+
+        :return:
+        :rtype:
+        """
+        my_gcode = self.ui.gcode_editor_tab.code_editor.toPlainText()
+        self.gcode_obj.source_file = my_gcode
+        self.deactivate()
+
+        self.ui.gcode_editor_tab.buttonSave.setStyleSheet("")
+        self.ui.gcode_editor_tab.buttonSave.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+
+    def on_open_gcode(self):
+        """
+
+        :return:
+        :rtype:
+        """
+        _filter_ = "G-Code Files (*.nc);; G-Code Files (*.txt);; G-Code Files (*.tap);; G-Code Files (*.cnc);; " \
+                   "All Files (*.*)"
+
+        path, _f = QtWidgets.QFileDialog.getOpenFileName(
+            caption=_('Open file'), directory=self.app.get_last_folder(), filter=_filter_)
+
+        if path:
+            file = QtCore.QFile(path)
+            if file.open(QtCore.QIODevice.ReadOnly):
+                stream = QtCore.QTextStream(file)
+                self.code_edited = stream.readAll()
+                self.ui.gcode_editor_tab.load_text(self.code_edited, move_to_start=True, clear_text=True)
+                file.close()
+
+    def activate(self):
+        self.app.call_source = 'gcode_editor'
+
+    def deactivate(self):
+        self.app.call_source = 'app'
+
+    def on_name_activate(self):
+        self.edited_obj_name = self.ui.name_entry.get_value()
+
+
+class AppGCodeEditorUI:
+    def __init__(self, app):
+        self.app = app
+
+        # Number of decimals used by tools in this class
+        self.decimals = self.app.decimals
+
+        # ## Current application units in Upper Case
+        self.units = self.app.defaults['units'].upper()
+
+        # self.setSizePolicy(
+        #     QtWidgets.QSizePolicy.MinimumExpanding,
+        #     QtWidgets.QSizePolicy.MinimumExpanding
+        # )
+
+        self.gcode_editor_tab = None
+
+        self.edit_widget = QtWidgets.QWidget()
+        # ## Box for custom widgets
+        # This gets populated in offspring implementations.
+        layout = QtWidgets.QVBoxLayout()
+        self.edit_widget.setLayout(layout)
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        self.edit_frame = QtWidgets.QFrame()
+        self.edit_frame.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self.edit_frame)
+        self.edit_box = QtWidgets.QVBoxLayout()
+        self.edit_box.setContentsMargins(0, 0, 0, 0)
+        self.edit_frame.setLayout(self.edit_box)
+
+        # ## Page Title box (spacing between children)
+        self.title_box = QtWidgets.QHBoxLayout()
+        self.edit_box.addLayout(self.title_box)
+
+        # ## Page Title icon
+        pixmap = QtGui.QPixmap(self.app.resource_location + '/flatcam_icon32.png')
+        self.icon = QtWidgets.QLabel()
+        self.icon.setPixmap(pixmap)
+        self.title_box.addWidget(self.icon, stretch=0)
+
+        # ## Title label
+        self.title_label = QtWidgets.QLabel("<font size=5><b>%s</b></font>" % _('GCode Editor'))
+        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.title_label, stretch=1)
+
+        # ## Object name
+        self.name_box = QtWidgets.QHBoxLayout()
+        self.edit_box.addLayout(self.name_box)
+        name_label = QtWidgets.QLabel(_("Name:"))
+        self.name_box.addWidget(name_label)
+        self.name_entry = FCEntry()
+        self.name_box.addWidget(self.name_entry)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.edit_box.addWidget(separator_line)
+
+        # CNC Tools Table when made out of Geometry
+        self.cnc_tools_table = FCTable()
+        self.cnc_tools_table.setSortingEnabled(False)
+        self.cnc_tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        self.edit_box.addWidget(self.cnc_tools_table)
+
+        self.cnc_tools_table.setColumnCount(6)
+        self.cnc_tools_table.setColumnWidth(0, 20)
+        self.cnc_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Offset'), _('Type'), _('TT'), ''])
+        self.cnc_tools_table.setColumnHidden(5, True)
+
+        # CNC Tools Table when made out of Excellon
+        self.exc_cnc_tools_table = FCTable()
+        self.exc_cnc_tools_table.setSortingEnabled(False)
+        self.exc_cnc_tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        self.edit_box.addWidget(self.exc_cnc_tools_table)
+
+        self.exc_cnc_tools_table.setColumnCount(6)
+        self.exc_cnc_tools_table.setColumnWidth(0, 20)
+        self.exc_cnc_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Drills'), _('Slots'), '', _("Cut Z")])
+        self.exc_cnc_tools_table.setColumnHidden(4, True)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.edit_box.addWidget(separator_line)
+
+        # Prepend text to GCode
+        prependlabel = QtWidgets.QLabel('%s 1:' % _('CNC Code Snippet'))
+        prependlabel.setToolTip(
+            _("Code snippet defined in Preferences.")
+        )
+        self.edit_box.addWidget(prependlabel)
+
+        self.prepend_text = FCTextArea()
+        self.prepend_text.setPlaceholderText(
+            _("Type here any G-Code commands you would\n"
+              "like to insert at the cursor location.")
+        )
+        self.edit_box.addWidget(self.prepend_text)
+
+        # Insert Button
+        self.update_gcode_button = FCButton(_('Insert Code'))
+        # self.update_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.update_gcode_button.setToolTip(
+            _("Insert the code above at the cursor location.")
+        )
+        self.edit_box.addWidget(self.update_gcode_button)
+
+        # Append text to GCode
+        appendlabel = QtWidgets.QLabel('%s 2:' % _('CNC Code Snippet'))
+        appendlabel.setToolTip(
+            _("Code snippet defined in Preferences.")
+        )
+        self.edit_box.addWidget(appendlabel)
+
+        self.append_text = FCTextArea()
+        self.append_text.setPlaceholderText(
+            _("Type here any G-Code commands you would\n"
+              "like to insert at the cursor location.")
+        )
+        self.edit_box.addWidget(self.append_text)
+
+        # Insert Button
+        self.update_gcode_sec_button = FCButton(_('Insert Code'))
+        # self.update_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.update_gcode_sec_button.setToolTip(
+            _("Insert the code above at the cursor location.")
+        )
+        self.edit_box.addWidget(self.update_gcode_sec_button)
+
+        layout.addStretch()
+
+        # Editor
+        self.exit_editor_button = FCButton(_('Exit Editor'))
+        self.exit_editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
+        self.exit_editor_button.setToolTip(
+            _("Exit from Editor.")
+        )
+        self.exit_editor_button.setStyleSheet("""
+                                          QPushButton
+                                          {
+                                              font-weight: bold;
+                                          }
+                                          """)
+        layout.addWidget(self.exit_editor_button)
+        # ############################ FINSIHED GUI ##################################################################
+        # #############################################################################################################
+
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%d, %d]' %
+                                            (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)

+ 181 - 0
appGUI/ColumnarFlowLayout.py

@@ -0,0 +1,181 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File by:  David Robertson (c)                            #
+# Date:     5/2020                                         #
+# License:  MIT Licence                                    #
+# ##########################################################
+
+import sys
+
+from PyQt5.QtCore import QPoint, QRect, QSize, Qt
+from PyQt5.QtWidgets import QLayout, QSizePolicy
+import math
+
+
+class ColumnarFlowLayout(QLayout):
+    def __init__(self, parent=None, margin=0, spacing=-1):
+        super().__init__(parent)
+
+        if parent is not None:
+            self.setContentsMargins(margin, margin, margin, margin)
+
+        self.setSpacing(spacing)
+        self.itemList = []
+
+    def __del__(self):
+        del_item = self.takeAt(0)
+        while del_item:
+            del_item = self.takeAt(0)
+
+    def addItem(self, item):
+        self.itemList.append(item)
+
+    def count(self):
+        return len(self.itemList)
+
+    def itemAt(self, index):
+        if 0 <= index < len(self.itemList):
+            return self.itemList[index]
+        return None
+
+    def takeAt(self, index):
+        if 0 <= index < len(self.itemList):
+            return self.itemList.pop(index)
+        return None
+
+    def expandingDirections(self):
+        return Qt.Orientations(Qt.Orientation(0))
+
+    def hasHeightForWidth(self):
+        return True
+
+    def heightForWidth(self, width):
+        height = self.doLayout(QRect(0, 0, width, 0), True)
+        return height
+
+    def setGeometry(self, rect):
+        super().setGeometry(rect)
+        self.doLayout(rect, False)
+
+    def sizeHint(self):
+        return self.minimumSize()
+
+    def minimumSize(self):
+        size = QSize()
+
+        for item in self.itemList:
+            size = size.expandedTo(item.minimumSize())
+
+        margin, _, _, _ = self.getContentsMargins()
+
+        size += QSize(2 * margin, 2 * margin)
+        return size
+
+    def doLayout(self, rect: QRect, testOnly: bool) -> int:
+        spacing = self.spacing()
+        x = rect.x()
+        y = rect.y()
+
+        # Determine width of widest item
+        widest = 0
+        for item in self.itemList:
+            widest = max(widest, item.sizeHint().width())
+
+        # Determine how many equal-width columns we can get, and how wide each one should be
+        column_count = math.floor(rect.width() / (widest + spacing))
+        column_count = min(column_count, len(self.itemList))
+        column_count = max(1, column_count)
+        column_width = math.floor((rect.width() - (column_count-1)*spacing - 1) / column_count)
+
+        # Get the heights for all of our items
+        item_heights = {}
+        for item in self.itemList:
+            height = item.heightForWidth(column_width) if item.hasHeightForWidth() else item.sizeHint().height()
+            item_heights[item] = height
+
+        # Prepare our column representation
+        column_contents = []
+        column_heights = []
+        for column_index in range(column_count):
+            column_contents.append([])
+            column_heights.append(0)
+
+        def add_to_column(column: int, item):
+            column_contents[column].append(item)
+            column_heights[column] += (item_heights[item] + spacing)
+
+        def shove_one(from_column: int) -> bool:
+            if len(column_contents[from_column]) >= 1:
+                item = column_contents[from_column].pop(0)
+                column_heights[from_column] -= (item_heights[item] + spacing)
+                add_to_column(from_column-1, item)
+                return True
+            return False
+
+        def shove_cascade_consider(from_column: int) -> bool:
+            changed_item = False
+
+            if len(column_contents[from_column]) > 1:
+                item = column_contents[from_column][0]
+                item_height = item_heights[item]
+                if column_heights[from_column-1] + item_height < max(column_heights):
+                    changed_item = shove_one(from_column) or changed_item
+
+            if from_column+1 < column_count:
+                changed_item = shove_cascade_consider(from_column+1) or changed_item
+
+            return changed_item
+
+        def shove_cascade() -> bool:
+            if column_count < 2:
+                return False
+            changed_item = True
+            while changed_item:
+                changed_item = shove_cascade_consider(1)
+            return changed_item
+
+        def pick_best_shoving_position() -> int:
+            best_pos = 1
+            best_height = sys.maxsize
+            for column_idx in range(1, column_count):
+                if len(column_contents[column_idx]) == 0:
+                    continue
+                item = column_contents[column_idx][0]
+                height_after_shove = column_heights[column_idx-1] + item_heights[item]
+                if height_after_shove < best_height:
+                    best_height = height_after_shove
+                    best_pos = column_idx
+            return best_pos
+
+        # Calculate the best layout
+        column_index = 0
+        for item in self.itemList:
+            item_height = item_heights[item]
+            if column_heights[column_index] != 0 and (column_heights[column_index] + item_height) > max(column_heights):
+                column_index += 1
+                if column_index >= column_count:
+                    # Run out of room, need to shove more stuff in each column
+                    if column_count >= 2:
+                        changed = shove_cascade()
+                        if not changed:
+                            shoving_pos = pick_best_shoving_position()
+                            shove_one(shoving_pos)
+                            shove_cascade()
+                    column_index = column_count-1
+
+            add_to_column(column_index, item)
+
+        shove_cascade()
+
+        # Set geometry according to the layout we have calculated
+        if not testOnly:
+            for column_index, items in enumerate(column_contents):
+                x = column_index * (column_width + spacing)
+                y = 0
+                for item in items:
+                    height = item_heights[item]
+                    item.setGeometry(QRect(x, y, column_width, height))
+                    y += (height + spacing)
+
+        # Return the overall height
+        return max(column_heights)

+ 4703 - 0
appGUI/GUIElements.py

@@ -0,0 +1,4703 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+# ##########################################################
+
+# ##########################################################
+# File Modified (major mod): Marius Adrian Stanciu         #
+# Date: 3/10/2019                                          #
+# ##########################################################
+
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import Qt, pyqtSlot, pyqtSignal
+from PyQt5.QtWidgets import QTextEdit, QCompleter, QAction
+from PyQt5.QtGui import QKeySequence, QTextCursor
+
+from copy import copy
+import re
+import logging
+import html
+import sys
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+log = logging.getLogger('base')
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+EDIT_SIZE_HINT = 70
+
+
+class RadioSet(QtWidgets.QWidget):
+    activated_custom = QtCore.pyqtSignal(str)
+
+    def __init__(self, choices, orientation='horizontal', parent=None, stretch=None):
+        """
+        The choices are specified as a list of dictionaries containing:
+
+        * 'label': Shown in the UI
+        * 'value': The value returned is selected
+
+        :param choices: List of choices. See description.
+        :param orientation: 'horizontal' (default) of 'vertical'.
+        :param parent: Qt parent widget.
+        :type choices: list
+        """
+        super(RadioSet, self).__init__(parent)
+        self.choices = copy(choices)
+        if orientation == 'horizontal':
+            layout = QtWidgets.QHBoxLayout()
+        else:
+            layout = QtWidgets.QVBoxLayout()
+
+        group = QtWidgets.QButtonGroup(self)
+
+        for choice in self.choices:
+            choice['radio'] = QtWidgets.QRadioButton(choice['label'])
+            group.addButton(choice['radio'])
+            layout.addWidget(choice['radio'], stretch=0)
+            choice['radio'].toggled.connect(self.on_toggle)
+
+        layout.setContentsMargins(0, 0, 0, 0)
+
+        if stretch is False:
+            pass
+        else:
+            layout.addStretch()
+
+        self.setLayout(layout)
+
+        self.group_toggle_fn = lambda: None
+
+    def on_toggle(self):
+        # log.debug("Radio toggled")
+        radio = self.sender()
+        if radio.isChecked():
+            self.group_toggle_fn()
+            ret_val = str(self.get_value())
+            self.activated_custom.emit(ret_val)
+        return
+
+    def get_value(self):
+        for choice in self.choices:
+            if choice['radio'].isChecked():
+                return choice['value']
+        log.error("No button was toggled in RadioSet.")
+        return None
+
+    def set_value(self, val):
+        for choice in self.choices:
+            if choice['value'] == val:
+                choice['radio'].setChecked(True)
+                return
+        log.error("Value given is not part of this RadioSet: %s" % str(val))
+
+    def setOptionsDisabled(self, options: list, val: bool) -> None:
+        for option in self.choices:
+            if option['label'] in options:
+                option['radio'].setDisabled(val)
+
+
+# class RadioGroupChoice(QtWidgets.QWidget):
+#     def __init__(self, label_1, label_2, to_check, hide_list, show_list, parent=None):
+#         """
+#         The choices are specified as a list of dictionaries containing:
+#
+#         * 'label': Shown in the UI
+#         * 'value': The value returned is selected
+#
+#         :param choices: List of choices. See description.
+#         :param orientation: 'horizontal' (default) of 'vertical'.
+#         :param parent: Qt parent widget.
+#         :type choices: list
+#         """
+#         super().__init__(parent)
+#
+#         group = QtGui.QButtonGroup(self)
+#
+#         self.lbl1 = label_1
+#         self.lbl2 = label_2
+#         self.hide_list = hide_list
+#         self.show_list = show_list
+#
+#         self.btn1 = QtGui.QRadioButton(str(label_1))
+#         self.btn2 = QtGui.QRadioButton(str(label_2))
+#         group.addButton(self.btn1)
+#         group.addButton(self.btn2)
+#
+#         if to_check == 1:
+#             self.btn1.setChecked(True)
+#         else:
+#             self.btn2.setChecked(True)
+#
+#         self.btn1.toggled.connect(lambda: self.btn_state(self.btn1))
+#         self.btn2.toggled.connect(lambda: self.btn_state(self.btn2))
+#
+#     def btn_state(self, btn):
+#         if btn.text() == self.lbl1:
+#             if btn.isChecked() is True:
+#                 self.show_widgets(self.show_list)
+#                 self.hide_widgets(self.hide_list)
+#             else:
+#                 self.show_widgets(self.hide_list)
+#                 self.hide_widgets(self.show_list)
+#
+#     def hide_widgets(self, lst):
+#         for wgt in lst:
+#             wgt.hide()
+#
+#     def show_widgets(self, lst):
+#         for wgt in lst:
+#             wgt.show()
+
+
+class FCTree(QtWidgets.QTreeWidget):
+    resize_sig = QtCore.pyqtSignal()
+
+    def __init__(self, parent=None, columns=2, header_hidden=True, extended_sel=False, protected_column=None):
+        super(FCTree, self).__init__(parent)
+
+        self.setColumnCount(columns)
+        self.setHeaderHidden(header_hidden)
+        self.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+        self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Expanding)
+
+        palette = QtGui.QPalette()
+        palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight,
+                         palette.color(QtGui.QPalette.Active, QtGui.QPalette.Highlight))
+
+        # make inactive rows text some color as active; may be useful in the future
+        # palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText,
+        #                  palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
+        self.setPalette(palette)
+
+        if extended_sel:
+            self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+
+        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        self.protected_column = protected_column
+        self.itemDoubleClicked.connect(self.on_double_click)
+        self.header().sectionDoubleClicked.connect(self.on_header_double_click)
+        self.resize_sig.connect(self.on_resize)
+
+    def on_double_click(self, item, column):
+        # from here: https://stackoverflow.com/questions/2801959/making-only-one-column-of-a-qtreewidgetitem-editable
+        tmp_flags = item.flags()
+        if self.is_editable(column):
+            item.setFlags(tmp_flags | QtCore.Qt.ItemIsEditable)
+        elif tmp_flags & QtCore.Qt.ItemIsEditable:
+            item.setFlags(tmp_flags ^ QtCore.Qt.ItemIsEditable)
+
+    def on_header_double_click(self, column):
+        header = self.header()
+        header.setSectionResizeMode(column, QtWidgets.QHeaderView.ResizeToContents)
+        width = header.sectionSize(column)
+        header.setSectionResizeMode(column, QtWidgets.QHeaderView.Interactive)
+        header.resizeSection(column, width)
+
+    def is_editable(self, tested_col):
+        try:
+            ret_val = False if tested_col in self.protected_column else True
+        except TypeError:
+            ret_val = False
+        return ret_val
+
+    def addParent(self, parent, title, expanded=False, color=None, font=None):
+        item = QtWidgets.QTreeWidgetItem(parent, [title])
+        item.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.ShowIndicator)
+        item.setExpanded(expanded)
+        if color is not None:
+            # item.setTextColor(0, color) # PyQt4
+            item.setForeground(0, QtGui.QBrush(color))
+        if font is not None:
+            item.setFont(0, font)
+        return item
+
+    def addParentEditable(self, parent, title, color=None, font=None, font_items=None, editable=False):
+        item = QtWidgets.QTreeWidgetItem(parent)
+        item.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.DontShowIndicator)
+        if editable:
+            item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
+
+        item.setFlags(item.flags() | QtCore.Qt.ItemIsSelectable)
+
+        for t in range(len(title)):
+            item.setText(t, title[t])
+
+        if color is not None:
+            # item.setTextColor(0, color) # PyQt4
+            item.setForeground(0, QtGui.QBrush(color))
+
+        if font and font_items:
+            try:
+                for fi in font_items:
+                    item.setFont(fi, font)
+            except TypeError:
+                item.setFont(font_items, font)
+        elif font:
+            item.setFont(0, font)
+        return item
+
+    def addChild(self, parent, title, column1=None, font=None, font_items=None, editable=False):
+        item = QtWidgets.QTreeWidgetItem(parent)
+        if editable:
+            item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
+
+        item.setText(0, str(title[0]))
+        if column1 is not None:
+            item.setText(1, str(title[1]))
+        if font and font_items:
+            try:
+                for fi in font_items:
+                    item.setFont(fi, font)
+            except TypeError:
+                item.setFont(font_items, font)
+
+    def resizeEvent(self, event):
+        """ Resize all sections to content and user interactive """
+
+        super(FCTree, self).resizeEvent(event)
+        self.on_resize()
+
+    def on_resize(self):
+        header = self.header()
+        for column in range(header.count()):
+            header.setSectionResizeMode(column, QtWidgets.QHeaderView.ResizeToContents)
+            width = header.sectionSize(column)
+            header.setSectionResizeMode(column, QtWidgets.QHeaderView.Interactive)
+            header.resizeSection(column, width)
+
+
+class FCLineEdit(QtWidgets.QLineEdit):
+
+    def __init__(self, *args, **kwargs):
+        super(FCLineEdit, self).__init__(*args, **kwargs)
+
+        self.menu = None
+
+    def contextMenuEvent(self, event):
+        self.menu = QtWidgets.QMenu()
+
+        # UNDO
+        undo_action = QAction('%s\t%s' % (_("Undo"), _('Ctrl+Z')), self)
+        self.menu.addAction(undo_action)
+        undo_action.triggered.connect(self.undo)
+        if self.isUndoAvailable() is False:
+            undo_action.setDisabled(True)
+
+        # REDO
+        redo_action = QAction('%s\t%s' % (_("Redo"), _('Ctrl+Y')), self)
+        self.menu.addAction(redo_action)
+        redo_action.triggered.connect(self.redo)
+        if self.isRedoAvailable() is False:
+            redo_action.setDisabled(True)
+
+        self.menu.addSeparator()
+
+        # CUT
+        cut_action = QAction('%s\t%s' % (_("Cut"), _('Ctrl+X')), self)
+        self.menu.addAction(cut_action)
+        cut_action.triggered.connect(self.cut_text)
+        if not self.hasSelectedText():
+            cut_action.setDisabled(True)
+
+        # COPY
+        copy_action = QAction('%s\t%s' % (_("Copy"), _('Ctrl+C')), self)
+        self.menu.addAction(copy_action)
+        copy_action.triggered.connect(self.copy_text)
+        if not self.hasSelectedText():
+            copy_action.setDisabled(True)
+
+        # PASTE
+        paste_action = QAction('%s\t%s' % (_("Paste"), _('Ctrl+V')), self)
+        self.menu.addAction(paste_action)
+        paste_action.triggered.connect(self.paste_text)
+
+        # DELETE
+        delete_action = QAction('%s\t%s' % (_("Delete"), _('Del')), self)
+        self.menu.addAction(delete_action)
+        delete_action.triggered.connect(self.del_)
+
+        self.menu.addSeparator()
+
+        # SELECT ALL
+        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)
+
+        self.menu.exec_(event.globalPos())
+
+    def cut_text(self):
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = self.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+        self.del_()
+
+    def copy_text(self):
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = self.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+    def paste_text(self):
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = clipboard.text()
+        self.insert(txt)
+
+
+class LengthEntry(FCLineEdit):
+    def __init__(self, output_units='IN', decimals=None, parent=None):
+        super(LengthEntry, self).__init__(parent)
+
+        self.output_units = output_units
+        self.format_re = re.compile(r"^([^\s]+)(?:\s([a-zA-Z]+))?$")
+
+        # Unit conversion table OUTPUT-INPUT
+        self.scales = {
+            'IN': {'IN': 1.0,
+                   'MM': 1 / 25.4},
+            'MM': {'IN': 25.4,
+                   'MM': 1.0}
+        }
+        self.readyToEdit = True
+        self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
+
+    def on_edit_finished(self):
+        self.clearFocus()
+
+    def mousePressEvent(self, e, Parent=None):
+        super(LengthEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(LengthEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
+
+    def returnPressed(self, *args, **kwargs):
+        val = self.get_value()
+        if val is not None:
+            self.set_text(str(val))
+        else:
+            log.warning("Could not interpret entry: %s" % self.get_text())
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        # match = self.format_re.search(raw)
+
+        try:
+            units = raw[-2:]
+            units = self.scales[self.output_units][units.upper()]
+            value = raw[:-2]
+            return float(eval(value)) * units
+        except IndexError:
+            value = raw
+            return float(eval(value))
+        except KeyError:
+            value = raw
+            return float(eval(value))
+        except Exception:
+            log.warning("Could not parse value in entry: %s" % str(raw))
+            return None
+
+    def set_value(self, val, decimals=None):
+        dec_digits = decimals if decimals is not None else self.decimals
+        self.setText(str('%.*f' % (dec_digits, val)))
+
+    def sizeHint(self):
+        default_hint_size = super(LengthEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FloatEntry(FCLineEdit):
+    def __init__(self, decimals=None, parent=None):
+        super(FloatEntry, self).__init__(parent)
+        self.readyToEdit = True
+        self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
+
+    def on_edit_finished(self):
+        self.clearFocus()
+
+    def mousePressEvent(self, e, Parent=None):
+        super(FloatEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit is True:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FloatEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
+
+    def returnPressed(self, *args, **kwargs):
+        val = self.get_value()
+        if val is not None:
+            self.set_text(str(val))
+        else:
+            log.warning("Could not interpret entry: %s" % self.text())
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+
+        try:
+            evaled = eval(raw)
+            return float(evaled)
+        except Exception as e:
+            if raw != '':
+                log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
+            return None
+
+    def set_value(self, val, decimals=None):
+        dig_digits = decimals if decimals is not None else self.decimals
+        if val is not None:
+            self.setText("%.*f" % (dig_digits, float(val)))
+        else:
+            self.setText("")
+
+    def sizeHint(self):
+        default_hint_size = super(FloatEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FloatEntry2(FCLineEdit):
+    def __init__(self, decimals=None, parent=None):
+        super(FloatEntry2, self).__init__(parent)
+        self.readyToEdit = True
+        self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
+
+    def on_edit_finished(self):
+        self.clearFocus()
+
+    def mousePressEvent(self, e, Parent=None):
+        super(FloatEntry2, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FloatEntry2, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+
+        try:
+            evaled = eval(raw)
+            return float(evaled)
+        except Exception as e:
+            if raw != '':
+                log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
+            return None
+
+    def set_value(self, val, decimals=None):
+        dig_digits = decimals if decimals is not None else self.decimals
+        self.setText("%.*f" % (dig_digits, val))
+
+    def sizeHint(self):
+        default_hint_size = super(FloatEntry2, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class IntEntry(FCLineEdit):
+
+    def __init__(self, parent=None, allow_empty=False, empty_val=None):
+        super(IntEntry, self).__init__(parent)
+        self.allow_empty = allow_empty
+        self.empty_val = empty_val
+        self.readyToEdit = True
+        self.editingFinished.connect(self.on_edit_finished)
+
+    def on_edit_finished(self):
+        self.clearFocus()
+
+    def mousePressEvent(self, e, Parent=None):
+        super(IntEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(IntEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
+
+    def get_value(self):
+
+        if self.allow_empty:
+            if str(self.text()) == "":
+                return self.empty_val
+        # make the text() first a float and then int because if text is a float type,
+        # the int() can't convert directly a "text float" into a int type.
+        ret_val = float(self.text())
+        ret_val = int(ret_val)
+        return ret_val
+
+    def set_value(self, val):
+
+        if val == self.empty_val and self.allow_empty:
+            self.setText("")
+            return
+
+        self.setText(str(val))
+
+    def sizeHint(self):
+        default_hint_size = super(IntEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCEntry(FCLineEdit):
+    def __init__(self, decimals=None, alignment=None, border_color=None, parent=None):
+        super(FCEntry, self).__init__(parent)
+        self.readyToEdit = True
+        self.editingFinished.connect(self.on_edit_finished)
+        self.decimals = decimals if decimals is not None else 4
+
+        if border_color:
+            self.setStyleSheet("QLineEdit {border: 1px solid %s;}" % border_color)
+
+        if alignment:
+            if alignment == 'center':
+                align_val = QtCore.Qt.AlignHCenter
+            elif alignment == 'right':
+                align_val = QtCore.Qt.AlignRight
+            else:
+                align_val = QtCore.Qt.AlignLeft
+            self.setAlignment(align_val)
+
+    def on_edit_finished(self):
+        self.clearFocus()
+
+    def mousePressEvent(self, e, parent=None):
+        super(FCEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FCEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
+
+    def get_value(self):
+        return str(self.text())
+
+    def set_value(self, val, decimals=None):
+        decimal_digits = decimals if decimals is not None else self.decimals
+        if type(val) is float:
+            self.setText('%.*f' % (decimal_digits, val))
+        elif val is None:
+            self.setText('')
+        else:
+            self.setText(str(val))
+
+    def sizeHint(self):
+        default_hint_size = super(FCEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCEntry2(FCEntry):
+    def __init__(self, parent=None):
+        super(FCEntry2, self).__init__(parent)
+
+    def set_value(self, val, decimals=4):
+        try:
+            fval = float(val)
+        except ValueError:
+            return
+        self.setText('%.*f' % (decimals, fval))
+
+
+class FCEntry3(FCEntry):
+    def __init__(self, parent=None):
+        super(FCEntry3, self).__init__(parent)
+
+    def set_value(self, val, decimals=4):
+        try:
+            fval = float(val)
+        except ValueError:
+            return
+        self.setText('%.*f' % (decimals, fval))
+
+    def get_value(self):
+        value = str(self.text()).strip(' ')
+
+        try:
+            return float(eval(value))
+        except Exception as e:
+            log.warning("Could not parse value in entry: %s" % str(e))
+            return None
+
+
+class EvalEntry(FCLineEdit):
+    def __init__(self, border_color=None, parent=None):
+        super(EvalEntry, self).__init__(parent)
+        self.readyToEdit = True
+
+        if border_color:
+            self.setStyleSheet("QLineEdit {border: 1px solid %s;}" % border_color)
+
+        self.editingFinished.connect(self.on_edit_finished)
+
+    def on_edit_finished(self):
+        self.clearFocus()
+
+    def mousePressEvent(self, e, parent=None):
+        super(EvalEntry, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(EvalEntry, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
+
+    def returnPressed(self, *args, **kwargs):
+        val = self.get_value()
+        if val is not None:
+            self.setText(str(val))
+        else:
+            log.warning("Could not interpret entry: %s" % self.get_text())
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        try:
+            evaled = eval(raw)
+        except Exception as e:
+            if raw != '':
+                log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
+            return None
+        return evaled
+
+    def set_value(self, val):
+        self.setText(str(val))
+
+    def sizeHint(self):
+        default_hint_size = super(EvalEntry, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class EvalEntry2(FCLineEdit):
+    def __init__(self, parent=None):
+        super(EvalEntry2, self).__init__(parent)
+        self.readyToEdit = True
+        self.editingFinished.connect(self.on_edit_finished)
+
+    def on_edit_finished(self):
+        self.clearFocus()
+
+    def mousePressEvent(self, e, parent=None):
+        super(EvalEntry2, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(EvalEntry2, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.deselect()
+            self.readyToEdit = True
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+
+        try:
+            evaled = eval(raw)
+        except Exception as e:
+            if raw != '':
+                log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
+            return None
+        return evaled
+
+    def set_value(self, val):
+        self.setText(str(val))
+
+    def sizeHint(self):
+        default_hint_size = super(EvalEntry2, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class NumericalEvalEntry(FCEntry):
+    """
+    Will evaluate the input and return a value. Accepts only float numbers and formulas using the operators: /,*,+,-,%
+    """
+
+    def __init__(self, border_color=None):
+        super().__init__(border_color=border_color)
+
+        regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\,\s]*")
+        validator = QtGui.QRegExpValidator(regex, self)
+        self.setValidator(validator)
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        raw = raw.replace(',', '.')
+        try:
+            evaled = eval(raw)
+        except Exception as e:
+            if raw != '':
+                log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
+            return None
+        return evaled
+
+
+class NumericalEvalTupleEntry(EvalEntry):
+    """
+    Will return a text value. Accepts only float numbers and formulas using the operators: /,*,+,-,%
+    """
+
+    def __init__(self, border_color=None):
+        super().__init__(border_color=border_color)
+
+        regex = QtCore.QRegExp("[0-9\/\*\+\-\%\.\s\,\[\]\(\)]*")
+        validator = QtGui.QRegExpValidator(regex, self)
+        self.setValidator(validator)
+
+    def get_value(self):
+        raw = str(self.text()).strip(' ')
+        try:
+            evaled = eval(raw)
+        except Exception as e:
+            if raw != '':
+                log.error("Could not evaluate val: %s, error: %s" % (str(raw), str(e)))
+            return None
+        return evaled
+
+
+class FCColorEntry(QtWidgets.QFrame):
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+
+        self.entry = FCEntry()
+        regex = QtCore.QRegExp("[#A-Fa-f0-9]*")
+        validator = QtGui.QRegExpValidator(regex, self.entry)
+        self.entry.setValidator(validator)
+
+        self.button = QtWidgets.QPushButton()
+        self.button.setFixedSize(15, 15)
+        self.button.setStyleSheet("border-color: dimgray;")
+
+        self.layout = QtWidgets.QHBoxLayout()
+        self.layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.layout.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.entry)
+        self.layout.addWidget(self.button)
+        self.setLayout(self.layout)
+
+        self.entry.editingFinished.connect(self._sync_button_color)
+        self.button.clicked.connect(self._on_button_clicked)
+
+        self.editingFinished = self.entry.editingFinished
+
+    def get_value(self) -> str:
+        return self.entry.get_value()
+
+    def set_value(self, value: str):
+        self.entry.set_value(value)
+        self._sync_button_color()
+
+    def _sync_button_color(self):
+        value = self.get_value()
+        self.button.setStyleSheet("background-color:%s;" % self._extract_color(value))
+
+    def _on_button_clicked(self):
+        value = self.entry.get_value()
+        current_color = QtGui.QColor(self._extract_color(value))
+
+        color_dialog = QtWidgets.QColorDialog()
+        selected_color = color_dialog.getColor(initial=current_color, options=QtWidgets.QColorDialog.ShowAlphaChannel)
+
+        if selected_color.isValid() is False:
+            return
+
+        new_value = str(selected_color.name()) + self._extract_alpha(value)
+        self.set_value(new_value)
+
+    def _extract_color(self, value: str) -> str:
+        return value[:7]
+
+    def _extract_alpha(self, value: str) -> str:
+        return value[7:9]
+
+
+class FCSliderWithSpinner(QtWidgets.QFrame):
+
+    def __init__(self, min=0, max=100, step=1, **kwargs):
+        super().__init__(**kwargs)
+
+        self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
+        self.slider.setMinimum(min)
+        self.slider.setMaximum(max)
+        self.slider.setSingleStep(step)
+
+        self.spinner = FCSpinner()
+        self.spinner.set_range(min, max)
+        self.spinner.set_step(step)
+        self.spinner.setMinimumWidth(70)
+
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
+        self.spinner.setSizePolicy(sizePolicy)
+
+        self.layout = QtWidgets.QHBoxLayout()
+        self.layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.layout.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.slider)
+        self.layout.addWidget(self.spinner)
+        self.setLayout(self.layout)
+
+        self.slider.valueChanged.connect(self._on_slider)
+        self.spinner.valueChanged.connect(self._on_spinner)
+
+        self.valueChanged = self.spinner.valueChanged
+
+    def get_value(self) -> int:
+        return self.spinner.get_value()
+
+    def set_value(self, value: int):
+        self.spinner.set_value(value)
+
+    def _on_spinner(self):
+        spinner_value = self.spinner.value()
+        self.slider.setValue(spinner_value)
+
+    def _on_slider(self):
+        slider_value = self.slider.value()
+        self.spinner.set_value(slider_value)
+
+
+class FCSpinner(QtWidgets.QSpinBox):
+    returnPressed = QtCore.pyqtSignal()
+    confirmation_signal = QtCore.pyqtSignal(bool, float, float)
+
+    def __init__(self, suffix=None, alignment=None, parent=None, callback=None, policy=True):
+        super(FCSpinner, self).__init__(parent)
+        self.readyToEdit = True
+
+        self.editingFinished.connect(self.on_edit_finished)
+        if callback:
+            self.confirmation_signal.connect(callback)
+
+        self.lineEdit().installEventFilter(self)
+
+        if suffix:
+            self.setSuffix(' %s' % str(suffix))
+
+        if alignment:
+            if alignment == 'center':
+                align_val = QtCore.Qt.AlignHCenter
+            elif alignment == 'right':
+                align_val = QtCore.Qt.AlignRight
+            else:
+                align_val = QtCore.Qt.AlignLeft
+            self.setAlignment(align_val)
+
+        self.prev_readyToEdit = True
+        self.menu = None
+
+        if policy:
+            sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Preferred)
+            self.setSizePolicy(sizePolicy)
+
+    def eventFilter(self, object, event):
+        if event.type() == QtCore.QEvent.MouseButtonPress and self.prev_readyToEdit is True:
+            self.prev_readyToEdit = False
+            if self.isEnabled():
+                if self.readyToEdit:
+                    self.lineEdit().selectAll()
+                    self.readyToEdit = False
+                else:
+                    self.lineEdit().deselect()
+                return True
+        return False
+
+    def keyPressEvent(self, event):
+        if event.key() == Qt.Key_Enter:
+            self.returnPressed.emit()
+            self.clearFocus()
+        else:
+            super().keyPressEvent(event)
+
+    def wheelEvent(self, *args, **kwargs):
+        # should work only there is a focus in the lineedit of the SpinBox
+        if self.readyToEdit is False:
+            super().wheelEvent(*args, **kwargs)
+
+    def on_edit_finished(self):
+        self.clearFocus()
+        self.returnPressed.emit()
+
+    # def mousePressEvent(self, e, parent=None):
+    #     super(FCSpinner, self).mousePressEvent(e)  # required to deselect on 2e click
+    #     if self.readyToEdit:
+    #         self.lineEdit().selectAll()
+    #         self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FCSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.lineEdit().deselect()
+            self.readyToEdit = True
+            self.prev_readyToEdit = True
+
+    def contextMenuEvent(self, event):
+        self.menu = QtWidgets.QMenu()
+        line_edit = self.lineEdit()
+
+        # UNDO
+        undo_action = QAction('%s\t%s' % (_("Undo"), _('Ctrl+Z')), self)
+        self.menu.addAction(undo_action)
+        undo_action.triggered.connect(line_edit.undo)
+        if line_edit.isUndoAvailable() is False:
+            undo_action.setDisabled(True)
+
+        # REDO
+        redo_action = QAction('%s\t%s' % (_("Redo"), _('Ctrl+Y')), self)
+        self.menu.addAction(redo_action)
+        redo_action.triggered.connect(line_edit.redo)
+        if line_edit.isRedoAvailable() is False:
+            redo_action.setDisabled(True)
+
+        self.menu.addSeparator()
+
+        # CUT
+        cut_action = QAction('%s\t%s' % (_("Cut"), _('Ctrl+X')), self)
+        self.menu.addAction(cut_action)
+        cut_action.triggered.connect(self.cut_text)
+        if not line_edit.hasSelectedText():
+            cut_action.setDisabled(True)
+
+        # COPY
+        copy_action = QAction('%s\t%s' % (_("Copy"), _('Ctrl+C')), self)
+        self.menu.addAction(copy_action)
+        copy_action.triggered.connect(self.copy_text)
+        if not line_edit.hasSelectedText():
+            copy_action.setDisabled(True)
+
+        # PASTE
+        paste_action = QAction('%s\t%s' % (_("Paste"), _('Ctrl+V')), self)
+        self.menu.addAction(paste_action)
+        paste_action.triggered.connect(self.paste_text)
+
+        # DELETE
+        delete_action = QAction('%s\t%s' % (_("Delete"), _('Del')), self)
+        self.menu.addAction(delete_action)
+        delete_action.triggered.connect(line_edit.del_)
+
+        self.menu.addSeparator()
+
+        # SELECT ALL
+        sel_all_action = QAction('%s\t%s' % (_("Select All"), _('Ctrl+A')), self)
+        self.menu.addAction(sel_all_action)
+        sel_all_action.triggered.connect(line_edit.selectAll)
+
+        self.menu.addSeparator()
+
+        # STEP UP
+        step_up_action = QAction('%s\t%s' % (_("Step Up"), ''), self)
+        self.menu.addAction(step_up_action)
+        step_up_action.triggered.connect(self.stepUp)
+
+        # STEP DOWN
+        step_down_action = QAction('%s\t%s' % (_("Step Down"), ''), self)
+        self.menu.addAction(step_down_action)
+        step_down_action.triggered.connect(self.stepDown)
+
+        self.menu.exec_(event.globalPos())
+
+    def cut_text(self):
+        clipboard = QtWidgets.QApplication.clipboard()
+        line_edit = self.lineEdit()
+
+        txt = line_edit.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+        line_edit.del_()
+
+    def copy_text(self):
+        clipboard = QtWidgets.QApplication.clipboard()
+        line_edit = self.lineEdit()
+
+        txt = line_edit.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+    def paste_text(self):
+        clipboard = QtWidgets.QApplication.clipboard()
+        line_edit = self.lineEdit()
+
+        txt = clipboard.text()
+        line_edit.insert(txt)
+
+    def valueFromText(self, text):
+        txt = text.strip('%%')
+        try:
+            ret_val = int(txt)
+        except ValueError:
+            ret_val = 0
+        return ret_val
+
+    def get_value(self):
+        return int(self.value())
+
+    def set_value(self, val):
+        try:
+            k = int(val)
+        except Exception as e:
+            log.debug(str(e))
+            return
+        self.setValue(k)
+
+    def validate(self, p_str, p_int):
+        text = p_str
+
+        min_val = self.minimum()
+        max_val = self.maximum()
+        try:
+            if int(text) < min_val or int(text) > max_val:
+                self.confirmation_signal.emit(False, min_val, max_val)
+                return QtGui.QValidator.Intermediate, text, p_int
+        except ValueError:
+            pass
+
+        self.confirmation_signal.emit(True, min_val, max_val)
+        return QtGui.QValidator.Acceptable, p_str, p_int
+
+    def set_range(self, min_val, max_val):
+        self.blockSignals(True)
+        self.setRange(min_val, max_val)
+        self.blockSignals(False)
+
+    def set_step(self, p_int):
+        self.blockSignals(True)
+        self.setSingleStep(p_int)
+        self.blockSignals(False)
+
+    # def sizeHint(self):
+    #     default_hint_size = super(FCSpinner, self).sizeHint()
+    #     return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCDoubleSlider(QtWidgets.QSlider):
+    # frome here: https://stackoverflow.com/questions/42820380/use-float-for-qslider
+
+    # create our our signal that we can connect to if necessary
+    doubleValueChanged = pyqtSignal(float)
+
+    def __init__(self, decimals=3, orientation='horizontal', *args, **kargs):
+        if orientation == 'horizontal':
+            super(FCDoubleSlider, self).__init__(QtCore.Qt.Horizontal, *args, **kargs)
+        else:
+            super(FCDoubleSlider, self).__init__(QtCore.Qt.Vertical, *args, **kargs)
+
+        self._multi = 10 ** decimals
+
+        self.valueChanged.connect(self.emitDoubleValueChanged)
+
+    def emitDoubleValueChanged(self):
+        value = float(super(FCDoubleSlider, self).value()) / self._multi
+        self.doubleValueChanged.emit(value)
+
+    def value(self):
+        return float(super(FCDoubleSlider, self).value()) / self._multi
+
+    def get_value(self):
+        return self.value()
+
+    def setMinimum(self, value):
+        return super(FCDoubleSlider, self).setMinimum(value * self._multi)
+
+    def setMaximum(self, value):
+        return super(FCDoubleSlider, self).setMaximum(value * self._multi)
+
+    def setSingleStep(self, value):
+        return super(FCDoubleSlider, self).setSingleStep(value * self._multi)
+
+    def singleStep(self):
+        return float(super(FCDoubleSlider, self).singleStep()) / self._multi
+
+    def set_value(self, value):
+        super(FCDoubleSlider, self).setValue(int(value * self._multi))
+
+    def set_precision(self, decimals):
+        self._multi = 10 ** decimals
+
+    def set_range(self, min, max):
+        self.blockSignals(True)
+        self.setRange(min * self._multi, max * self._multi)
+        self.blockSignals(False)
+
+
+class FCSliderWithDoubleSpinner(QtWidgets.QFrame):
+
+    def __init__(self, min=0, max=10000.0000, step=1, precision=4, orientation='horizontal', **kwargs):
+        super().__init__(**kwargs)
+
+        self.slider = FCDoubleSlider(orientation=orientation)
+        self.slider.setMinimum(min)
+        self.slider.setMaximum(max)
+        self.slider.setSingleStep(step)
+        self.slider.set_range(min, max)
+
+        self.spinner = FCDoubleSpinner()
+        self.spinner.set_range(min, max)
+        self.spinner.set_precision(precision)
+
+        self.spinner.set_step(step)
+        self.spinner.setMinimumWidth(70)
+
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
+        self.spinner.setSizePolicy(sizePolicy)
+
+        self.layout = QtWidgets.QHBoxLayout()
+        self.layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.layout.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.slider)
+        self.layout.addWidget(self.spinner)
+        self.setLayout(self.layout)
+
+        self.slider.doubleValueChanged.connect(self._on_slider)
+        self.spinner.valueChanged.connect(self._on_spinner)
+
+        self.valueChanged = self.spinner.valueChanged
+
+    def set_precision(self, prec):
+        self.spinner.set_precision(prec)
+
+    def setSingleStep(self, step):
+        self.spinner.set_step(step)
+
+    def set_range(self, min, max):
+        self.spinner.set_range(min, max)
+        self.slider.set_range(min, max)
+
+    def set_minimum(self, min):
+        self.slider.setMinimum(min)
+        self.spinner.setMinimum(min)
+
+    def set_maximum(self, max):
+        self.slider.setMaximum(max)
+        self.spinner.setMaximum(max)
+
+    def get_value(self) -> float:
+        return self.spinner.get_value()
+
+    def set_value(self, value: float):
+        self.spinner.set_value(value)
+
+    def _on_spinner(self):
+        spinner_value = self.spinner.value()
+        self.slider.set_value(spinner_value)
+
+    def _on_slider(self):
+        slider_value = self.slider.value()
+        self.spinner.set_value(slider_value)
+
+
+class FCButtonWithDoubleSpinner(QtWidgets.QFrame):
+
+    def __init__(self, min=0, max=100, step=1, decimals=4, button_text='', button_icon=None, callback=None, **kwargs):
+        super().__init__(**kwargs)
+
+        self.button = QtWidgets.QToolButton()
+        if button_text != '':
+            self.button.setText(button_text)
+        if button_icon:
+            self.button.setIcon(button_icon)
+
+        self.spinner = FCDoubleSpinner()
+        self.spinner.set_range(min, max)
+        self.spinner.set_step(step)
+        self.spinner.set_precision(decimals)
+        self.spinner.setMinimumWidth(70)
+
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)
+        self.spinner.setSizePolicy(sizePolicy)
+
+        self.layout = QtWidgets.QHBoxLayout()
+        self.layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.layout.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.spinner)
+        self.layout.addWidget(self.button)
+        self.setLayout(self.layout)
+
+        self.valueChanged = self.spinner.valueChanged
+
+        self._callback = callback
+        self.button.clicked.connect(self._callback)
+
+    def get_value(self) -> float:
+        return self.spinner.get_value()
+
+    def set_value(self, value: float):
+        self.spinner.set_value(value)
+
+    def set_callback(self, callback):
+        self._callback = callback
+
+    def set_text(self, txt: str):
+        if txt:
+            self.button.setText(txt)
+
+    def set_icon(self, icon: QtGui.QIcon):
+        self.button.setIcon(icon)
+
+
+class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
+    returnPressed = QtCore.pyqtSignal()
+    confirmation_signal = QtCore.pyqtSignal(bool, float, float)
+
+    def __init__(self, suffix=None, alignment=None, parent=None, callback=None, policy=True):
+        """
+
+        :param suffix:      a char added to the end of the value in the LineEdit; like a '%' or '$' etc
+        :param alignment:   the value is aligned to left or right
+        :param parent:
+        :param callback:    called when the entered value is outside limits; the min and max value will be passed to it
+        """
+        super(FCDoubleSpinner, self).__init__(parent)
+        self.readyToEdit = True
+
+        self.editingFinished.connect(self.on_edit_finished)
+        if callback:
+            self.confirmation_signal.connect(callback)
+
+        self.lineEdit().installEventFilter(self)
+
+        # by default don't allow the minus sign to be entered as the default for QDoubleSpinBox is the positive range
+        # between 0.00 and 99.00 (2 decimals)
+        self.lineEdit().setValidator(
+            QtGui.QRegExpValidator(QtCore.QRegExp("\+?[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
+
+        if suffix:
+            self.setSuffix(' %s' % str(suffix))
+
+        if alignment:
+            if alignment == 'center':
+                align_val = QtCore.Qt.AlignHCenter
+            elif alignment == 'right':
+                align_val = QtCore.Qt.AlignRight
+            else:
+                align_val = QtCore.Qt.AlignLeft
+            self.setAlignment(align_val)
+
+        self.prev_readyToEdit = True
+        self.menu = None
+
+        if policy:
+            sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Preferred)
+            self.setSizePolicy(sizePolicy)
+
+    def on_edit_finished(self):
+        self.clearFocus()
+        self.returnPressed.emit()
+
+    def eventFilter(self, object, event):
+        if event.type() == QtCore.QEvent.MouseButtonPress and self.prev_readyToEdit is True:
+            self.prev_readyToEdit = False
+            if self.isEnabled():
+                if self.readyToEdit:
+                    self.cursor_pos = self.lineEdit().cursorPosition()
+                    self.lineEdit().selectAll()
+                    self.readyToEdit = False
+                else:
+                    self.lineEdit().deselect()
+                return True
+        return False
+
+    def keyPressEvent(self, event):
+        if event.key() == Qt.Key_Enter:
+            self.returnPressed.emit()
+            self.clearFocus()
+        else:
+            super().keyPressEvent(event)
+
+    def wheelEvent(self, *args, **kwargs):
+        # should work only there is a focus in the lineedit of the SpinBox
+        if self.readyToEdit is False:
+            super().wheelEvent(*args, **kwargs)
+
+    def focusOutEvent(self, e):
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(FCDoubleSpinner, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.lineEdit().deselect()
+            self.readyToEdit = True
+            self.prev_readyToEdit = True
+
+    def contextMenuEvent(self, event):
+        self.menu = QtWidgets.QMenu()
+        line_edit = self.lineEdit()
+
+        # UNDO
+        undo_action = QAction('%s\t%s' % (_("Undo"), _('Ctrl+Z')), self)
+        self.menu.addAction(undo_action)
+        undo_action.triggered.connect(line_edit.undo)
+        if line_edit.isUndoAvailable() is False:
+            undo_action.setDisabled(True)
+
+        # REDO
+        redo_action = QAction('%s\t%s' % (_("Redo"), _('Ctrl+Y')), self)
+        self.menu.addAction(redo_action)
+        redo_action.triggered.connect(line_edit.redo)
+        if line_edit.isRedoAvailable() is False:
+            redo_action.setDisabled(True)
+
+        self.menu.addSeparator()
+
+        # CUT
+        cut_action = QAction('%s\t%s' % (_("Cut"), _('Ctrl+X')), self)
+        self.menu.addAction(cut_action)
+        cut_action.triggered.connect(self.cut_text)
+        if not line_edit.hasSelectedText():
+            cut_action.setDisabled(True)
+
+        # COPY
+        copy_action = QAction('%s\t%s' % (_("Copy"), _('Ctrl+C')), self)
+        self.menu.addAction(copy_action)
+        copy_action.triggered.connect(self.copy_text)
+        if not line_edit.hasSelectedText():
+            copy_action.setDisabled(True)
+
+        # PASTE
+        paste_action = QAction('%s\t%s' % (_("Paste"), _('Ctrl+V')), self)
+        self.menu.addAction(paste_action)
+        paste_action.triggered.connect(self.paste_text)
+
+        # DELETE
+        delete_action = QAction('%s\t%s' % (_("Delete"), _('Del')), self)
+        self.menu.addAction(delete_action)
+        delete_action.triggered.connect(line_edit.del_)
+
+        self.menu.addSeparator()
+
+        # SELECT ALL
+        sel_all_action = QAction('%s\t%s' % (_("Select All"), _('Ctrl+A')), self)
+        self.menu.addAction(sel_all_action)
+        sel_all_action.triggered.connect(line_edit.selectAll)
+
+        self.menu.addSeparator()
+
+        # STEP UP
+        step_up_action = QAction('%s\t%s' % (_("Step Up"), ''), self)
+        self.menu.addAction(step_up_action)
+        step_up_action.triggered.connect(self.stepUp)
+
+        # STEP DOWN
+        step_down_action = QAction('%s\t%s' % (_("Step Down"), ''), self)
+        self.menu.addAction(step_down_action)
+        step_down_action.triggered.connect(self.stepDown)
+
+        self.menu.exec_(event.globalPos())
+
+    def cut_text(self):
+        clipboard = QtWidgets.QApplication.clipboard()
+        line_edit = self.lineEdit()
+
+        txt = line_edit.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+        line_edit.del_()
+
+    def copy_text(self):
+        clipboard = QtWidgets.QApplication.clipboard()
+        line_edit = self.lineEdit()
+
+        txt = line_edit.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+    def paste_text(self):
+        clipboard = QtWidgets.QApplication.clipboard()
+        line_edit = self.lineEdit()
+
+        txt = clipboard.text()
+        line_edit.insert(txt)
+
+    def valueFromText(self, p_str):
+        text = p_str.replace(',', '.')
+        text = text.strip('%%')
+        try:
+            ret_val = float(text)
+        except ValueError:
+            ret_val = 0.0
+        return ret_val
+
+    def validate(self, p_str, p_int):
+        text = p_str.replace(',', '.')
+
+        min_val = self.minimum()
+        max_val = self.maximum()
+        try:
+            if float(text) < min_val or float(text) > max_val:
+                self.confirmation_signal.emit(False, min_val, max_val)
+                return QtGui.QValidator.Intermediate, text, p_int
+        except ValueError:
+            pass
+
+        self.confirmation_signal.emit(True, min_val, max_val)
+        return QtGui.QValidator.Acceptable, p_str, p_int
+
+    def get_value(self):
+        return float(self.value())
+
+    def set_value(self, val):
+        try:
+            k = float(val)
+        except Exception as e:
+            log.debug(str(e))
+            return
+        self.setValue(k)
+
+    def set_precision(self, val):
+        self.setDecimals(val)
+
+        # make sure that the user can't type more decimals than the set precision
+        if self.minimum() < 0 or self.maximum() <= 0:
+            self.lineEdit().setValidator(
+                QtGui.QRegExpValidator(QtCore.QRegExp("-?[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
+        else:
+            self.lineEdit().setValidator(
+                QtGui.QRegExpValidator(QtCore.QRegExp("\+?[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
+
+    def set_range(self, min_val, max_val):
+        if min_val < 0 or max_val <= 0:
+            self.lineEdit().setValidator(
+                QtGui.QRegExpValidator(QtCore.QRegExp("-?[0-9]*[.,]?[0-9]{%d}" % self.decimals()), self))
+
+        self.setRange(min_val, max_val)
+
+    def set_step(self, p_int):
+        self.blockSignals(True)
+        self.setSingleStep(p_int)
+        self.blockSignals(False)
+
+    # def sizeHint(self):
+    #     default_hint_size = super(FCDoubleSpinner, self).sizeHint()
+    #     return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCCheckBox(QtWidgets.QCheckBox):
+    def __init__(self, label='', parent=None):
+        super(FCCheckBox, self).__init__(str(label), parent)
+
+    def get_value(self):
+        return self.isChecked()
+
+    def set_value(self, val):
+        self.setChecked(val)
+
+    def toggle(self):
+        self.set_value(not self.get_value())
+
+
+class FCTextArea(QtWidgets.QPlainTextEdit):
+    def __init__(self, parent=None):
+        super(FCTextArea, self).__init__(parent)
+
+    def set_value(self, val):
+        self.setPlainText(val)
+
+    def get_value(self):
+        return str(self.toPlainText())
+
+    def sizeHint(self, custom_sizehint=None):
+        default_hint_size = super(FCTextArea, self).sizeHint()
+
+        if custom_sizehint is None:
+            return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+        else:
+            return QtCore.QSize(custom_sizehint, default_hint_size.height())
+
+
+class FCTextEdit(QtWidgets.QTextEdit):
+
+    def __init__(self, *args, **kwargs):
+        super(FCTextEdit, self).__init__(*args, **kwargs)
+
+        self.menu = None
+        self.undo_flag = False
+        self.redo_flag = False
+
+        self.undoAvailable.connect(self.on_undo_available)
+        self.redoAvailable.connect(self.on_redo_available)
+
+    def on_undo_available(self, val):
+        self.undo_flag = val
+
+    def on_redo_available(self, val):
+        self.redo_flag = val
+
+    def contextMenuEvent(self, event):
+        self.menu = QtWidgets.QMenu()
+        tcursor = self.textCursor()
+        txt = tcursor.selectedText()
+
+        # UNDO
+        undo_action = QAction('%s\t%s' % (_("Undo"), _('Ctrl+Z')), self)
+        self.menu.addAction(undo_action)
+        undo_action.triggered.connect(self.undo)
+        if self.undo_flag is False:
+            undo_action.setDisabled(True)
+
+        # REDO
+        redo_action = QAction('%s\t%s' % (_("Redo"), _('Ctrl+Y')), self)
+        self.menu.addAction(redo_action)
+        redo_action.triggered.connect(self.redo)
+        if self.redo_flag is False:
+            redo_action.setDisabled(True)
+
+        self.menu.addSeparator()
+
+        # CUT
+        cut_action = QAction('%s\t%s' % (_("Cut"), _('Ctrl+X')), self)
+        self.menu.addAction(cut_action)
+        cut_action.triggered.connect(self.cut_text)
+        if txt == '':
+            cut_action.setDisabled(True)
+
+        # COPY
+        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)
+
+        # PASTE
+        paste_action = QAction('%s\t%s' % (_("Paste"), _('Ctrl+V')), self)
+        self.menu.addAction(paste_action)
+        paste_action.triggered.connect(self.paste_text)
+
+        # DELETE
+        delete_action = QAction('%s\t%s' % (_("Delete"), _('Del')), self)
+        self.menu.addAction(delete_action)
+        delete_action.triggered.connect(self.delete_text)
+
+        self.menu.addSeparator()
+
+        # SELECT ALL
+        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)
+
+        self.menu.exec_(event.globalPos())
+
+    def cut_text(self):
+        tcursor = self.textCursor()
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = tcursor.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+        tcursor.deleteChar()
+
+    def copy_text(self):
+        tcursor = self.textCursor()
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = tcursor.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+    def paste_text(self):
+        tcursor = self.textCursor()
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = clipboard.text()
+        tcursor.insertText(txt)
+
+    def delete_text(self):
+        tcursor = self.textCursor()
+        tcursor.deleteChar()
+
+
+class FCTextAreaRich(FCTextEdit):
+    def __init__(self, parent=None):
+        super(FCTextAreaRich, self).__init__(parent)
+
+    def set_value(self, val):
+        self.setText(val)
+
+    def get_value(self):
+        return str(self.toPlainText())
+
+    def sizeHint(self):
+        default_hint_size = super(FCTextAreaRich, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+
+class FCTextAreaExtended(FCTextEdit):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+
+        self.completer = MyCompleter()
+
+        self.model = QtCore.QStringListModel()
+        self.completer.setModel(self.model)
+        self.set_model_data(keyword_list=[])
+        self.completer.insertText.connect(self.insertCompletion)
+        self.completer.popup().clicked.connect(self.insert_completion_click)
+
+        self.completer_enable = False
+
+    def set_model_data(self, keyword_list):
+        self.model.setStringList(keyword_list)
+
+    def insert_completion_click(self):
+        self.completer.insertText.emit(self.completer.getSelected())
+        self.completer.setCompletionMode(QCompleter.PopupCompletion)
+
+    def insertCompletion(self, completion):
+        tc = self.textCursor()
+        extra = (len(completion) - len(self.completer.completionPrefix()))
+
+        # don't insert if the word is finished but add a space instead
+        if extra == 0:
+            tc.insertText(' ')
+            self.completer.popup().hide()
+            return
+
+        tc.movePosition(QTextCursor.Left)
+        tc.movePosition(QTextCursor.EndOfWord)
+        tc.insertText(completion[-extra:])
+        # add a space after inserting the word
+        tc.insertText(' ')
+        self.setTextCursor(tc)
+        self.completer.popup().hide()
+
+    def focusInEvent(self, event):
+        if self.completer:
+            self.completer.setWidget(self)
+        QTextEdit.focusInEvent(self, event)
+
+    def set_value(self, val):
+        self.setText(val)
+
+    def get_value(self):
+        self.toPlainText()
+
+    def insertFromMimeData(self, data):
+        """
+        Reimplemented such that when SHIFT is pressed and doing click Paste in the contextual menu, the '\' symbol
+        is replaced with the '/' symbol. That's because of the difference in path separators in Windows and TCL
+        :param data:
+        :return:
+        """
+        modifier = QtWidgets.QApplication.keyboardModifiers()
+        if modifier == Qt.ShiftModifier:
+            text = data.text()
+            text = text.replace('\\', '/')
+            self.insertPlainText(text)
+        else:
+            self.insertPlainText(data.text())
+
+    def keyPressEvent(self, event):
+        """
+        Reimplemented so the CTRL + SHIFT + V shortcut key combo will paste the text but replacing '\' with '/'
+        :param event:
+        :return:
+        """
+        key = event.key()
+        modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if modifier & Qt.ControlModifier and modifier & Qt.ShiftModifier:
+            if key == QtCore.Qt.Key_V:
+                clipboard = QtWidgets.QApplication.clipboard()
+                clip_text = clipboard.text()
+                clip_text = clip_text.replace('\\', '/')
+                self.insertPlainText(clip_text)
+        elif modifier & Qt.ControlModifier:
+            if key == Qt.Key_Slash:
+                self.comment()
+
+        tc = self.textCursor()
+        if (key == Qt.Key_Tab or key == Qt.Key_Enter or key == Qt.Key_Return) and self.completer.popup().isVisible():
+            self.completer.insertText.emit(self.completer.getSelected())
+            self.completer.setCompletionMode(QCompleter.PopupCompletion)
+            return
+        elif key == Qt.Key_BraceLeft:
+            tc.insertText('{}')
+            self.moveCursor(QtGui.QTextCursor.Left)
+        elif key == Qt.Key_BracketLeft:
+            tc.insertText('[]')
+            self.moveCursor(QtGui.QTextCursor.Left)
+        elif key == Qt.Key_ParenLeft:
+            tc.insertText('()')
+            self.moveCursor(QtGui.QTextCursor.Left)
+
+        elif key == Qt.Key_BraceRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == '}':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText('}')
+        elif key == Qt.Key_BracketRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == ']':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText(']')
+        elif key == Qt.Key_ParenRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == ')':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText(')')
+        else:
+            super(FCTextAreaExtended, self).keyPressEvent(event)
+
+        if self.completer_enable:
+            tc.select(QTextCursor.WordUnderCursor)
+            cr = self.cursorRect()
+
+            if len(tc.selectedText()) > 0:
+                self.completer.setCompletionPrefix(tc.selectedText())
+                popup = self.completer.popup()
+                popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
+
+                cr.setWidth(self.completer.popup().sizeHintForColumn(0)
+                            + self.completer.popup().verticalScrollBar().sizeHint().width())
+                self.completer.complete(cr)
+            else:
+                self.completer.popup().hide()
+
+    def comment(self):
+        """
+        Got it from here:
+        https://stackoverflow.com/questions/49898820/how-to-get-text-next-to-cursor-in-qtextedit-in-pyqt4
+        :return:
+        """
+        pos = self.textCursor().position()
+        self.moveCursor(QtGui.QTextCursor.StartOfLine)
+        line_text = self.textCursor().block().text()
+        if self.textCursor().block().text().startswith(" "):
+            # skip the white space
+            self.moveCursor(QtGui.QTextCursor.NextWord)
+        self.moveCursor(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor)
+        character = self.textCursor().selectedText()
+        if character == "#":
+            # delete #
+            self.textCursor().deletePreviousChar()
+            # delete white space 
+            self.moveCursor(QtGui.QTextCursor.NextWord, QtGui.QTextCursor.KeepAnchor)
+            self.textCursor().removeSelectedText()
+        else:
+            self.moveCursor(QtGui.QTextCursor.PreviousCharacter, QtGui.QTextCursor.KeepAnchor)
+            self.textCursor().insertText("# ")
+        cursor = QtGui.QTextCursor(self.textCursor())
+        cursor.setPosition(pos)
+        self.setTextCursor(cursor)
+
+
+class FCPlainTextAreaExtended(QtWidgets.QPlainTextEdit):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+
+        self.completer = MyCompleter()
+
+        self.model = QtCore.QStringListModel()
+        self.completer.setModel(self.model)
+        self.set_model_data(keyword_list=[])
+        self.completer.insertText.connect(self.insertCompletion)
+        self.completer.popup().clicked.connect(self.insert_completion_click)
+
+        self.completer_enable = False
+
+        self.menu = None
+        self.undo_flag = False
+        self.redo_flag = False
+
+        self.undoAvailable.connect(self.on_undo_available)
+        self.redoAvailable.connect(self.on_redo_available)
+
+    def on_undo_available(self, val):
+        self.undo_flag = val
+
+    def on_redo_available(self, val):
+        self.redo_flag = val
+
+    def append(self, text):
+        """
+        Added this to make this subclass compatible with FCTextAreaExtended
+        :param text: string
+        :return:
+        """
+        self.appendPlainText(text)
+
+    def set_model_data(self, keyword_list):
+        self.model.setStringList(keyword_list)
+
+    def insert_completion_click(self):
+        self.completer.insertText.emit(self.completer.getSelected())
+        self.completer.setCompletionMode(QCompleter.PopupCompletion)
+
+    def insertCompletion(self, completion):
+        tc = self.textCursor()
+        extra = (len(completion) - len(self.completer.completionPrefix()))
+
+        # don't insert if the word is finished but add a space instead
+        if extra == 0:
+            tc.insertText(' ')
+            self.completer.popup().hide()
+            return
+
+        tc.movePosition(QTextCursor.Left)
+        tc.movePosition(QTextCursor.EndOfWord)
+        tc.insertText(completion[-extra:])
+        # add a space after inserting the word
+        tc.insertText(' ')
+        self.setTextCursor(tc)
+        self.completer.popup().hide()
+
+    def focusInEvent(self, event):
+        if self.completer:
+            self.completer.setWidget(self)
+        QtWidgets.QPlainTextEdit.focusInEvent(self, event)
+
+    def contextMenuEvent(self, event):
+        self.menu = QtWidgets.QMenu()
+        tcursor = self.textCursor()
+        txt = tcursor.selectedText()
+
+        # UNDO
+        undo_action = QAction('%s\t%s' % (_("Undo"), _('Ctrl+Z')), self)
+        self.menu.addAction(undo_action)
+        undo_action.triggered.connect(self.undo)
+        if self.undo_flag is False:
+            undo_action.setDisabled(True)
+
+        # REDO
+        redo_action = QAction('%s\t%s' % (_("Redo"), _('Ctrl+Y')), self)
+        self.menu.addAction(redo_action)
+        redo_action.triggered.connect(self.redo)
+        if self.redo_flag is False:
+            redo_action.setDisabled(True)
+
+        self.menu.addSeparator()
+
+        # CUT
+        cut_action = QAction('%s\t%s' % (_("Cut"), _('Ctrl+X')), self)
+        self.menu.addAction(cut_action)
+        cut_action.triggered.connect(self.cut_text)
+        if txt == '':
+            cut_action.setDisabled(True)
+
+        # COPY
+        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)
+
+        # PASTE
+        paste_action = QAction('%s\t%s' % (_("Paste"), _('Ctrl+V')), self)
+        self.menu.addAction(paste_action)
+        paste_action.triggered.connect(self.paste_text)
+
+        # DELETE
+        delete_action = QAction('%s\t%s' % (_("Delete"), _('Del')), self)
+        self.menu.addAction(delete_action)
+        delete_action.triggered.connect(self.delete_text)
+
+        self.menu.addSeparator()
+
+        # SELECT ALL
+        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)
+
+        self.menu.exec_(event.globalPos())
+
+    def cut_text(self):
+        tcursor = self.textCursor()
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = tcursor.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+        tcursor.deleteChar()
+
+    def copy_text(self):
+        tcursor = self.textCursor()
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = tcursor.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+    def paste_text(self):
+        tcursor = self.textCursor()
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = clipboard.text()
+        tcursor.insertText(txt)
+
+    def delete_text(self):
+        tcursor = self.textCursor()
+        tcursor.deleteChar()
+
+    def set_value(self, val):
+        self.setPlainText(val)
+
+    def get_value(self):
+        self.toPlainText()
+
+    def insertFromMimeData(self, data):
+        """
+        Reimplemented such that when SHIFT is pressed and doing click Paste in the contextual menu, the '\' symbol
+        is replaced with the '/' symbol. That's because of the difference in path separators in Windows and TCL
+        :param data:
+        :return:
+        """
+        modifier = QtWidgets.QApplication.keyboardModifiers()
+        if modifier == Qt.ShiftModifier:
+            text = data.text()
+            text = text.replace('\\', '/')
+            self.insertPlainText(text)
+        else:
+            self.insertPlainText(data.text())
+
+    def keyPressEvent(self, event):
+        """
+        Reimplemented so the CTRL + SHIFT + V shortcut key combo will paste the text but replacing '\' with '/'
+        :param event:
+        :return:
+        """
+        key = event.key()
+        modifier = QtWidgets.QApplication.keyboardModifiers()
+
+        if modifier & Qt.ControlModifier and modifier & Qt.ShiftModifier:
+            if key == QtCore.Qt.Key_V:
+                clipboard = QtWidgets.QApplication.clipboard()
+                clip_text = clipboard.text()
+                clip_text = clip_text.replace('\\', '/')
+                self.insertPlainText(clip_text)
+
+        if modifier & Qt.ControlModifier and key == Qt.Key_Slash:
+            self.comment()
+
+        tc = self.textCursor()
+        if (key == Qt.Key_Tab or key == Qt.Key_Enter or key == Qt.Key_Return) and self.completer.popup().isVisible():
+            self.completer.insertText.emit(self.completer.getSelected())
+            self.completer.setCompletionMode(QCompleter.PopupCompletion)
+            return
+        elif key == Qt.Key_BraceLeft:
+            tc.insertText('{}')
+            self.moveCursor(QtGui.QTextCursor.Left)
+        elif key == Qt.Key_BracketLeft:
+            tc.insertText('[]')
+            self.moveCursor(QtGui.QTextCursor.Left)
+        elif key == Qt.Key_ParenLeft:
+            tc.insertText('()')
+            self.moveCursor(QtGui.QTextCursor.Left)
+
+        elif key == Qt.Key_BraceRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == '}':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText('}')
+        elif key == Qt.Key_BracketRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == ']':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText(']')
+        elif key == Qt.Key_ParenRight:
+            tc.select(QtGui.QTextCursor.WordUnderCursor)
+            if tc.selectedText() == ')':
+                tc.movePosition(QTextCursor.Right)
+                self.setTextCursor(tc)
+            else:
+                tc.clearSelection()
+                self.textCursor().insertText(')')
+        else:
+            super(FCPlainTextAreaExtended, self).keyPressEvent(event)
+
+        if self.completer_enable:
+            tc.select(QTextCursor.WordUnderCursor)
+            cr = self.cursorRect()
+
+            if len(tc.selectedText()) > 0:
+                self.completer.setCompletionPrefix(tc.selectedText())
+                popup = self.completer.popup()
+                popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
+
+                cr.setWidth(self.completer.popup().sizeHintForColumn(0)
+                            + self.completer.popup().verticalScrollBar().sizeHint().width())
+                self.completer.complete(cr)
+            else:
+                self.completer.popup().hide()
+
+    def comment(self):
+        """
+        Got it from here:
+        https://stackoverflow.com/questions/49898820/how-to-get-text-next-to-cursor-in-qtextedit-in-pyqt4
+        :return:
+        """
+        pos = self.textCursor().position()
+        self.moveCursor(QtGui.QTextCursor.StartOfLine)
+        self.textCursor().block().text()
+        if self.textCursor().block().text().startswith(" "):
+            # skip the white space
+            self.moveCursor(QtGui.QTextCursor.NextWord)
+        self.moveCursor(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor)
+        character = self.textCursor().selectedText()
+        if character == "#":
+            # delete #
+            self.textCursor().deletePreviousChar()
+            # delete white space
+            self.moveCursor(QtGui.QTextCursor.NextWord, QtGui.QTextCursor.KeepAnchor)
+            self.textCursor().removeSelectedText()
+        else:
+            self.moveCursor(QtGui.QTextCursor.PreviousCharacter, QtGui.QTextCursor.KeepAnchor)
+            self.textCursor().insertText("# ")
+        cursor = QtGui.QTextCursor(self.textCursor())
+        cursor.setPosition(pos)
+        self.setTextCursor(cursor)
+
+
+class FCComboBox(QtWidgets.QComboBox):
+
+    def __init__(self, parent=None, callback=None, policy=True):
+        super(FCComboBox, self).__init__(parent)
+        self.setFocusPolicy(QtCore.Qt.StrongFocus)
+        self.view = self.view()
+        self.view.viewport().installEventFilter(self)
+        self.view.setContextMenuPolicy(Qt.CustomContextMenu)
+
+        self._set_last = False
+        self._obj_type = None
+
+        if policy is True:
+            sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Preferred)
+            self.setSizePolicy(sizePolicy)
+
+        # the callback() will be called on customcontextmenu event and will be be passed 2 parameters:
+        # pos = mouse right click click position
+        # self = is the combobox object itself
+        if callback:
+            self.view.customContextMenuRequested.connect(lambda pos: callback(pos, self))
+
+    def eventFilter(self, obj, event):
+        if event.type() == QtCore.QEvent.MouseButtonRelease:
+            if event.button() == Qt.RightButton:
+                return True
+        return False
+
+    def wheelEvent(self, *args, **kwargs):
+        pass
+
+    def get_value(self):
+        return str(self.currentText())
+
+    def set_value(self, val):
+        idx = self.findText(str(val))
+        if idx == -1:
+            self.setCurrentIndex(0)
+            return
+        self.setCurrentIndex(idx)
+
+    @property
+    def is_last(self):
+        return self._set_last
+
+    @is_last.setter
+    def is_last(self, val):
+        self._set_last = val
+        if self._set_last is True:
+            self.model().rowsInserted.connect(self.on_model_changed)
+        self.setCurrentIndex(1)
+
+    @property
+    def obj_type(self):
+        return self._obj_type
+
+    @obj_type.setter
+    def obj_type(self, val):
+        self._obj_type = val
+
+    def on_model_changed(self, parent, first, last):
+        if self.model().data(parent, QtCore.Qt.DisplayRole) == self.obj_type:
+            self.setCurrentIndex(first)
+
+
+class FCComboBox2(FCComboBox):
+    def __init__(self, parent=None, callback=None):
+        super(FCComboBox2, self).__init__(parent=parent, callback=callback)
+
+    def get_value(self):
+        return int(self.currentIndex())
+
+    def set_value(self, val):
+        self.setCurrentIndex(val)
+
+
+class FCInputDialog(QtWidgets.QInputDialog):
+    def __init__(self, parent=None, ok=False, val=None, title=None, text=None, min=None, max=None, decimals=None,
+                 init_val=None):
+        super(FCInputDialog, self).__init__(parent)
+
+        self.allow_empty = ok
+        self.empty_val = val
+
+        self.val = 0.0
+        self.ok = ''
+
+        self.init_value = init_val if init_val else 0.0
+
+        if title is None:
+            self.title = 'title'
+        else:
+            self.title = title
+        if text is None:
+            self.text = 'text'
+        else:
+            self.text = text
+        if min is None:
+            self.min = 0
+        else:
+            self.min = min
+        if max is None:
+            self.max = 0
+        else:
+            self.max = max
+        if decimals is None:
+            self.decimals = 6
+        else:
+            self.decimals = decimals
+
+    def get_value(self):
+        self.val, self.ok = self.getDouble(self, self.title, self.text, min=self.min,
+                                           max=self.max, decimals=self.decimals, value=self.init_value)
+        return [self.val, self.ok]
+
+    # "Transform", "Enter the Angle value:"
+    def set_value(self, val):
+        pass
+
+
+class FCInputDoubleSpinner(QtWidgets.QDialog):
+    def __init__(self, parent=None, title=None, text=None,
+                 min=0.0, max=100.0000, step=1, decimals=4, init_val=None):
+        super(FCInputDoubleSpinner, self).__init__(parent)
+
+        self.val = 0.0
+
+        self.init_value = init_val if init_val else 0.0
+
+        self.setWindowTitle(title) if title else self.setWindowTitle('title')
+        self.text = text if text else 'text'
+
+        self.min = min
+        self.max = max
+        self.step = step
+        self.decimals = decimals
+
+        self.lbl = FCLabel(self.text)
+
+        if title is None:
+            self.title = 'title'
+        else:
+            self.title = title
+        if text is None:
+            self.text = 'text'
+        else:
+            self.text = text
+
+        self.wdg = FCDoubleSpinner()
+        self.wdg.set_precision(self.decimals)
+        self.wdg.set_range(self.min, self.max)
+        self.wdg.set_step(self.step)
+        self.wdg.set_value(self.init_value)
+
+        QBtn = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
+
+        self.buttonBox = QtWidgets.QDialogButtonBox(QBtn)
+        self.buttonBox.accepted.connect(self.accept)
+        self.buttonBox.rejected.connect(self.reject)
+
+        self.layout = QtWidgets.QVBoxLayout()
+        self.layout.addWidget(self.lbl)
+        self.layout.addWidget(self.wdg)
+        self.layout.addWidget(self.buttonBox)
+
+        self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText(_("Ok"))
+        self.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).setText(_("Cancel"))
+
+        self.setLayout(self.layout)
+
+    def set_title(self, txt):
+        self.setWindowTitle(txt)
+
+    def set_text(self, txt):
+        self.lbl.set_value(txt)
+
+    def set_icon(self, icon):
+        self.setWindowIcon(icon)
+
+    def set_min(self, val):
+        self.wdg.setMinimum(val)
+
+    def set_max(self, val):
+        self.wdg.setMaximum(val)
+
+    def set_range(self, min, max):
+        self.wdg.set_range(min, max)
+
+    def set_step(self, val):
+        self.wdg.set_step(val)
+
+    def set_value(self, val):
+        self.wdg.set_value(val)
+
+    def get_value(self):
+        if self.exec_() == QtWidgets.QDialog.Accepted:
+            return self.wdg.get_value(), True
+        else:
+            return None, False
+
+
+class FCInputSpinner(QtWidgets.QDialog):
+    def __init__(self, parent=None, title=None, text=None, min=None, max=None, decimals=4, step=1, init_val=None):
+        super().__init__(parent)
+
+        self.val = 0.0
+        self.ok = ''
+
+        self.init_value = init_val if init_val else 0.0
+
+        self.setWindowTitle(title) if title else self.setWindowTitle('title')
+        self.text = text if text else 'text'
+
+        self.min = min if min else 0
+        self.max = max if max else 255
+        self.step = step if step else 1
+
+        self.lbl = FCLabel(self.text)
+
+        self.wdg = FCDoubleSpinner()
+        self.wdg.set_value(self.init_value)
+        self.wdg.set_range(self.min, self.max)
+        self.wdg.set_step(self.step)
+        self.wdg.set_precision(decimals)
+
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
+        self.wdg.setSizePolicy(sizePolicy)
+
+        QBtn = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
+
+        self.buttonBox = QtWidgets.QDialogButtonBox(QBtn)
+        self.buttonBox.accepted.connect(self.accept)
+        self.buttonBox.rejected.connect(self.reject)
+
+        self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText(_("Ok"))
+        self.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).setText(_("Cancel"))
+
+        self.layout = QtWidgets.QVBoxLayout()
+        self.layout.addWidget(self.lbl)
+        self.layout.addWidget(self.wdg)
+        self.layout.addWidget(self.buttonBox)
+        self.setLayout(self.layout)
+
+    def set_title(self, txt):
+        self.setWindowTitle(txt)
+
+    def set_text(self, txt):
+        self.lbl.set_value(txt)
+
+    def set_min(self, val):
+        self.wdg.spinner.setMinimum(val)
+
+    def set_max(self, val):
+        self.wdg.spinner.setMaximum(val)
+
+    def set_range(self, min, max):
+        self.wdg.spinner.set_range(min, max)
+
+    def set_step(self, val):
+        self.wdg.spinner.set_step(val)
+
+    def get_value(self):
+        if self.exec_() == QtWidgets.QDialog.Accepted:
+            return [self.wdg.get_value(), True]
+        else:
+            return [None, False]
+
+
+class FCInputDialogSlider(QtWidgets.QDialog):
+
+    def __init__(self, parent=None, title=None, text=None, min=None, max=None, step=1, init_val=None):
+        super().__init__(parent)
+
+        self.val = 0.0
+
+        self.init_value = init_val if init_val else 0.0
+
+        self.setWindowTitle(title) if title else self.setWindowTitle('title')
+        self.text = text if text else 'text'
+
+        self.min = min if min else 0
+        self.max = max if max else 255
+        self.step = step if step else 1
+
+        self.lbl = FCLabel(self.text)
+
+        self.wdg = FCSliderWithSpinner(min=self.min, max=self.max, step=self.step)
+        self.wdg.set_value(self.init_value)
+
+        QBtn = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
+
+        self.buttonBox = QtWidgets.QDialogButtonBox(QBtn)
+        self.buttonBox.accepted.connect(self.accept)
+        self.buttonBox.rejected.connect(self.reject)
+
+        self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText(_("Ok"))
+        self.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).setText(_("Cancel"))
+
+        self.layout = QtWidgets.QVBoxLayout()
+        self.layout.addWidget(self.lbl)
+        self.layout.addWidget(self.wdg)
+        self.layout.addWidget(self.buttonBox)
+        self.setLayout(self.layout)
+
+    def set_title(self, txt):
+        self.setWindowTitle(txt)
+
+    def set_text(self, txt):
+        self.lbl.set_value(txt)
+
+    def set_min(self, val):
+        self.wdg.spinner.setMinimum(val)
+
+    def set_max(self, val):
+        self.wdg.spinner.setMaximum(val)
+
+    def set_range(self, min, max):
+        self.wdg.spinner.set_range(min, max)
+
+    def set_step(self, val):
+        self.wdg.spinner.set_step(val)
+
+    def get_results(self):
+        if self.exec_() == QtWidgets.QDialog.Accepted:
+            return self.wdg.get_value(), True
+        else:
+            return None, False
+
+
+class FCInputDialogSpinnerButton(QtWidgets.QDialog):
+
+    def __init__(self, parent=None, title=None, text=None, min=None, max=None, step=1, decimals=4, init_val=None,
+                 button_text='', button_icon=None, callback=None):
+        super().__init__(parent)
+
+        self.val = 0.0
+
+        self.init_value = init_val if init_val else 0.0
+
+        self.setWindowTitle(title) if title else self.setWindowTitle('title')
+        self.text = text if text else 'text'
+
+        self.min = min if min else 0
+        self.max = max if max else 255
+        self.step = step if step else 1
+        self.decimals = decimals if decimals else 4
+
+        self.lbl = FCLabel(self.text)
+
+        self.wdg = FCButtonWithDoubleSpinner(min=self.min, max=self.max, step=self.step, decimals=decimals,
+                                             button_text=button_text, button_icon=button_icon, callback=callback)
+        self.wdg.set_value(self.init_value)
+
+        QBtn = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
+
+        self.buttonBox = QtWidgets.QDialogButtonBox(QBtn)
+        self.buttonBox.accepted.connect(self.accept)
+        self.buttonBox.rejected.connect(self.reject)
+
+        self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setText(_("Ok"))
+        self.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel).setText(_("Cancel"))
+
+        self.layout = QtWidgets.QVBoxLayout()
+        self.layout.addWidget(self.lbl)
+        self.layout.addWidget(self.wdg)
+        self.layout.addWidget(self.buttonBox)
+        self.setLayout(self.layout)
+
+    def set_title(self, txt):
+        self.setWindowTitle(txt)
+
+    def set_text(self, txt):
+        self.lbl.set_value(txt)
+
+    def set_icon(self, icon):
+        self.setWindowIcon(icon)
+
+    def set_min(self, val):
+        self.wdg.spinner.setMinimum(val)
+
+    def set_max(self, val):
+        self.wdg.spinner.setMaximum(val)
+
+    def set_range(self, min, max):
+        self.wdg.spinner.set_range(min, max)
+
+    def set_step(self, val):
+        self.wdg.spinner.set_step(val)
+
+    def set_value(self, val):
+        self.wdg.spinner.set_value(val)
+
+    def get_results(self):
+        if self.exec_() == QtWidgets.QDialog.Accepted:
+            return self.wdg.get_value(), True
+        else:
+            return None, False
+
+
+class FCButton(QtWidgets.QPushButton):
+    def __init__(self, text=None, checkable=None, click_callback=None, parent=None):
+        super(FCButton, self).__init__(text, parent)
+        if checkable is not None:
+            self.setCheckable(checkable)
+
+        if not click_callback is None:
+            self.clicked.connect(click_callback)
+
+    def get_value(self):
+        return self.isChecked()
+
+    def set_value(self, val):
+        self.setText(str(val))
+
+
+class FCLabel(QtWidgets.QLabel):
+    clicked = QtCore.pyqtSignal(bool)
+    right_clicked = QtCore.pyqtSignal(bool)
+    middle_clicked = QtCore.pyqtSignal(bool)
+
+    def __init__(self, parent=None):
+        super(FCLabel, self).__init__(parent)
+
+        # for the usage of this label as a clickable label, to know that current state
+        self.clicked_state = False
+        self.middle_clicked_state = False
+        self.right_clicked_state = False
+
+    def mousePressEvent(self, event):
+        if event.button() == Qt.LeftButton:
+            self.clicked_state = not self.clicked_state
+            self.clicked.emit(self.clicked_state)
+        elif event.button() == Qt.RightButton:
+            self.right_clicked_state = not self.right_clicked_state
+            self.right_clicked.emit(True)
+        elif event.button() == Qt.MiddleButton:
+            self.middle_clicked_state = not self.middle_clicked_state
+            self.middle_clicked.emit(True)
+
+    def get_value(self):
+        return self.text()
+
+    def set_value(self, val):
+        self.setText(str(val))
+
+
+class FCMenu(QtWidgets.QMenu):
+    def __init__(self):
+        super().__init__()
+        self.mouse_is_panning = False
+        self.popup_active = False
+
+    def popup(self, pos, action=None):
+        super().popup(pos)
+        self.mouse_is_panning = False
+        self.popup_active = True
+
+
+class FCTab(QtWidgets.QTabWidget):
+    def __init__(self, parent=None):
+        super(FCTab, self).__init__(parent)
+        self.setTabsClosable(True)
+        self.tabCloseRequested.connect(self.closeTab)
+
+    def deleteTab(self, currentIndex):
+        widget = self.widget(currentIndex)
+        if widget is not None:
+            widget.deleteLater()
+        self.removeTab(currentIndex)
+
+    def closeTab(self, currentIndex):
+        self.removeTab(currentIndex)
+
+    def protectTab(self, currentIndex):
+        self.tabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
+
+
+# class FCTabBar(QtWidgets.QTabBar):
+#     def tabSizeHint(self, index):
+#         size =QtWidgets.QTabBar.tabSizeHint(self, index)
+#         w = int(self.width()/self.count())
+#         return QtCore.QSize(w, size.height())
+
+
+class FCDetachableTab(QtWidgets.QTabWidget):
+    """
+    From here:
+    https://stackoverflow.com/questions/47267195/in-pyqt4-is-it-possible-to-detach-tabs-from-a-qtabwidget
+    """
+    tab_detached = QtCore.pyqtSignal(str)
+    tab_attached = QtCore.pyqtSignal(str)
+
+    def __init__(self, protect=None, protect_by_name=None, parent=None):
+        super().__init__(parent=parent)
+
+        self.tabBar = self.FCTabBar(self)
+        self.tabBar.onMoveTabSignal.connect(self.moveTab)
+        self.tabBar.onCloseTabSignal.connect(self.on_closetab_middle_button)
+
+        self.tabBar.detachedTabDropSignal.connect(self.detachedTabDrop)
+        self.set_detachable(val=True)
+
+        self.setTabBar(self.tabBar)
+
+        # Used to keep a reference to detached tabs since their QMainWindow
+        # does not have a parent
+        self.detachedTabs = {}
+
+        # a way to make sure that tabs can't be closed after they attach to the parent tab
+        self.protect_tab = True if protect is not None and protect is True else False
+
+        self.protect_by_name = protect_by_name if isinstance(protect_by_name, list) else None
+
+        # Close all detached tabs if the application is closed explicitly
+        QtWidgets.qApp.aboutToQuit.connect(self.closeDetachedTabs)  # @UndefinedVariable
+
+        # used by the property self.useOldIndex(param)
+        self.use_old_index = None
+        self.old_index = None
+
+        self.setTabsClosable(True)
+        self.tabCloseRequested.connect(self.closeTab)
+
+    def set_rmb_callback(self, callback):
+        """
+
+        :param callback: Function to call on right mouse click on tab
+        :type callback: func
+        :return: None
+        """
+
+        self.tabBar.right_click.connect(callback)
+
+    def set_detachable(self, val=True):
+        try:
+            self.tabBar.onDetachTabSignal.disconnect()
+        except TypeError:
+            pass
+
+        if val is True:
+            self.tabBar.onDetachTabSignal.connect(self.detachTab)
+            # the tab can be moved around
+            self.tabBar.can_be_dragged = True
+        else:
+            # the detached tab can't be moved
+            self.tabBar.can_be_dragged = False
+
+        return val
+
+    def setupContextMenu(self):
+        self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+
+    def addContextMenu(self, entry, call_function, icon=None, initial_checked=False):
+        action_name = str(entry)
+        action = QtWidgets.QAction(self)
+        action.setCheckable(True)
+        action.setText(action_name)
+        if icon:
+            assert isinstance(icon, QtGui.QIcon), \
+                "Expected the argument to be QtGui.QIcon. Instead it is %s" % type(icon)
+            action.setIcon(icon)
+        action.setChecked(initial_checked)
+        self.addAction(action)
+        action.triggered.connect(call_function)
+
+    def useOldIndex(self, param):
+        if param:
+            self.use_old_index = True
+        else:
+            self.use_old_index = False
+
+    def deleteTab(self, currentIndex):
+        widget = self.widget(currentIndex)
+        if widget is not None:
+            widget.deleteLater()
+        self.removeTab(currentIndex)
+
+    def closeTab(self, currentIndex):
+        """
+        Slot connected to the tabCloseRequested signal
+
+        :param currentIndex:
+        :return:
+        """
+
+        self.removeTab(currentIndex)
+
+    def on_closetab_middle_button(self, current_index):
+        """
+
+        :param current_index:
+        :return:
+        """
+
+        # if tab is protected don't delete it
+        if self.tabBar.tabButton(current_index, QtWidgets.QTabBar.RightSide) is not None:
+            self.removeTab(current_index)
+
+    def protectTab(self, currentIndex):
+        # self.FCTabBar().setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
+        self.tabBar.setTabButton(currentIndex, QtWidgets.QTabBar.RightSide, None)
+
+    def setMovable(self, movable):
+        """
+        The default movable functionality of QTabWidget must remain disabled
+        so as not to conflict with the added features
+
+        :param movable:
+        :return:
+        """
+        pass
+
+    @pyqtSlot(int, int)
+    def moveTab(self, fromIndex, toIndex):
+        """
+        Move a tab from one position (index) to another
+
+        :param fromIndex:   the original index location of the tab
+        :param toIndex:     the new index location of the tab
+        :return:
+        """
+
+        widget = self.widget(fromIndex)
+        icon = self.tabIcon(fromIndex)
+        text = self.tabText(fromIndex)
+
+        self.removeTab(fromIndex)
+        self.insertTab(toIndex, widget, icon, text)
+        self.setCurrentIndex(toIndex)
+
+    # @pyqtSlot(int, QtCore.QPoint)
+    def detachTab(self, index, point):
+        """
+        Detach the tab by removing it's contents and placing them in
+        a DetachedTab window
+
+        :param index:   the index location of the tab to be detached
+        :param point:   the screen position for creating the new DetachedTab window
+        :return:
+        """
+        self.old_index = index
+
+        # Get the tab content and add name FlatCAM to the tab so we know on which app is this tab linked
+        name = "FlatCAM " + self.tabText(index)
+        icon = self.tabIcon(index)
+        if icon.isNull():
+            icon = self.window().windowIcon()
+        contentWidget = self.widget(index)
+
+        try:
+            contentWidgetRect = contentWidget.frameGeometry()
+        except AttributeError:
+            return
+
+        # Create a new detached tab window
+        detachedTab = self.FCDetachedTab(name, contentWidget)
+        detachedTab.setWindowModality(QtCore.Qt.NonModal)
+        detachedTab.setWindowIcon(icon)
+        detachedTab.setGeometry(contentWidgetRect)
+        detachedTab.onCloseSignal.connect(self.attachTab)
+        detachedTab.onDropSignal.connect(self.tabBar.detachedTabDrop)
+        detachedTab.move(point)
+        detachedTab.show()
+
+        # Create a reference to maintain access to the detached tab
+        self.detachedTabs[name] = detachedTab
+
+        self.tab_detached.emit(name)
+
+    def attachTab(self, contentWidget, name, icon, insertAt=None):
+        """
+        Re-attach the tab by removing the content from the DetachedTab window,
+        closing it, and placing the content back into the DetachableTabWidget
+
+        :param contentWidget:   the content widget from the DetachedTab window
+        :param name:            the name of the detached tab
+        :param icon:            the window icon for the detached tab
+        :param insertAt:        insert the re-attached tab at the given index
+        :return:
+        """
+
+        old_name = name
+
+        # Make the content widget a child of this widget
+        contentWidget.setParent(self)
+
+        # make sure that we strip the 'FlatCAM' part of the detached name otherwise the tab name will be too long
+        name = name.partition(' ')[2]
+
+        # helps in restoring the tab to the same index that it was before was detached
+        insert_index = self.old_index if self.use_old_index is True else insertAt
+
+        # Create an image from the given icon (for comparison)
+        if not icon.isNull():
+            try:
+                tabIconPixmap = icon.pixmap(icon.availableSizes()[0])
+                tabIconImage = tabIconPixmap.toImage()
+            except IndexError:
+                tabIconImage = None
+        else:
+            tabIconImage = None
+
+        # Create an image of the main window icon (for comparison)
+        if not icon.isNull():
+            try:
+                windowIconPixmap = self.window().windowIcon().pixmap(icon.availableSizes()[0])
+                windowIconImage = windowIconPixmap.toImage()
+            except IndexError:
+                windowIconImage = None
+        else:
+            windowIconImage = None
+
+        # Determine if the given image and the main window icon are the same.
+        # If they are, then do not add the icon to the tab
+        if tabIconImage == windowIconImage:
+            if insert_index is None:
+                index = self.addTab(contentWidget, name)
+            else:
+                index = self.insertTab(insert_index, contentWidget, name)
+        else:
+            if insert_index is None:
+                index = self.addTab(contentWidget, icon, name)
+            else:
+                index = self.insertTab(insert_index, contentWidget, icon, name)
+
+        obj_name = contentWidget.objectName()
+        self.tab_attached.emit(obj_name)
+
+        # on reattaching the tab if protect is true then the closure button is not added
+        if self.protect_tab is True:
+            self.protectTab(index)
+
+        # on reattaching the tab disable the closure button for the tabs with the name in the self.protect_by_name list
+        if self.protect_by_name is not None:
+            for tab_name in self.protect_by_name:
+                for index in range(self.count()):
+                    if str(tab_name) == str(self.tabText(index)):
+                        self.protectTab(index)
+
+            # Make this tab the current tab
+            if index > -1:
+                self.setCurrentIndex(insert_index) if self.use_old_index else self.setCurrentIndex(index)
+
+        # Remove the reference
+        # Unix-like OS's crash with segmentation fault after this. FOr whatever reason, they loose reference
+        if sys.platform == 'win32':
+            try:
+                del self.detachedTabs[old_name]
+            except KeyError:
+                pass
+
+    def removeTabByName(self, name):
+        """
+        Remove the tab with the given name, even if it is detached
+
+        :param name: the name of the tab to be removed
+        :return:
+        """
+
+        # Remove the tab if it is attached
+        attached = False
+        for index in range(self.count()):
+            if str(name) == str(self.tabText(index)):
+                self.removeTab(index)
+                attached = True
+                break
+
+        # If the tab is not attached, close it's window and
+        # remove the reference to it
+        if not attached:
+            for key in self.detachedTabs:
+                if str(name) == str(key):
+                    self.detachedTabs[key].onCloseSignal.disconnect()
+                    self.detachedTabs[key].close()
+                    del self.detachedTabs[key]
+                    break
+
+    @QtCore.pyqtSlot(str, int, QtCore.QPoint)
+    def detachedTabDrop(self, name, index, dropPos):
+        """
+        Handle dropping of a detached tab inside the DetachableTabWidget
+
+        :param name:        the name of the detached tab
+        :param index:       the index of an existing tab (if the tab bar
+    #                       determined that the drop occurred on an
+    #                       existing tab)
+        :param dropPos:     the mouse cursor position when the drop occurred
+        :return:
+        """
+
+        # If the drop occurred on an existing tab, insert the detached
+        # tab at the existing tab's location
+        if index > -1:
+
+            # Create references to the detached tab's content and icon
+            contentWidget = self.detachedTabs[name].contentWidget
+            icon = self.detachedTabs[name].windowIcon()
+
+            # Disconnect the detached tab's onCloseSignal so that it
+            # does not try to re-attach automatically
+            self.detachedTabs[name].onCloseSignal.disconnect()
+
+            # Close the detached
+            self.detachedTabs[name].close()
+
+            # Re-attach the tab at the given index
+            self.attachTab(contentWidget, name, icon, index)
+
+        # If the drop did not occur on an existing tab, determine if the drop
+        # occurred in the tab bar area (the area to the side of the QTabBar)
+        else:
+
+            # Find the drop position relative to the DetachableTabWidget
+            tabDropPos = self.mapFromGlobal(dropPos)
+
+            # If the drop position is inside the DetachableTabWidget...
+            if self.rect().contains(tabDropPos):
+
+                # If the drop position is inside the tab bar area (the
+                # area to the side of the QTabBar) or there are not tabs
+                # currently attached...
+                if tabDropPos.y() < self.tabBar.height() or self.count() == 0:
+                    # Close the detached tab and allow it to re-attach
+                    # automatically
+                    self.detachedTabs[name].close()
+
+    def closeDetachedTabs(self):
+        """
+        Close all tabs that are currently detached.
+
+        :return:
+        """
+        listOfDetachedTabs = []
+
+        for key in self.detachedTabs:
+            listOfDetachedTabs.append(self.detachedTabs[key])
+
+        for detachedTab in listOfDetachedTabs:
+            detachedTab.close()
+
+    class FCDetachedTab(QtWidgets.QMainWindow):
+        """
+        When a tab is detached, the contents are placed into this QMainWindow.  The tab
+        can be re-attached by closing the dialog or by dragging the window into the tab bar
+        """
+
+        onCloseSignal = QtCore.pyqtSignal(QtWidgets.QWidget, str, QtGui.QIcon)
+        onDropSignal = QtCore.pyqtSignal(str, QtCore.QPoint)
+
+        def __init__(self, name, contentWidget):
+            QtWidgets.QMainWindow.__init__(self, None)
+
+            self.setObjectName(name)
+            self.setWindowTitle(name)
+
+            self.contentWidget = contentWidget
+            self.setCentralWidget(self.contentWidget)
+            self.contentWidget.show()
+
+            self.windowDropFilter = self.WindowDropFilter()
+            self.installEventFilter(self.windowDropFilter)
+            self.windowDropFilter.onDropSignal.connect(self.windowDropSlot)
+
+        @QtCore.pyqtSlot(QtCore.QPoint)
+        def windowDropSlot(self, dropPos):
+            """
+            Handle a window drop event
+
+            :param dropPos:     the mouse cursor position of the drop
+            :return:
+            """
+            self.onDropSignal.emit(self.objectName(), dropPos)
+
+        def closeEvent(self, event):
+            """
+            If the window is closed, emit the onCloseSignal and give the
+            content widget back to the DetachableTabWidget
+
+            :param event:    a close event
+            :return:
+            """
+            self.onCloseSignal.emit(self.contentWidget, self.objectName(), self.windowIcon())
+
+        class WindowDropFilter(QtCore.QObject):
+            """
+            An event filter class to detect a QMainWindow drop event
+            """
+
+            onDropSignal = QtCore.pyqtSignal(QtCore.QPoint)
+
+            def __init__(self):
+                QtCore.QObject.__init__(self)
+                self.lastEvent = None
+
+            def eventFilter(self, obj, event):
+                """
+                Detect a QMainWindow drop event by looking for a NonClientAreaMouseMove (173)
+                event that immediately follows a Move event
+
+                :param obj:     the object that generated the event
+                :param event:   the current event
+                :return:
+                """
+
+                # If a NonClientAreaMouseMove (173) event immediately follows a Move event...
+                if self.lastEvent == QtCore.QEvent.Move and event.type() == 173:
+
+                    # Determine the position of the mouse cursor and emit it with the
+                    # onDropSignal
+                    mouseCursor = QtGui.QCursor()
+                    dropPos = mouseCursor.pos()
+                    self.onDropSignal.emit(dropPos)
+                    self.lastEvent = event.type()
+                    return True
+
+                else:
+                    self.lastEvent = event.type()
+                    return False
+
+    class FCTabBar(QtWidgets.QTabBar):
+        onDetachTabSignal = QtCore.pyqtSignal(int, QtCore.QPoint)
+        onMoveTabSignal = QtCore.pyqtSignal(int, int)
+        detachedTabDropSignal = QtCore.pyqtSignal(str, int, QtCore.QPoint)
+        onCloseTabSignal = QtCore.pyqtSignal(int)
+        right_click = QtCore.pyqtSignal(int)
+
+        def __init__(self, parent=None):
+            QtWidgets.QTabBar.__init__(self, parent)
+
+            self.setAcceptDrops(True)
+            self.setElideMode(QtCore.Qt.ElideRight)
+            self.setSelectionBehaviorOnRemove(QtWidgets.QTabBar.SelectLeftTab)
+
+            self.prev_index = -1
+
+            self.dragStartPos = QtCore.QPoint()
+            self.dragDropedPos = QtCore.QPoint()
+            self.mouseCursor = QtGui.QCursor()
+            self.dragInitiated = False
+
+            # set this to False and the tab will no longer be displayed as detached
+            self.can_be_dragged = True
+
+        def mouseDoubleClickEvent(self, event):
+            """
+            Send the onDetachTabSignal when a tab is double clicked
+
+            :param event:   a mouse double click event
+            :return:
+            """
+
+            event.accept()
+            self.onDetachTabSignal.emit(self.tabAt(event.pos()), self.mouseCursor.pos())
+
+        def mousePressEvent(self, event):
+            """
+            Set the starting position for a drag event when the left mouse button is pressed.
+            Start detection of a right mouse click.
+
+            :param event:   a mouse press event
+            :return:
+            """
+            if event.button() == QtCore.Qt.LeftButton:
+                self.dragStartPos = event.pos()
+            elif event.button() == QtCore.Qt.RightButton:
+                self.prev_index = self.tabAt(event.pos())
+
+            self.dragDropedPos.setX(0)
+            self.dragDropedPos.setY(0)
+            self.dragInitiated = False
+
+            QtWidgets.QTabBar.mousePressEvent(self, event)
+
+        def mouseReleaseEvent(self, event):
+            """
+            Finish the detection of the right mouse click on the tab
+
+
+            :param event:   a mouse press event
+            :return:
+            """
+            if event.button() == QtCore.Qt.RightButton and self.prev_index == self.tabAt(event.pos()):
+                self.right_click.emit(self.prev_index)
+
+            if event.button() == QtCore.Qt.MiddleButton:
+                self.onCloseTabSignal.emit(int(self.tabAt(event.pos())))
+
+            self.prev_index = -1
+
+            QtWidgets.QTabBar.mouseReleaseEvent(self, event)
+
+        def mouseMoveEvent(self, event):
+            """
+            Determine if the current movement is a drag.  If it is, convert it into a QDrag.  If the
+            drag ends inside the tab bar, emit an onMoveTabSignal.  If the drag ends outside the tab
+            bar, emit an onDetachTabSignal.
+
+            :param event:   a mouse move event
+            :return:
+            """
+            # Determine if the current movement is detected as a drag
+            if not self.dragStartPos.isNull() and \
+                    ((event.pos() - self.dragStartPos).manhattanLength() < QtWidgets.QApplication.startDragDistance()):
+                self.dragInitiated = True
+
+            # If the current movement is a drag initiated by the left button
+            if (event.buttons() & QtCore.Qt.LeftButton) and self.dragInitiated and self.can_be_dragged:
+
+                # Stop the move event
+                finishMoveEvent = QtGui.QMouseEvent(
+                    QtCore.QEvent.MouseMove, event.pos(), QtCore.Qt.NoButton, QtCore.Qt.NoButton, QtCore.Qt.NoModifier
+                )
+                QtWidgets.QTabBar.mouseMoveEvent(self, finishMoveEvent)
+
+                # Convert the move event into a drag
+                drag = QtGui.QDrag(self)
+                mimeData = QtCore.QMimeData()
+                # mimeData.setData('action', 'application/tab-detach')
+                drag.setMimeData(mimeData)
+                # screen = QScreen(self.parentWidget().currentWidget().winId())
+                # Create the appearance of dragging the tab content
+                try:
+                    pixmap = self.parent().widget(self.tabAt(self.dragStartPos)).grab()
+                except Exception as e:
+                    log.debug("GUIElements.FCDetachable. FCTabBar.mouseMoveEvent() --> %s" % str(e))
+                    return
+
+                targetPixmap = QtGui.QPixmap(pixmap.size())
+                targetPixmap.fill(QtCore.Qt.transparent)
+                painter = QtGui.QPainter(targetPixmap)
+                painter.setOpacity(0.85)
+                painter.drawPixmap(0, 0, pixmap)
+                painter.end()
+                drag.setPixmap(targetPixmap)
+
+                # Initiate the drag
+                dropAction = drag.exec_(QtCore.Qt.MoveAction | QtCore.Qt.CopyAction)
+
+                # For Linux:  Here, drag.exec_() will not return MoveAction on Linux.  So it
+                #             must be set manually
+                if self.dragDropedPos.x() != 0 and self.dragDropedPos.y() != 0:
+                    dropAction = QtCore.Qt.MoveAction
+
+                # If the drag completed outside of the tab bar, detach the tab and move
+                # the content to the current cursor position
+                if dropAction == QtCore.Qt.IgnoreAction:
+                    event.accept()
+                    self.onDetachTabSignal.emit(self.tabAt(self.dragStartPos), self.mouseCursor.pos())
+
+                # Else if the drag completed inside the tab bar, move the selected tab to the new position
+                elif dropAction == QtCore.Qt.MoveAction:
+                    if not self.dragDropedPos.isNull():
+                        event.accept()
+                        self.onMoveTabSignal.emit(self.tabAt(self.dragStartPos), self.tabAt(self.dragDropedPos))
+            else:
+                QtWidgets.QTabBar.mouseMoveEvent(self, event)
+
+        def dragEnterEvent(self, event):
+            """
+            Determine if the drag has entered a tab position from another tab position
+
+            :param event:   a drag enter event
+            :return:
+            """
+            mimeData = event.mimeData()
+            # formats = mcd imeData.formats()
+
+            # if formats.contains('action') and mimeData.data('action') == 'application/tab-detach':
+            # event.acceptProposedAction()
+
+            QtWidgets.QTabBar.dragMoveEvent(self, event)
+
+        def dropEvent(self, event):
+            """
+            Get the position of the end of the drag
+
+            :param event:    a drop event
+            :return:
+            """
+            self.dragDropedPos = event.pos()
+            QtWidgets.QTabBar.dropEvent(self, event)
+
+        def detachedTabDrop(self, name, dropPos):
+            """
+            Determine if the detached tab drop event occurred on an existing tab,
+            then send the event to the DetachableTabWidget
+
+            :param name:
+            :param dropPos:
+            :return:
+            """
+            tabDropPos = self.mapFromGlobal(dropPos)
+
+            index = self.tabAt(tabDropPos)
+
+            self.detachedTabDropSignal.emit(name, index, dropPos)
+
+
+class FCDetachableTab2(FCDetachableTab):
+    tab_closed_signal = QtCore.pyqtSignal(object, int)
+
+    def __init__(self, protect=None, protect_by_name=None, parent=None):
+        super(FCDetachableTab2, self).__init__(protect=protect, protect_by_name=protect_by_name, parent=parent)
+
+        try:
+            self.tabBar.onCloseTabSignal.disconnect()
+        except TypeError:
+            pass
+
+        self.tabBar.onCloseTabSignal.connect(self.on_closetab_middle_button)
+
+    def on_closetab_middle_button(self, current_index):
+        """
+
+        :param current_index:
+        :return:
+        """
+
+        # if tab is protected don't delete it
+        if self.tabBar.tabButton(current_index, QtWidgets.QTabBar.RightSide) is not None:
+            self.closeTab(current_index)
+
+    def closeTab(self, currentIndex):
+        """
+        Slot connected to the tabCloseRequested signal
+
+        :param currentIndex:
+        :return:
+        """
+        # idx = self.currentIndex()
+        tab_name = self.widget(currentIndex).objectName()
+        self.tab_closed_signal.emit(tab_name, currentIndex)
+
+        self.removeTab(currentIndex)
+
+
+class VerticalScrollArea(QtWidgets.QScrollArea):
+    """
+    This widget extends QtGui.QScrollArea to make a vertical-only
+    scroll area that also expands horizontally to accommodate
+    its contents.
+    """
+
+    def __init__(self, parent=None):
+        QtWidgets.QScrollArea.__init__(self, parent=parent)
+        self.setWidgetResizable(True)
+        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+
+    def eventFilter(self, source, event):
+        """
+        The event filter gets automatically installed when setWidget()
+        is called.
+
+        :param source:
+        :param event:
+        :return:
+        """
+        if event.type() == QtCore.QEvent.Resize and source == self.widget():
+            # log.debug("VerticalScrollArea: Widget resized:")
+            # log.debug(" minimumSizeHint().width() = %d" % self.widget().minimumSizeHint().width())
+            # log.debug(" verticalScrollBar().width() = %d" % self.verticalScrollBar().width())
+
+            self.setMinimumWidth(self.widget().sizeHint().width() +
+                                 self.verticalScrollBar().sizeHint().width())
+
+            # if self.verticalScrollBar().isVisible():
+            #     log.debug(" Scroll bar visible")
+            #     self.setMinimumWidth(self.widget().minimumSizeHint().width() +
+            #                          self.verticalScrollBar().width())
+            # else:
+            #     log.debug(" Scroll bar hidden")
+            #     self.setMinimumWidth(self.widget().minimumSizeHint().width())
+        return QtWidgets.QWidget.eventFilter(self, source, event)
+
+
+class OptionalInputSection:
+
+    def __init__(self, cb, optinputs, logic=True):
+        """
+        Associates the a checkbox with a set of inputs.
+
+        :param cb: Checkbox that enables the optional inputs.
+        :param optinputs: List of widgets that are optional.
+        :param logic: When True the logic is normal, when False the logic is in reverse
+        It means that for logic=True, when the checkbox is checked the widgets are Enabled, and
+        for logic=False, when the checkbox is checked the widgets are Disabled
+        :return:
+        """
+        assert isinstance(cb, FCCheckBox), \
+            "Expected an FCCheckBox, got %s" % type(cb)
+
+        self.cb = cb
+        self.optinputs = optinputs
+        self.logic = logic
+
+        self.on_cb_change()
+        self.cb.stateChanged.connect(self.on_cb_change)
+
+    def on_cb_change(self):
+
+        if self.cb.checkState():
+            for widget in self.optinputs:
+                if self.logic is True:
+                    widget.setEnabled(True)
+                else:
+                    widget.setEnabled(False)
+        else:
+            for widget in self.optinputs:
+                if self.logic is True:
+                    widget.setEnabled(False)
+                else:
+                    widget.setEnabled(True)
+
+
+class OptionalHideInputSection:
+
+    def __init__(self, cb, optinputs, logic=True):
+        """
+        Associates the a checkbox with a set of inputs.
+
+        :param cb:          Checkbox that enables the optional inputs.
+        :type cb:           QtWidgets.QCheckBox
+        :param optinputs:   List of widgets that are optional.
+        :type optinputs:    list
+        :param logic:       When True the logic is normal, when False the logic is in reverse
+                            It means that for logic=True, when the checkbox is checked the widgets are Enabled, and
+                            for logic=False, when the checkbox is checked the widgets are Disabled
+        :type logic:        bool
+        :return:
+        """
+        assert isinstance(cb, FCCheckBox), \
+            "Expected an FCCheckBox, got %s" % type(cb)
+
+        self.cb = cb
+        self.optinputs = optinputs
+        self.logic = logic
+
+        self.on_cb_change()
+        self.cb.stateChanged.connect(self.on_cb_change)
+
+    def on_cb_change(self):
+
+        if self.cb.checkState():
+            for widget in self.optinputs:
+                if self.logic is True:
+                    widget.show()
+                else:
+                    widget.hide()
+        else:
+            for widget in self.optinputs:
+                if self.logic is True:
+                    widget.hide()
+                else:
+                    widget.show()
+
+
+class FCTable(QtWidgets.QTableWidget):
+    drag_drop_sig = QtCore.pyqtSignal(object, int)
+    lost_focus = QtCore.pyqtSignal()
+
+    def __init__(self, drag_drop=False, protected_rows=None, parent=None):
+        super(FCTable, self).__init__(parent)
+
+        palette = QtGui.QPalette()
+        palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight,
+                         palette.color(QtGui.QPalette.Active, QtGui.QPalette.Highlight))
+
+        # make inactive rows text some color as active; may be useful in the future
+        palette.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.HighlightedText,
+                         palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
+        self.setPalette(palette)
+
+        if drag_drop:
+            self.setDragEnabled(True)
+            self.setAcceptDrops(True)
+            self.viewport().setAcceptDrops(True)
+            self.setDragDropOverwriteMode(False)
+            self.setDropIndicatorShown(True)
+
+            self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+            self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+            self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
+
+        self.rows_not_for_drag_and_drop = []
+        if protected_rows:
+            try:
+                for r in protected_rows:
+                    self.rows_not_for_drag_and_drop.append(r)
+            except TypeError:
+                self.rows_not_for_drag_and_drop = [protected_rows]
+
+        self.rows_to_move = []
+        self.rows_dragged = None
+
+    def sizeHint(self):
+        default_hint_size = super(FCTable, self).sizeHint()
+        return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
+
+    def getHeight(self):
+        height = self.horizontalHeader().height()
+        for i in range(self.rowCount()):
+            height += self.rowHeight(i)
+        return height
+
+    def getWidth(self):
+        width = self.verticalHeader().width()
+        for i in range(self.columnCount()):
+            width += self.columnWidth(i)
+        return width
+
+    # color is in format QtGui.Qcolor(r, g, b, alpha) with or without alpha
+    def setColortoRow(self, rowIndex, color):
+        for j in range(self.columnCount()):
+            self.item(rowIndex, j).setBackground(color)
+
+    # if user is clicking an blank area inside the QTableWidget it will deselect currently selected rows
+    def mousePressEvent(self, event):
+        clicked_item = self.itemAt(event.pos())
+        if not clicked_item:
+            self.clearSelection()
+            self.clearFocus()
+        else:
+            self.rows_dragged = [it.row() for it in self.selectedItems()]
+            QtWidgets.QTableWidget.mousePressEvent(self, event)
+
+    def focusOutEvent(self, event):
+        self.lost_focus.emit()
+        super().focusOutEvent(event)
+
+    def setupContextMenu(self):
+        self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+
+    def addContextMenu(self, entry, call_function, icon=None):
+        action_name = str(entry)
+        action = QtWidgets.QAction(self)
+        action.setText(action_name)
+        if icon:
+            assert isinstance(icon, QtGui.QIcon), \
+                "Expected the argument to be QtGui.QIcon. Instead it is %s" % type(icon)
+            action.setIcon(icon)
+        self.addAction(action)
+        action.triggered.connect(call_function)
+
+    # def dropEvent(self, event: QtGui.QDropEvent):
+    #     if not event.isAccepted() and event.source() == self:
+    #         drop_row = self.drop_on(event)
+    #
+    #         rows = sorted(set(item.row() for item in self.selectedItems()))
+    #         # rows_to_move = [
+    #         #     [QtWidgets.QTableWidgetItem(self.item(row_index, column_index))
+    #         #      for column_index in range(self.columnCount())] for row_index in rows
+    #         # ]
+    #         self.rows_to_move[:] = []
+    #         for row_index in rows:
+    #             row_items = []
+    #             for column_index in range(self.columnCount()):
+    #                 r_item = self.item(row_index, column_index)
+    #                 w_item = self.cellWidget(row_index, column_index)
+    #
+    #                 if r_item is not None:
+    #                     row_items.append(QtWidgets.QTableWidgetItem(r_item))
+    #                 elif w_item is not None:
+    #                     row_items.append(w_item)
+    #
+    #             self.rows_to_move.append(row_items)
+    #
+    #         for row_index in reversed(rows):
+    #             self.removeRow(row_index)
+    #             if row_index < drop_row:
+    #                 drop_row -= 1
+    #
+    #         for row_index, data in enumerate(self.rows_to_move):
+    #             row_index += drop_row
+    #             self.insertRow(row_index)
+    #
+    #             for column_index, column_data in enumerate(data):
+    #                 if isinstance(column_data, QtWidgets.QTableWidgetItem):
+    #                     self.setItem(row_index, column_index, column_data)
+    #                 else:
+    #                     self.setCellWidget(row_index, column_index, column_data)
+    #
+    #         event.accept()
+    #         for row_index in range(len(self.rows_to_move)):
+    #             self.item(drop_row + row_index, 0).setSelected(True)
+    #             self.item(drop_row + row_index, 1).setSelected(True)
+    #
+    #     super().dropEvent(event)
+    #
+    # def drop_on(self, event):
+    #     ret_val = False
+    #     index = self.indexAt(event.pos())
+    #     if not index.isValid():
+    #         return self.rowCount()
+    #
+    #     ret_val = index.row() + 1 if self.is_below(event.pos(), index) else index.row()
+    #
+    #     return ret_val
+    #
+    # def is_below(self, pos, index):
+    #     rect = self.visualRect(index)
+    #     margin = 2
+    #     if pos.y() - rect.top() < margin:
+    #         return False
+    #     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()
+
+    def dragEnterEvent(self, e: QtGui.QDragEnterEvent) -> None:
+        if e.source() == self:
+            self.blockSignals(True)
+            e.accept()
+        else:
+            e.ignore()
+
+    # def dropEvent(self, event):
+    #     """
+    #     From here: https://stackoverflow.com/questions/26227885/drag-and-drop-rows-within-qtablewidget
+    #     :param event:
+    #     :return:
+    #     """
+    #     if event.source() == self:
+    #         event.acceptProposedAction()
+    #
+    #         # create a set of the selected rows that are dragged to another position
+    #         rows = set([mi.row() for mi in self.selectedIndexes()])
+    #         # if one of the selected rows for drag and drop is within the protected list, return
+    #         for r in rows:
+    #             if r in self.rows_not_for_drag_and_drop:
+    #                 return
+    #
+    #         drop_index = self.indexAt(event.pos())
+    #         # row where we drop the selected rows
+    #         targetRow = drop_index.row()
+    #
+    #         # drop_indicator = self.dropIndicatorPosition()
+    #         # if targetRow != -1:
+    #         #     if drop_indicator == QtWidgets.QAbstractItemView.AboveItem:
+    #         #         print("above")
+    #         #     elif drop_indicator == QtWidgets.QAbstractItemView.BelowItem:
+    #         #         print("below")
+    #         #     elif drop_indicator == QtWidgets.QAbstractItemView.OnItem:
+    #         #         print("on")
+    #         #     elif drop_indicator == QtWidgets.QAbstractItemView.OnViewport:
+    #         #         print("on viewport")
+    #
+    #         # if we drop on one row from the already dragged rows
+    #         rows.discard(targetRow)
+    #         rows = sorted(rows)
+    #         if not rows:
+    #             return
+    #         if targetRow == -1:
+    #             targetRow = self.rowCount()
+    #
+    #         # insert empty rows at the index of the targetRow
+    #         for _ in range(len(rows)):
+    #             self.insertRow(targetRow)
+    #
+    #         rowMapping = {}  # Src row to target row.
+    #         for idx, row in enumerate(rows):
+    #             if row < targetRow:
+    #                 rowMapping[row] = targetRow + idx
+    #             else:
+    #                 rowMapping[row + len(rows)] = targetRow + idx
+    #
+    #         colCount = self.columnCount()
+    #         for srcRow, tgtRow in sorted(rowMapping.items()):
+    #             for col in range(0, colCount):
+    #                 new_item = self.item(srcRow, col)
+    #                 if new_item is None:
+    #                     new_item = self.cellWidget(srcRow, col)
+    #
+    #                 if isinstance(new_item, QtWidgets.QTableWidgetItem):
+    #                     new_item = self.takeItem(srcRow, col)
+    #                     self.setItem(tgtRow, col, new_item)
+    #                 else:
+    #                     self.setCellWidget(tgtRow, col, new_item)
+    #
+    #         for row in reversed(sorted(rowMapping.keys())):
+    #             self.removeRow(row)
+    #
+    #         self.blockSignals(False)
+    #         self.drag_drop_sig.emit(int(self.row_dragged), int(targetRow))
+    #     else:
+    #         event.ignore()
+
+    def dropEvent(self, event: QtGui.QDropEvent):
+        if not event.isAccepted() and event.source() == self:
+            drop_row = self.drop_on(event)
+
+            rows = sorted(set(item.row() for item in self.selectedItems()))
+
+            rows_to_move = []
+            for row_index in rows:
+                temp_lst = []
+                for column_index in range(self.columnCount()):
+                    col_data = self.item(row_index, column_index)
+
+                    if isinstance(col_data, QtWidgets.QTableWidgetItem):
+                        table_item = QtWidgets.QTableWidgetItem(col_data)
+                    else:
+                        old_item = self.cellWidget(row_index, column_index)
+                        if isinstance(old_item, QtWidgets.QComboBox):
+                            table_item = FCComboBox()
+                            items = [old_item.itemText(i) for i in range(old_item.count())]
+                            table_item.addItems(items)
+                            table_item.setCurrentIndex(old_item.currentIndex())
+                        elif isinstance(old_item, QtWidgets.QCheckBox):
+                            table_item = FCCheckBox()
+                            table_item.setChecked(old_item.isChecked())
+                            table_item.setText(old_item.text())
+                        else:
+                            table_item = None
+
+                    temp_lst.append(table_item)
+                rows_to_move.append(temp_lst)
+
+            for row_index in reversed(rows):
+                self.removeRow(row_index)
+                if row_index < drop_row:
+                    drop_row -= 1
+
+            for row_index, data in enumerate(rows_to_move):
+                row_index += drop_row
+                self.insertRow(row_index)
+                for column_index, column_data in enumerate(data):
+                    if column_data is None:
+                        continue
+
+                    if isinstance(column_data, QtWidgets.QTableWidgetItem):
+                        self.setItem(row_index, column_index, column_data)
+                    else:
+                        self.setCellWidget(row_index, column_index, column_data)
+            event.accept()
+            for row_index in range(len(rows_to_move)):
+                self.item(drop_row + row_index, 0).setSelected(True)
+                self.item(drop_row + row_index, 1).setSelected(True)
+
+            self.blockSignals(False)
+            self.drag_drop_sig.emit(self.rows_dragged, int(drop_row))
+
+        self.blockSignals(False)
+        self.resizeRowsToContents()
+        super().dropEvent(event)
+
+    def drop_on(self, event):
+        index = self.indexAt(event.pos())
+        if not index.isValid():
+            return self.rowCount()
+
+        return index.row() + 1 if self.is_below(event.pos(), index) else index.row()
+
+    def is_below(self, pos, index):
+        rect = self.visualRect(index)
+        margin = 2
+        if pos.y() - rect.top() < margin:
+            return False
+        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()
+
+
+class SpinBoxDelegate(QtWidgets.QItemDelegate):
+
+    def __init__(self, units):
+        super(SpinBoxDelegate, self).__init__()
+        self.units = units
+        self.current_value = None
+
+    def createEditor(self, parent, option, index):
+        editor = QtWidgets.QDoubleSpinBox(parent)
+        editor.setMinimum(-999.9999)
+        editor.setMaximum(999.9999)
+
+        if self.units == 'MM':
+            editor.setDecimals(2)
+        else:
+            editor.setDecimals(3)
+
+        return editor
+
+    def setEditorData(self, spinBox, index):
+        try:
+            value = float(index.model().data(index, Qt.EditRole))
+        except ValueError:
+            value = self.current_value
+            # return
+
+        spinBox.setValue(value)
+
+    def setModelData(self, spinBox, model, index):
+        spinBox.interpretText()
+        value = spinBox.value()
+        self.current_value = value
+
+        model.setData(index, value, Qt.EditRole)
+
+    def updateEditorGeometry(self, editor, option, index):
+        editor.setGeometry(option.rect)
+
+    @staticmethod
+    def setDecimals(spinbox, digits):
+        spinbox.setDecimals(digits)
+
+
+class Dialog_box(QtWidgets.QWidget):
+    def __init__(self, title=None, label=None, icon=None, initial_text=None):
+        """
+
+        :param title: string with the window title
+        :param label: string with the message inside the dialog box
+        """
+        super(Dialog_box, self).__init__()
+        if initial_text is None:
+            self.location = str((0, 0))
+        else:
+            self.location = initial_text
+
+        self.ok = False
+
+        self.dialog_box = QtWidgets.QInputDialog()
+        self.dialog_box.setMinimumWidth(290)
+        self.setWindowIcon(icon)
+
+        self.location, self.ok = self.dialog_box.getText(self, title, label,
+                                                         text=str(self.location).replace('(', '').replace(')', ''))
+        self.readyToEdit = True
+
+    def mousePressEvent(self, e, parent=None):
+        super(Dialog_box, self).mousePressEvent(e)  # required to deselect on 2e click
+        if self.readyToEdit:
+            self.lineEdit().selectAll()
+            self.readyToEdit = False
+
+    def focusOutEvent(self, e):
+        # don't focus out if the user requests an popup menu
+        if e.reason() != QtCore.Qt.PopupFocusReason:
+            super(Dialog_box, self).focusOutEvent(e)  # required to remove cursor on focusOut
+            self.lineEdit().deselect()
+            self.readyToEdit = True
+
+
+class DialogBoxRadio(QtWidgets.QDialog):
+    def __init__(self, title=None, label=None, icon=None, initial_text=None, reference='abs', parent=None):
+        """
+
+        :param title: string with the window title
+        :param label: string with the message inside the dialog box
+        """
+        super(DialogBoxRadio, self).__init__(parent=parent)
+        if initial_text is None:
+            self.location = str((0, 0))
+        else:
+            self.location = initial_text
+
+        self.ok = False
+
+        self.setWindowIcon(icon)
+        self.setWindowTitle(str(title))
+
+        self.form = QtWidgets.QFormLayout(self)
+
+        self.ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
+        self.ref_label.setToolTip(
+            _("The reference can be:\n"
+              "- Absolute -> the reference point is point (0,0)\n"
+              "- Relative -> the reference point is the mouse position before Jump")
+        )
+        self.ref_radio = RadioSet([
+            {"label": _("Abs"), "value": "abs"},
+            {"label": _("Relative"), "value": "rel"}
+        ], orientation='horizontal', stretch=False)
+        self.ref_radio.set_value(reference)
+        self.form.addRow(self.ref_label, self.ref_radio)
+
+        self.form.addRow(QtWidgets.QLabel(''))
+
+        self.wdg_label = QtWidgets.QLabel('<b>%s</b>' % str(label))
+        self.form.addRow(self.wdg_label)
+
+        self.loc_label = QtWidgets.QLabel('%s:' % _("Location"))
+        self.loc_label.setToolTip(
+            _("The Location value is a tuple (x,y).\n"
+              "If the reference is Absolute then the Jump will be at the position (x,y).\n"
+              "If the reference is Relative then the Jump will be at the (x,y) distance\n"
+              "from the current mouse location point.")
+        )
+        self.lineEdit = EvalEntry(parent=self)
+        self.lineEdit.setText(str(self.location).replace('(', '').replace(')', ''))
+        self.lineEdit.selectAll()
+        self.lineEdit.setFocus()
+        self.form.addRow(self.loc_label, self.lineEdit)
+
+        self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel,
+                                                     orientation=Qt.Horizontal, parent=self)
+        self.form.addRow(self.button_box)
+
+        self.button_box.button(QtWidgets.QDialogButtonBox.Ok).setText(_("Ok"))
+        self.button_box.button(QtWidgets.QDialogButtonBox.Cancel).setText(_("Cancel"))
+
+        self.button_box.accepted.connect(self.accept)
+        self.button_box.rejected.connect(self.reject)
+
+        self.readyToEdit = True
+
+        if self.exec_() == QtWidgets.QDialog.Accepted:
+            self.ok = True
+            self.location = self.lineEdit.text()
+            self.reference = self.ref_radio.get_value()
+        else:
+            self.ok = False
+
+
+class _BrowserTextEdit(QTextEdit):
+
+    def __init__(self, version, app=None):
+        QTextEdit.__init__(self)
+        self.menu = None
+        self.version = version
+        self.app = app
+
+    def contextMenuEvent(self, event):
+        # self.menu = self.createStandardContextMenu(event.pos())
+        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)
+
+        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)
+
+        if self.app:
+            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))
+
+        clear_action = QAction('%s\t%s' % (_("Clear All"), _('Del')), self)
+        # clear_action.setShortcut(QKeySequence(Qt.Key_Delete))
+        self.menu.addAction(clear_action)
+        clear_action.triggered.connect(self.clear)
+
+        # if self.app:
+        #     close_action = QAction(_("Close"), self)
+        #     self.menu.addAction(close_action)
+        #     close_action.triggered.connect(lambda: self.app.ui.shell_dock.hide())
+
+        self.menu.exec_(event.globalPos())
+
+    def keyPressEvent(self, event) -> None:
+        modifiers = QtWidgets.QApplication.keyboardModifiers()
+        key = event.key()
+
+        if modifiers == QtCore.Qt.ControlModifier:
+            # Select All
+            if key == QtCore.Qt.Key_A:
+                self.selectAll()
+            # Copy Text
+            elif key == QtCore.Qt.Key_C:
+                self.copy_text()
+            # Save Log
+            elif key == QtCore.Qt.Key_S:
+                if self.app:
+                    self.save_log(app=self.app)
+
+        elif modifiers == QtCore.Qt.NoModifier:
+            # Clear all
+            if key == QtCore.Qt.Key_Delete:
+                self.clear()
+            # Shell toggle
+            if key == QtCore.Qt.Key_S:
+                self.app.ui.toggle_shell_ui()
+
+    def copy_text(self):
+        tcursor = self.textCursor()
+        clipboard = QtWidgets.QApplication.clipboard()
+
+        txt = tcursor.selectedText()
+        clipboard.clear()
+        clipboard.setText(txt)
+
+    def clear(self):
+        QTextEdit.clear(self)
+
+        text = "!FlatCAM %s? - %s" % (self.version, _("Type >help< to get started"))
+        text = html.escape(text)
+        # hack so I can make text bold because the escape method will replace the '<' and '>' signs with html code
+        text = text.replace('!', '<b>')
+        text = text.replace('?', '</b>')
+        text += '<br><br>'
+        self.moveCursor(QTextCursor.End)
+        self.insertHtml(text)
+
+    def save_log(self, app):
+        html_content = self.toHtml()
+        txt_content = self.toPlainText()
+        app.save_to_file(content_to_save=html_content, txt_content=txt_content)
+
+
+class _ExpandableTextEdit(FCTextEdit):
+    """
+    Class implements edit line, which expands themselves automatically
+    """
+
+    historyNext = QtCore.pyqtSignal()
+    historyPrev = QtCore.pyqtSignal()
+
+    def __init__(self, termwidget, *args):
+        FCTextEdit.__init__(self, *args)
+        self.setStyleSheet("font: 9pt \"Courier\";")
+        self._fittedHeight = 1
+        self.textChanged.connect(self._fit_to_document)
+        self._fit_to_document()
+        self._termWidget = termwidget
+
+        self.completer = MyCompleter()
+
+        self.model = QtCore.QStringListModel()
+        self.completer.setModel(self.model)
+        self.set_model_data(keyword_list=[])
+        self.completer.insertText.connect(self.insertCompletion)
+        self.completer.popup().clicked.connect(self.insert_completion_click)
+
+    def set_model_data(self, keyword_list):
+        self.model.setStringList(keyword_list)
+
+    def insert_completion_click(self):
+        self.completer.insertText.emit(self.completer.getSelected())
+        self.completer.setCompletionMode(QCompleter.PopupCompletion)
+
+    def insertCompletion(self, completion):
+        tc = self.textCursor()
+        extra = (len(completion) - len(self.completer.completionPrefix()))
+
+        # don't insert if the word is finished but add a space instead
+        if extra == 0:
+            tc.insertText(' ')
+            self.completer.popup().hide()
+            return
+
+        tc.movePosition(QTextCursor.Left)
+        tc.movePosition(QTextCursor.EndOfWord)
+        tc.insertText(completion[-extra:])
+        # add a space after inserting the word
+        tc.insertText(' ')
+        self.setTextCursor(tc)
+        self.completer.popup().hide()
+
+    def focusInEvent(self, event):
+        if self.completer:
+            self.completer.setWidget(self)
+        QTextEdit.focusInEvent(self, event)
+
+    def keyPressEvent(self, event):
+        """
+        Catch keyboard events. Process Enter, Up, Down
+        """
+
+        key = event.key()
+        if (key == Qt.Key_Tab or key == Qt.Key_Return or key == Qt.Key_Enter) and self.completer.popup().isVisible():
+            self.completer.insertText.emit(self.completer.getSelected())
+            self.completer.setCompletionMode(QCompleter.PopupCompletion)
+            return
+
+        if event.matches(QKeySequence.InsertParagraphSeparator):
+            text = self.toPlainText()
+            if self._termWidget.is_command_complete(text):
+                self._termWidget.exec_current_command()
+                return
+        elif event.matches(QKeySequence.MoveToNextLine):
+            text = self.toPlainText()
+            cursor_pos = self.textCursor().position()
+            textBeforeEnd = text[cursor_pos:]
+
+            if len(textBeforeEnd.split('\n')) <= 1:
+                self.historyNext.emit()
+                return
+        elif event.matches(QKeySequence.MoveToPreviousLine):
+            text = self.toPlainText()
+            cursor_pos = self.textCursor().position()
+            text_before_start = text[:cursor_pos]
+            # lineCount = len(textBeforeStart.splitlines())
+            line_count = len(text_before_start.split('\n'))
+            if len(text_before_start) > 0 and \
+                    (text_before_start[-1] == '\n' or text_before_start[-1] == '\r'):
+                line_count += 1
+            if line_count <= 1:
+                self.historyPrev.emit()
+                return
+        elif event.matches(QKeySequence.MoveToNextPage) or event.matches(QKeySequence.MoveToPreviousPage):
+            return self._termWidget.browser().keyPressEvent(event)
+
+        tc = self.textCursor()
+
+        QTextEdit.keyPressEvent(self, event)
+        tc.select(QTextCursor.WordUnderCursor)
+        cr = self.cursorRect()
+
+        if len(tc.selectedText()) > 0:
+            self.completer.setCompletionPrefix(tc.selectedText())
+            popup = self.completer.popup()
+            popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
+
+            cr.setWidth(self.completer.popup().sizeHintForColumn(0)
+                        + self.completer.popup().verticalScrollBar().sizeHint().width())
+            self.completer.complete(cr)
+        else:
+            self.completer.popup().hide()
+
+    def sizeHint(self):
+        """
+        QWidget sizeHint impelemtation
+        """
+        hint = QTextEdit.sizeHint(self)
+        hint.setHeight(self._fittedHeight)
+        return hint
+
+    def _fit_to_document(self):
+        """
+        Update widget height to fit all text
+        """
+        documentsize = self.document().size().toSize()
+        self._fittedHeight = documentsize.height() + (self.height() - self.viewport().height())
+        self.setMaximumHeight(self._fittedHeight)
+        self.updateGeometry()
+
+    def insertFromMimeData(self, mime_data):
+        # Paste only plain text.
+        self.insertPlainText(mime_data.text())
+
+
+class MyCompleter(QCompleter):
+    insertText = QtCore.pyqtSignal(str)
+
+    def __init__(self, parent=None):
+        QCompleter.__init__(self, parent=parent)
+        self.setCompletionMode(QCompleter.PopupCompletion)
+        self.highlighted.connect(self.setHighlighted)
+
+        self.lastSelected = ''
+
+        # self.popup().installEventFilter(self)
+
+    # def eventFilter(self, obj, event):
+    #     if event.type() == QtCore.QEvent.Wheel and obj is self.popup():
+    #         pass
+    #     return False
+
+    def setHighlighted(self, text):
+        self.lastSelected = text
+
+    def getSelected(self):
+        return self.lastSelected
+
+
+class FCTextAreaLineNumber(QtWidgets.QFrame):
+    textChanged = QtCore.pyqtSignal()
+
+    class NumberBar(QtWidgets.QWidget):
+
+        def __init__(self, edit):
+            QtWidgets.QWidget.__init__(self, edit)
+
+            self.edit = edit
+            self.adjustWidth(1)
+
+        def paintEvent(self, event):
+            self.edit.numberbarPaint(self, event)
+            QtWidgets.QWidget.paintEvent(self, event)
+
+        def adjustWidth(self, count):
+            # three spaces added to the width to make up for the space added in the line number
+            width = self.fontMetrics().width(str(count) + '   ')
+            if self.width() != width:
+                self.setFixedWidth(width)
+
+        def updateContents(self, rect, scroll):
+            if scroll:
+                self.scroll(0, scroll)
+            else:
+                # It would be nice to do
+                # self.update(0, rect.y(), self.width(), rect.height())
+                # But we can't because it will not remove the bold on the
+                # current line if word wrap is enabled and a new block is
+                # selected.
+                self.update()
+
+    class PlainTextEdit(FCPlainTextAreaExtended):
+        """
+        TextEdit with line numbers and highlight
+        From here: https://nachtimwald.com/2009/08/19/better-qplaintextedit-with-line-numbers/
+        and from here: https://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html
+        """
+
+        def __init__(self, *args):
+            FCPlainTextAreaExtended.__init__(self, *args)
+
+            # self.setFrameStyle(QFrame.NoFrame)
+            self.setFrameStyle(QtWidgets.QFrame.NoFrame)
+            self.highlight()
+            # self.setLineWrapMode(QPlainTextEdit.NoWrap)
+            self.cursorPositionChanged.connect(self.highlight)
+
+        def highlight(self):
+            hi_selection = QTextEdit.ExtraSelection()
+
+            hi_selection.format.setBackground(self.palette().alternateBase())
+            hi_selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True)
+            hi_selection.cursor = self.textCursor()
+            hi_selection.cursor.clearSelection()
+
+            self.setExtraSelections([hi_selection])
+
+        def numberbarPaint(self, number_bar, event):
+            font_metrics = self.fontMetrics()
+            current_line = self.document().findBlock(self.textCursor().position()).blockNumber() + 1
+
+            painter = QtGui.QPainter(number_bar)
+            painter.fillRect(event.rect(), QtCore.Qt.lightGray)
+
+            block = self.firstVisibleBlock()
+            line_count = int(block.blockNumber())
+            block_top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
+            block_bottom = block_top + int(self.blockBoundingRect(block).height())
+
+            # Iterate over all visible text blocks in the document.
+            while block.isValid() and block_top <= event.rect().bottom():
+                line_count += 1
+
+                # Check if the position of the block is out side of the visible
+                # area.
+                if block.isVisible() and block_bottom >= event.rect().top():
+
+                    # We want the line number for the selected line to be bold.
+                    if line_count == current_line:
+                        font = painter.font()
+                        font.setBold(True)
+                        painter.setPen(QtCore.Qt.blue)
+                        painter.setFont(font)
+                    else:
+                        font = painter.font()
+                        font.setBold(False)
+                        painter.setPen(self.palette().base().color())
+                        painter.setFont(font)
+
+                    # Draw the line number right justified at the position of the line.
+                    paint_rect = QtCore.QRect(0, block_top, number_bar.width(), font_metrics.height())
+                    # I add some spaces to the line_count to prettify; make sure to remember adjust the width in the
+                    # NumberBar() class above
+                    painter.drawText(paint_rect, Qt.AlignRight, ' ' + str(line_count) + '  ')
+
+                block = block.next()
+                block_top = block_bottom
+                block_bottom = block_top + self.blockBoundingRect(block).height()
+
+            painter.end()
+
+    def __init__(self, *args):
+        QtWidgets.QFrame.__init__(self, *args)
+
+        self.setFrameStyle(QtWidgets.QFrame.StyledPanel | QtWidgets.QFrame.Sunken)
+
+        self.edit = self.PlainTextEdit()
+        self.number_bar = self.NumberBar(self.edit)
+
+        hbox = QtWidgets.QHBoxLayout(self)
+        hbox.setSpacing(0)
+        hbox.setContentsMargins(0, 0, 0, 0)
+        hbox.addWidget(self.number_bar)
+        hbox.addWidget(self.edit)
+
+        self.edit.blockCountChanged.connect(self.number_bar.adjustWidth)
+        self.edit.updateRequest.connect(self.number_bar.updateContents)
+
+    def getText(self):
+        return str(self.edit.toPlainText())
+
+    def setText(self, text):
+        self.edit.setPlainText(text)
+
+    def isModified(self):
+        return self.edit.document().isModified()
+
+    def setModified(self, modified):
+        self.edit.document().setModified(modified)
+
+    def setLineWrapMode(self, mode):
+        self.edit.setLineWrapMode(mode)
+
+
+class FCFileSaveDialog(QtWidgets.QFileDialog):
+
+    def __init__(self, *args):
+        super(FCFileSaveDialog, self).__init__(*args)
+
+    @staticmethod
+    def get_saved_filename(parent=None, caption='', directory='', ext_filter='', initialFilter=''):
+        filename, _filter = QtWidgets.QFileDialog.getSaveFileName(parent=parent, caption=caption,
+                                                                  directory=directory, filter=ext_filter,
+                                                                  initialFilter=initialFilter)
+
+        filename = str(filename)
+        if filename == '':
+            return filename, _filter
+
+        extension = '.' + _filter.strip(')').rpartition('.')[2]
+
+        if filename.endswith(extension) or extension == '.*':
+            return filename, _filter
+        else:
+            filename += extension
+            return filename, _filter
+
+
+class FCDock(QtWidgets.QDockWidget):
+
+    def __init__(self, *args, **kwargs):
+        super(FCDock, self).__init__(*args)
+        self.close_callback = kwargs["close_callback"] if "close_callback" in kwargs else None
+
+    def closeEvent(self, event: QtGui.QCloseEvent) -> None:
+        self.close_callback()
+        super().closeEvent(event)
+
+    def show(self) -> None:
+        if self.isFloating():
+            self.setFloating(False)
+        super().show()
+
+
+class FCJog(QtWidgets.QFrame):
+
+    def __init__(self, app, *args, **kwargs):
+        super(FCJog, self).__init__(*args, **kwargs)
+
+        self.app = app
+        self.setFrameShape(QtWidgets.QFrame.Box)
+        self.setLineWidth(1)
+
+        # JOG axes
+        grbl_jog_grid = QtWidgets.QGridLayout()
+        grbl_jog_grid.setAlignment(QtCore.Qt.AlignCenter)
+        grbl_jog_grid.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize)
+        grbl_jog_grid.setContentsMargins(2, 4, 2, 4)
+
+        self.setLayout(grbl_jog_grid)
+
+        # JOG Y Up
+        self.jog_up_button = QtWidgets.QToolButton()
+        self.jog_up_button.setIcon(QtGui.QIcon(self.app.resource_location + '/up-arrow32.png'))
+        self.jog_up_button.setToolTip(
+            _("Jog the Y axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_up_button, 2, 1)
+
+        # Origin
+        self.jog_origin_button = QtWidgets.QToolButton()
+        self.jog_origin_button.setIcon(QtGui.QIcon(self.app.resource_location + '/origin2_32.png'))
+        self.jog_origin_button.setToolTip(
+            '%s' % _("Move to Origin")
+        )
+
+        grbl_jog_grid.addWidget(self.jog_origin_button, 3, 1)
+
+        # JOG Y Down
+        self.jog_down_button = QtWidgets.QToolButton()
+        self.jog_down_button.setIcon(QtGui.QIcon(self.app.resource_location + '/down-arrow32.png'))
+        self.jog_down_button.setToolTip(
+            _("Jog the Y axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_down_button, 4, 1)
+
+        # JOG X Left
+        self.jog_left_button = QtWidgets.QToolButton()
+        self.jog_left_button.setIcon(QtGui.QIcon(self.app.resource_location + '/left_arrow32.png'))
+        self.jog_left_button.setToolTip(
+            _("Jog the X axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_left_button, 3, 0)
+
+        # JOG X Right
+        self.jog_right_button = QtWidgets.QToolButton()
+        self.jog_right_button.setIcon(QtGui.QIcon(self.app.resource_location + '/right_arrow32.png'))
+        self.jog_right_button.setToolTip(
+            _("Jog the X axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_right_button, 3, 2)
+
+        # JOG Z Up
+        self.jog_z_up_button = QtWidgets.QToolButton()
+        self.jog_z_up_button.setIcon(QtGui.QIcon(self.app.resource_location + '/up-arrow32.png'))
+        self.jog_z_up_button.setText('Z')
+        self.jog_z_up_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
+        self.jog_z_up_button.setToolTip(
+            _("Jog the Z axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_z_up_button, 2, 3)
+
+        # JOG Z Down
+        self.jog_z_down_button = QtWidgets.QToolButton()
+        self.jog_z_down_button.setIcon(QtGui.QIcon(self.app.resource_location + '/down-arrow32.png'))
+        self.jog_z_down_button.setText('Z')
+        self.jog_z_down_button.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
+        self.jog_z_down_button.setToolTip(
+            _("Jog the Z axis.")
+        )
+        grbl_jog_grid.addWidget(self.jog_z_down_button, 4, 3)
+
+
+class FCZeroAxes(QtWidgets.QFrame):
+
+    def __init__(self, app, *args, **kwargs):
+        super(FCZeroAxes, self).__init__(*args, **kwargs)
+        self.app = app
+
+        self.setFrameShape(QtWidgets.QFrame.Box)
+        self.setLineWidth(1)
+
+        # Zero the axes
+        grbl_zero_grid = QtWidgets.QGridLayout()
+        grbl_zero_grid.setContentsMargins(2, 4, 2, 4)
+        grbl_zero_grid.setColumnStretch(0, 0)
+        grbl_zero_grid.setColumnStretch(1, 0)
+        # grbl_zero_grid.setRowStretch(4, 1)
+        self.setLayout(grbl_zero_grid)
+
+        # Zero X axis
+        self.grbl_zerox_button = QtWidgets.QToolButton()
+        self.grbl_zerox_button.setText(_("X"))
+        self.grbl_zerox_button.setToolTip(
+            _("Zero the CNC X axes at current position.")
+        )
+        grbl_zero_grid.addWidget(self.grbl_zerox_button, 1, 0)
+        # Zero Y axis
+        self.grbl_zeroy_button = QtWidgets.QToolButton()
+        self.grbl_zeroy_button.setText(_("Y"))
+
+        self.grbl_zeroy_button.setToolTip(
+            _("Zero the CNC Y axes at current position.")
+        )
+        grbl_zero_grid.addWidget(self.grbl_zeroy_button, 2, 0)
+        # Zero Z axis
+        self.grbl_zeroz_button = QtWidgets.QToolButton()
+        self.grbl_zeroz_button.setText(_("Z"))
+
+        self.grbl_zeroz_button.setToolTip(
+            _("Zero the CNC Z axes at current position.")
+        )
+        grbl_zero_grid.addWidget(self.grbl_zeroz_button, 3, 0)
+        self.grbl_homing_button = QtWidgets.QToolButton()
+        self.grbl_homing_button.setText(_("Do Home"))
+        self.grbl_homing_button.setToolTip(
+            _("Perform a homing cycle on all axis."))
+        grbl_zero_grid.addWidget(self.grbl_homing_button, 4, 0, 1, 2)
+        # Zeroo all axes
+        self.grbl_zero_all_button = QtWidgets.QToolButton()
+        self.grbl_zero_all_button.setText(_("All"))
+        self.grbl_zero_all_button.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+
+        self.grbl_zero_all_button.setToolTip(
+            _("Zero all CNC axes at current position.")
+        )
+        grbl_zero_grid.addWidget(self.grbl_zero_all_button, 1, 1, 3, 1)
+
+
+class RotatedToolButton(QtWidgets.QToolButton):
+    def __init__(self, orientation="east", *args, **kwargs):
+        super(RotatedToolButton, self).__init__(*args, **kwargs)
+        self.orientation = orientation
+
+    def paintEvent(self, event):
+        painter = QtWidgets.QStylePainter(self)
+        if self.orientation == "east":
+            painter.rotate(270)
+            painter.translate(-1 * self.height(), 0)
+        if self.orientation == "west":
+            painter.rotate(90)
+            painter.translate(0, -1 * self.width())
+        painter.drawControl(QtWidgets.QStyle.CE_PushButton, self.getSyleOptions())
+
+    def minimumSizeHint(self):
+        size = super(RotatedToolButton, self).minimumSizeHint()
+        size.transpose()
+        return size
+
+    def sizeHint(self):
+        size = super(RotatedToolButton, self).sizeHint()
+        size.transpose()
+        return size
+
+    def getSyleOptions(self):
+        options = QtWidgets.QStyleOptionButton()
+        options.initFrom(self)
+        size = options.rect.size()
+        size.transpose()
+        options.rect.setSize(size)
+        options.features = QtWidgets.QStyleOptionButton.None_
+        # if self.isFlat():
+        #     options.features |= QtWidgets.QStyleOptionButton.Flat
+        if self.menu():
+            options.features |= QtWidgets.QStyleOptionButton.HasMenu
+        # if self.autoDefault() or self.isDefault():
+        #     options.features |= QtWidgets.QStyleOptionButton.AutoDefaultButton
+        # if self.isDefault():
+        #     options.features |= QtWidgets.QStyleOptionButton.DefaultButton
+        if self.isDown() or (self.menu() and self.menu().isVisible()):
+            options.state |= QtWidgets.QStyle.State_Sunken
+        if self.isChecked():
+            options.state |= QtWidgets.QStyle.State_On
+        # if not self.isFlat() and not self.isDown():
+        #     options.state |= QtWidgets.QStyle.State_Raised
+
+        options.text = self.text()
+        options.icon = self.icon()
+        options.iconSize = self.iconSize()
+        return options
+
+
+class RotatedButton(QtWidgets.QPushButton):
+    def __init__(self, orientation="west", *args, **kwargs):
+        super(RotatedButton, self).__init__(*args, **kwargs)
+        self.orientation = orientation
+
+    def paintEvent(self, event):
+        painter = QtWidgets.QStylePainter(self)
+        if self.orientation == "east":
+            painter.rotate(270)
+            painter.translate(-1 * self.height(), 0)
+        if self.orientation == "west":
+            painter.rotate(90)
+            painter.translate(0, -1 * self.width())
+        painter.drawControl(QtWidgets.QStyle.CE_PushButton, self.getSyleOptions())
+
+    def minimumSizeHint(self):
+        size = super(RotatedButton, self).minimumSizeHint()
+        size.transpose()
+        return size
+
+    def sizeHint(self):
+        size = super(RotatedButton, self).sizeHint()
+        size.transpose()
+        return size
+
+    def getSyleOptions(self):
+        options = QtWidgets.QStyleOptionButton()
+        options.initFrom(self)
+        size = options.rect.size()
+        size.transpose()
+        options.rect.setSize(size)
+        options.features = QtWidgets.QStyleOptionButton.None_
+        if self.isFlat():
+            options.features |= QtWidgets.QStyleOptionButton.Flat
+        if self.menu():
+            options.features |= QtWidgets.QStyleOptionButton.HasMenu
+        if self.autoDefault() or self.isDefault():
+            options.features |= QtWidgets.QStyleOptionButton.AutoDefaultButton
+        if self.isDefault():
+            options.features |= QtWidgets.QStyleOptionButton.DefaultButton
+        if self.isDown() or (self.menu() and self.menu().isVisible()):
+            options.state |= QtWidgets.QStyle.State_Sunken
+        if self.isChecked():
+            options.state |= QtWidgets.QStyle.State_On
+        if not self.isFlat() and not self.isDown():
+            options.state |= QtWidgets.QStyle.State_Raised
+
+        options.text = self.text()
+        options.icon = self.icon()
+        options.iconSize = self.iconSize()
+        return options
+
+
+class FlatCAMActivityView(QtWidgets.QWidget):
+    """
+    This class create and control the activity icon displayed in the App status bar
+    """
+
+    def __init__(self, app, parent=None):
+        super().__init__(parent=parent)
+
+        self.app = app
+
+        if self.app.defaults["global_activity_icon"] == "Ball green":
+            icon = self.app.resource_location + '/active_2_static.png'
+            movie = self.app.resource_location + "/active_2.gif"
+        elif self.app.defaults["global_activity_icon"] == "Ball black":
+            icon = self.app.resource_location + '/active_static.png'
+            movie = self.app.resource_location + "/active.gif"
+        elif self.app.defaults["global_activity_icon"] == "Arrow green":
+            icon = self.app.resource_location + '/active_3_static.png'
+            movie = self.app.resource_location + "/active_3.gif"
+        elif self.app.defaults["global_activity_icon"] == "Eclipse green":
+            icon = self.app.resource_location + '/active_4_static.png'
+            movie = self.app.resource_location + "/active_4.gif"
+        else:
+            icon = self.app.resource_location + '/active_static.png'
+            movie = self.app.resource_location + "/active.gif"
+
+        self.setMinimumWidth(200)
+        self.movie_path = movie
+        self.icon_path = icon
+
+        self.icon = FCLabel(self)
+        self.icon.setGeometry(0, 0, 16, 12)
+        self.movie = QtGui.QMovie(self.movie_path)
+
+        self.icon.setMovie(self.movie)
+        # self.movie.start()
+
+        layout = QtWidgets.QHBoxLayout()
+        layout.setContentsMargins(5, 0, 5, 0)
+        layout.setAlignment(QtCore.Qt.AlignLeft)
+        self.setLayout(layout)
+
+        layout.addWidget(self.icon)
+        self.text = QtWidgets.QLabel(self)
+        self.text.setText(_("Idle."))
+        self.icon.setPixmap(QtGui.QPixmap(self.icon_path))
+
+        layout.addWidget(self.text)
+
+        self.icon.clicked.connect(self.app.on_toolbar_replot)
+
+    def set_idle(self):
+        self.movie.stop()
+        self.text.setText(_("Idle."))
+
+    def set_busy(self, msg, no_movie=None):
+        if no_movie is not True:
+            self.icon.setMovie(self.movie)
+            self.movie.start()
+        self.text.setText(msg)
+
+
+class FlatCAMInfoBar(QtWidgets.QWidget):
+    """
+    This class create a place to display the App messages in the Status Bar
+    """
+
+    def __init__(self, parent=None, app=None):
+        super(FlatCAMInfoBar, self).__init__(parent=parent)
+
+        self.app = app
+
+        self.icon = QtWidgets.QLabel(self)
+        self.icon.setGeometry(0, 0, 12, 12)
+        self.pmap = QtGui.QPixmap(self.app.resource_location + '/graylight12.png')
+        self.icon.setPixmap(self.pmap)
+
+        self.lock_pmaps = False
+
+        layout = QtWidgets.QHBoxLayout()
+        layout.setContentsMargins(5, 0, 5, 0)
+        self.setLayout(layout)
+
+        layout.addWidget(self.icon)
+
+        self.text = QtWidgets.QLabel(self)
+        self.text.setText(_("Application started ..."))
+        self.text.setToolTip(_("Hello!"))
+
+        layout.addWidget(self.text)
+        layout.addStretch()
+
+    def set_text_(self, text, color=None):
+        self.text.setText(text)
+        self.text.setToolTip(text)
+        if color:
+            self.text.setStyleSheet('color: %s' % str(color))
+
+    def set_status(self, text, level="info"):
+        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')
+
+        try:
+            self.set_text_(text)
+            self.icon.setPixmap(self.pmap)
+        except Exception as e:
+            log.debug("FlatCAMInfoBar.set_status() --> %s" % str(e))
+
+
+class FlatCAMSystemTray(QtWidgets.QSystemTrayIcon):
+    """
+    This class create the Sys Tray icon for the app
+    """
+
+    def __init__(self, app, icon, headless=None, parent=None):
+        # QtWidgets.QSystemTrayIcon.__init__(self, icon, parent)
+        super().__init__(icon, parent=parent)
+        self.app = app
+
+        menu = QtWidgets.QMenu(parent)
+
+        menu_runscript = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/script14.png'),
+                                           '%s' % _('Run Script ...'), self)
+        menu_runscript.setToolTip(
+            _("Will run the opened Tcl Script thus\n"
+              "enabling the automation of certain\n"
+              "functions of FlatCAM.")
+        )
+        menu.addAction(menu_runscript)
+
+        menu.addSeparator()
+
+        if headless is None:
+            self.menu_open = menu.addMenu(QtGui.QIcon(self.app.resource_location + '/folder32_bis.png'), _('Open'))
+
+            # Open Project ...
+            menu_openproject = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/folder16.png'),
+                                                 '%s ...' % _('Open Project'), self)
+            self.menu_open.addAction(menu_openproject)
+            self.menu_open.addSeparator()
+
+            # Open Gerber ...
+            menu_opengerber = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/flatcam_icon24.png'),
+                                                '%s ...\t%s' % (_('Open Gerber'), _('Ctrl+G')), self)
+            self.menu_open.addAction(menu_opengerber)
+
+            # Open Excellon ...
+            menu_openexcellon = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/open_excellon32.png'),
+                                                  '%s ...\t%s' % (_('Open Excellon'), _('Ctrl+E')), self)
+            self.menu_open.addAction(menu_openexcellon)
+
+            # Open G-Code ...
+            menu_opengcode = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/code.png'),
+                                               '%s ...' % _('Open G-Code'), self)
+            self.menu_open.addAction(menu_opengcode)
+
+            self.menu_open.addSeparator()
+
+            menu_openproject.triggered.connect(self.app.f_handlers.on_file_openproject)
+            menu_opengerber.triggered.connect(self.app.f_handlers.on_fileopengerber)
+            menu_openexcellon.triggered.connect(self.app.f_handlers.on_fileopenexcellon)
+            menu_opengcode.triggered.connect(self.app.f_handlers.on_fileopengcode)
+
+        exitAction = menu.addAction(_("Exit"))
+        exitAction.setIcon(QtGui.QIcon(self.app.resource_location + '/power16.png'))
+        self.setContextMenu(menu)
+
+        menu_runscript.triggered.connect(lambda: self.app.on_filerunscript(
+            silent=True if self.app.cmd_line_headless == 1 else False))
+
+        exitAction.triggered.connect(self.app.final_save)
+
+
+def message_dialog(title, message, kind="info", parent=None):
+    """
+    Builds and show a custom QMessageBox to be used in FlatCAM.
+
+    :param title:       title of the QMessageBox
+    :param message:     message to be displayed
+    :param kind:        type of QMessageBox; will display a specific icon.
+    :param parent:      parent
+    :return:            None
+    """
+    icon = {"info": QtWidgets.QMessageBox.Information,
+            "warning": QtWidgets.QMessageBox.Warning,
+            "error": QtWidgets.QMessageBox.Critical}[str(kind)]
+    dlg = QtWidgets.QMessageBox(icon, title, message, parent=parent)
+    dlg.setText(message)
+    dlg.exec_()
+
+
+def rreplace(s, old, new, occurrence):
+    """
+    Credits go here:
+    https://stackoverflow.com/questions/2556108/rreplace-how-to-replace-the-last-occurrence-of-an-expression-in-a-string
+
+    :param s: string to be processed
+    :param old: old char to be replaced
+    :param new: new char to replace the old one
+    :param occurrence: how many places from end to replace the old char
+    :return: modified string
+    """
+
+    li = s.rsplit(old, occurrence)
+    return new.join(li)

+ 4983 - 0
appGUI/MainGUI.py

@@ -0,0 +1,4983 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+# ##########################################################
+
+# ##########################################################
+# File Modified (major mod): Marius Adrian Stanciu         #
+# Date: 3/10/2019                                          #
+# ##########################################################
+from PyQt5.QtCore import QSettings
+
+import platform
+
+from appGUI.GUIElements import *
+from appGUI.preferences import settings
+from appGUI.preferences.cncjob.CNCJobPreferencesUI import CNCJobPreferencesUI
+from appGUI.preferences.excellon.ExcellonPreferencesUI import ExcellonPreferencesUI
+from appGUI.preferences.general.GeneralPreferencesUI import GeneralPreferencesUI
+from appGUI.preferences.geometry.GeometryPreferencesUI import GeometryPreferencesUI
+from appGUI.preferences.gerber.GerberPreferencesUI import GerberPreferencesUI
+from appEditors.AppGeoEditor import FCShapeTool
+
+from matplotlib.backend_bases import KeyEvent as mpl_key_event
+
+import webbrowser
+
+from appGUI.preferences.tools.Tools2PreferencesUI import Tools2PreferencesUI
+from appGUI.preferences.tools.ToolsPreferencesUI import ToolsPreferencesUI
+from appGUI.preferences.utilities.UtilPreferencesUI import UtilPreferencesUI
+from appObjects.ObjectCollection import KeySensitiveListView
+
+import subprocess
+import os
+import sys
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class MainGUI(QtWidgets.QMainWindow):
+    # Emitted when persistent window geometry needs to be retained
+    geom_update = QtCore.pyqtSignal(int, int, int, int, int, name='geomUpdate')
+    final_save = QtCore.pyqtSignal(name='saveBeforeExit')
+
+    def __init__(self, app):
+        super(MainGUI, self).__init__()
+
+        self.app = app
+        self.decimals = self.app.decimals
+
+        # Divine icon pack by Ipapun @ finicons.com
+
+        # #######################################################################
+        # ############ BUILDING THE GUI IS EXECUTED HERE ########################
+        # #######################################################################
+
+        # #######################################################################
+        # ###################### Menu BUILDING ##################################
+        # #######################################################################
+        self.menu = self.menuBar()
+
+        self.menu_toggle_nb = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/notebook32.png'), _("Toggle Panel"))
+        self.menu_toggle_nb.setToolTip(
+            _("Toggle Panel")
+        )
+        # self.menu_toggle_nb = QtWidgets.QAction("NB")
+
+        self.menu_toggle_nb.setCheckable(True)
+        self.menu.addAction(self.menu_toggle_nb)
+
+        # ########################################################################
+        # ########################## File # ######################################
+        # ########################################################################
+        self.menufile = self.menu.addMenu(_('File'))
+        self.menufile.setToolTipsVisible(True)
+
+        # New Project
+        self.menufilenewproject = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/file16.png'),
+                                                    '%s...\t%s' % (_('New Project'), _("Ctrl+N")), self)
+        self.menufilenewproject.setToolTip(
+            _("Will create a new, blank project")
+        )
+        self.menufile.addAction(self.menufilenewproject)
+
+        # New Category (Excellon, Geometry)
+        self.menufilenew = self.menufile.addMenu(QtGui.QIcon(self.app.resource_location + '/file16.png'), _('New'))
+        self.menufilenew.setToolTipsVisible(True)
+
+        self.menufilenewgeo = self.menufilenew.addAction(
+            QtGui.QIcon(self.app.resource_location + '/new_file_geo16.png'), '%s\t%s' % (_('Geometry'), _('N')))
+        self.menufilenewgeo.setToolTip(
+            _("Will create a new, empty Geometry Object.")
+        )
+        self.menufilenewgrb = self.menufilenew.addAction(
+            QtGui.QIcon(self.app.resource_location + '/new_file_grb16.png'), '%s\t%s' % (_('Gerber'), _('B')))
+        self.menufilenewgrb.setToolTip(
+            _("Will create a new, empty Gerber Object.")
+        )
+        self.menufilenewexc = self.menufilenew.addAction(
+            QtGui.QIcon(self.app.resource_location + '/new_file_exc16.png'), '%s\t%s' % (_('Excellon'), _('L')))
+        self.menufilenewexc.setToolTip(
+            _("Will create a new, empty Excellon Object.")
+        )
+        self.menufilenew.addSeparator()
+
+        self.menufilenewdoc = self.menufilenew.addAction(
+            QtGui.QIcon(self.app.resource_location + '/notes16_1.png'), '%s\t%s' % (_('Document'), _('D')))
+        self.menufilenewdoc.setToolTip(
+            _("Will create a new, empty Document Object.")
+        )
+
+        self.menufile_open = self.menufile.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/folder32_bis.png'), '%s' % _('Open'))
+        self.menufile_open.setToolTipsVisible(True)
+
+        # Open Project ...
+        self.menufileopenproject = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/folder16.png'), '%s...\t%s' % (_('Open Project'), _('Ctrl+O')),
+            self)
+        self.menufile_open.addAction(self.menufileopenproject)
+        self.menufile_open.addSeparator()
+
+        # Open Gerber ...
+        self.menufileopengerber = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/flatcam_icon24.png'),
+                                                    '%s...\t%s' % (_('Open Gerber'), _('Ctrl+G')), self)
+        self.menufile_open.addAction(self.menufileopengerber)
+
+        # Open Excellon ...
+        self.menufileopenexcellon = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/open_excellon32.png'),
+                                                      '%s...\t%s' % (_('Open Excellon'), _('Ctrl+E')), self)
+        self.menufile_open.addAction(self.menufileopenexcellon)
+
+        # Open G-Code ...
+        self.menufileopengcode = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/code.png'), '%s...\t%s' % (_('Open G-Code'), ''), self)
+        self.menufile_open.addAction(self.menufileopengcode)
+
+        self.menufile_open.addSeparator()
+
+        # Open Config File...
+        self.menufileopenconfig = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/folder16.png'), '%s...\t%s' % (_('Open Config'), ''), self)
+        self.menufile_open.addAction(self.menufileopenconfig)
+
+        # Recent
+        self.recent_projects = self.menufile.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/recent_files.png'), _("Recent projects"))
+        self.recent = self.menufile.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/recent_files.png'), _("Recent files"))
+
+        # SAVE category
+        self.menufile_save = self.menufile.addMenu(QtGui.QIcon(self.app.resource_location + '/save_as.png'), _('Save'))
+
+        # Save Project
+        self.menufilesaveproject = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/floppy16.png'), '%s...\t%s' % (_('Save Project'), _('Ctrl+S')),
+            self)
+        self.menufile_save.addAction(self.menufilesaveproject)
+
+        # Save Project As ...
+        self.menufilesaveprojectas = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/floppy16.png'),
+                                                       '%s...\t%s' % (_('Save Project As'), _('Ctrl+Shift+S')), self)
+        self.menufile_save.addAction(self.menufilesaveprojectas)
+
+        # Save Project Copy ...
+        # self.menufilesaveprojectcopy = QtWidgets.QAction(
+        #     QtGui.QIcon(self.app.resource_location + '/floppy16.png'), _('Save Project Copy ...'), self)
+        # self.menufile_save.addAction(self.menufilesaveprojectcopy)
+
+        self.menufile_save.addSeparator()
+
+        # Separator
+        self.menufile.addSeparator()
+
+        # Scripting
+        self.menufile_scripting = self.menufile.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/script16.png'), _('Scripting'))
+        self.menufile_scripting.setToolTipsVisible(True)
+
+        self.menufilenewscript = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/script_new16.png'),
+                                                   '%s...\t%s' % (_('New Script'), ''), self)
+        self.menufileopenscript = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/open_script32.png'),
+                                                    '%s...\t%s' % (_('Open Script'), ''), self)
+        self.menufileopenscriptexample = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/open_script32.png'),
+            '%s...\t%s' % (_('Open Example'), ''), self)
+        self.menufilerunscript = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/script16.png'),
+            '%s...\t%s' % (_('Run Script'), _('Shift+S')), self)
+        self.menufilerunscript.setToolTip(
+            _("Will run the opened Tcl Script thus\n"
+              "enabling the automation of certain\n"
+              "functions of FlatCAM.")
+        )
+        self.menufile_scripting.addAction(self.menufilenewscript)
+        self.menufile_scripting.addAction(self.menufileopenscript)
+        self.menufile_scripting.addAction(self.menufileopenscriptexample)
+        self.menufile_scripting.addSeparator()
+        self.menufile_scripting.addAction(self.menufilerunscript)
+
+        # Separator
+        self.menufile.addSeparator()
+
+        # Import ...
+        self.menufileimport = self.menufile.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/import.png'), _('Import'))
+        self.menufileimportsvg = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/svg16.png'),
+            '%s...\t%s' % (_('SVG as Geometry Object'), ''), self)
+        self.menufileimport.addAction(self.menufileimportsvg)
+        self.menufileimportsvg_as_gerber = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/svg16.png'),
+            '%s...\t%s' % (_('SVG as Gerber Object'), ''), self)
+        self.menufileimport.addAction(self.menufileimportsvg_as_gerber)
+        self.menufileimport.addSeparator()
+
+        self.menufileimportdxf = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/dxf16.png'),
+            '%s...\t%s' % (_('DXF as Geometry Object'), ''), self)
+        self.menufileimport.addAction(self.menufileimportdxf)
+        self.menufileimportdxf_as_gerber = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/dxf16.png'),
+            '%s...\t%s' % (_('DXF as Gerber Object'), ''), self)
+        self.menufileimport.addAction(self.menufileimportdxf_as_gerber)
+        self.menufileimport.addSeparator()
+        self.menufileimport_hpgl2_as_geo = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/dxf16.png'),
+            '%s...\t%s' % (_('HPGL2 as Geometry Object'), ''), self)
+        self.menufileimport.addAction(self.menufileimport_hpgl2_as_geo)
+        self.menufileimport.addSeparator()
+
+        # Export ...
+        self.menufileexport = self.menufile.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/export.png'), _('Export'))
+        self.menufileexport.setToolTipsVisible(True)
+
+        self.menufileexportsvg = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/export.png'),
+            '%s...\t%s' % (_('Export SVG'), ''), self)
+        self.menufileexport.addAction(self.menufileexportsvg)
+
+        self.menufileexportdxf = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/export.png'),
+            '%s...\t%s' % (_('Export DXF'), ''), self)
+        self.menufileexport.addAction(self.menufileexportdxf)
+
+        self.menufileexport.addSeparator()
+
+        self.menufileexportpng = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/export_png32.png'),
+            '%s...\t%s' % (_('Export PNG'), ''), self)
+        self.menufileexportpng.setToolTip(
+            _("Will export an image in PNG format,\n"
+              "the saved image will contain the visual \n"
+              "information currently in FlatCAM Plot Area.")
+        )
+        self.menufileexport.addAction(self.menufileexportpng)
+
+        self.menufileexport.addSeparator()
+
+        self.menufileexportexcellon = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/drill32.png'),
+            '%s...\t%s' % (_('Export Excellon'), ''), self)
+        self.menufileexportexcellon.setToolTip(
+            _("Will export an Excellon Object as Excellon file,\n"
+              "the coordinates format, the file units and zeros\n"
+              "are set in Preferences -> Excellon Export.")
+        )
+        self.menufileexport.addAction(self.menufileexportexcellon)
+
+        self.menufileexportgerber = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/flatcam_icon32.png'),
+            '%s...\t%s' % (_('Export Gerber'), ''), self)
+        self.menufileexportgerber.setToolTip(
+            _("Will export an Gerber Object as Gerber file,\n"
+              "the coordinates format, the file units and zeros\n"
+              "are set in Preferences -> Gerber Export.")
+        )
+        self.menufileexport.addAction(self.menufileexportgerber)
+
+        # Separator
+        self.menufile.addSeparator()
+
+        self.menufile_backup = self.menufile.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/backup24.png'), _('Backup'))
+
+        # Import Preferences
+        self.menufileimportpref = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/backup_import24.png'),
+            '%s...\t%s' % (_('Import Preferences from file'), ''), self
+        )
+        self.menufile_backup.addAction(self.menufileimportpref)
+
+        # Export Preferences
+        self.menufileexportpref = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/backup_export24.png'),
+            '%s...\t%s' % (_('Export Preferences to file'), ''), self)
+        self.menufile_backup.addAction(self.menufileexportpref)
+
+        # Separator
+        self.menufile_backup.addSeparator()
+
+        # Save Defaults
+        self.menufilesavedefaults = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/defaults.png'),
+            '%s\t%s' % (_('Save Preferences'), ''), self)
+        self.menufile_backup.addAction(self.menufilesavedefaults)
+
+        # Separator
+        self.menufile.addSeparator()
+        self.menufile_print = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/printer32.png'),
+            '%s\t%s' % (_('Print (PDF)'), _('Ctrl+P')))
+        self.menufile.addAction(self.menufile_print)
+
+        # Separator
+        self.menufile.addSeparator()
+
+        # Quit
+        self.menufile_exit = QtWidgets.QAction(
+            QtGui.QIcon(self.app.resource_location + '/power16.png'),
+            '%s\t%s' % (_('Exit'), ''), self)
+        # exitAction.setShortcut('Ctrl+Q')
+        # exitAction.setStatusTip('Exit application')
+        self.menufile.addAction(self.menufile_exit)
+
+        # ########################################################################
+        # ########################## Edit # ######################################
+        # ########################################################################
+        self.menuedit = self.menu.addMenu(_('Edit'))
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditedit = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/edit16.png'),
+            '%s\t%s' % (_('Edit Object'), _('E')))
+        self.menueditok = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/power16.png'),
+            '%s\t%s' % (_('Exit Editor'), _('Ctrl+S')))
+
+        # adjust the initial state of the menu entries related to the editor
+        self.menueditedit.setDisabled(False)
+        self.menueditok.setDisabled(True)
+
+        # ############################ EDIT -> CONVERSION ######################################################
+        # Separator
+        self.menuedit.addSeparator()
+        self.menuedit_convert = self.menuedit.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/convert24.png'), _('Conversion'))
+
+        self.menuedit_convert_sg2mg = self.menuedit_convert.addAction(
+            QtGui.QIcon(self.app.resource_location + '/convert24.png'),
+            '%s\t%s' % (_('Convert Single to MultiGeo'), ''))
+        self.menuedit_convert_sg2mg.setToolTip(
+            _("Will convert a Geometry object from single_geometry type\n"
+              "to a multi_geometry type.")
+        )
+        self.menuedit_convert_mg2sg = self.menuedit_convert.addAction(
+            QtGui.QIcon(self.app.resource_location + '/convert24.png'),
+            '%s\t%s' % (_('Convert Multi to SingleGeo'), ''))
+        self.menuedit_convert_mg2sg.setToolTip(
+            _("Will convert a Geometry object from multi_geometry type\n"
+              "to a single_geometry type.")
+        )
+        # Separator
+        self.menuedit_convert.addSeparator()
+        self.menueditconvert_any2geo = self.menuedit_convert.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy_geo.png'),
+            '%s\t%s' % (_('Convert Any to Geo'), ''))
+        self.menueditconvert_any2gerber = self.menuedit_convert.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy_geo.png'),
+            '%s\t%s' % (_('Convert Any to Gerber'), ''))
+        self.menueditconvert_any2excellon = self.menuedit_convert.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy_geo.png'),
+            '%s\t%s' % (_('Convert Any to Excellon'), ''))
+        self.menuedit_convert.setToolTipsVisible(True)
+
+        # ############################ EDIT -> JOIN        ######################################################
+        self.menuedit_join = self.menuedit.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/join16.png'), _('Join Objects'))
+        self.menuedit_join2geo = self.menuedit_join.addAction(
+            QtGui.QIcon(self.app.resource_location + '/join16.png'),
+            '%s\t%s' % (_('Join Geo/Gerber/Exc -> Geo'), ''))
+        self.menuedit_join2geo.setToolTip(
+            _("Merge a selection of objects, which can be of type:\n"
+              "- Gerber\n"
+              "- Excellon\n"
+              "- Geometry\n"
+              "into a new combo Geometry object.")
+        )
+        self.menuedit_join_exc2exc = self.menuedit_join.addAction(
+            QtGui.QIcon(self.app.resource_location + '/join16.png'),
+            '%s\t%s' % (_('Join Excellon(s) -> Excellon'), ''))
+        self.menuedit_join_exc2exc.setToolTip(
+            _("Merge a selection of Excellon objects into a new combo Excellon object.")
+        )
+        self.menuedit_join_grb2grb = self.menuedit_join.addAction(
+            QtGui.QIcon(self.app.resource_location + '/join16.png'),
+            '%s\t%s' % (_('Join Gerber(s) -> Gerber'), ''))
+        self.menuedit_join_grb2grb.setToolTip(
+            _("Merge a selection of Gerber objects into a new combo Gerber object.")
+        )
+        self.menuedit_join.setToolTipsVisible(True)
+
+        # ############################ EDIT -> COPY        ######################################################
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditcopyobject = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy.png'),
+            '%s\t%s' % (_('Copy'), _('Ctrl+C')))
+
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditdelete = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/trash16.png'),
+            '%s\t%s' % (_('Delete'), _('DEL')))
+
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditorigin = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/origin16.png'),
+            '%s\t%s' % (_('Set Origin'), _('O')))
+        self.menuedit_move2origin = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/origin2_16.png'),
+            '%s\t%s' % (_('Move to Origin'), _('Shift+O')))
+
+        self.menueditjump = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/jump_to16.png'),
+            '%s\t%s' % (_('Jump to Location'), _('J')))
+        self.menueditlocate = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/locate16.png'),
+            '%s\t%s' % (_('Locate in Object'), _('Shift+J')))
+
+        # Separator
+        self.menuedit.addSeparator()
+        self.menuedittoggleunits = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/toggle_units16.png'),
+            '%s\t%s' % (_('Toggle Units'), _('Q')))
+        self.menueditselectall = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/select_all.png'),
+            '%s\t%s' % (_('Select All'), _('Ctrl+A')))
+
+        # Separator
+        self.menuedit.addSeparator()
+        self.menueditpreferences = self.menuedit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/pref.png'),
+            '%s\t%s' % (_('Preferences'), _('Shift+P')))
+
+        # ########################################################################
+        # ########################## OPTIONS # ###################################
+        # ########################################################################
+
+        self.menuoptions = self.menu.addMenu(_('Options'))
+        self.menuoptions_transform_rotate = self.menuoptions.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rotate.png'),
+            '%s\t%s' % (_("Rotate Selection"), _('Shift+(R)')))
+        # Separator
+        self.menuoptions.addSeparator()
+
+        self.menuoptions_transform_skewx = self.menuoptions.addAction(
+            QtGui.QIcon(self.app.resource_location + '/skewX.png'),
+            '%s\t%s' % (_("Skew on X axis"), _('Shift+X')))
+        self.menuoptions_transform_skewy = self.menuoptions.addAction(
+            QtGui.QIcon(self.app.resource_location + '/skewY.png'),
+            '%s\t%s' % (_("Skew on Y axis"), _('Shift+Y')))
+
+        # Separator
+        self.menuoptions.addSeparator()
+        self.menuoptions_transform_flipx = self.menuoptions.addAction(
+            QtGui.QIcon(self.app.resource_location + '/flipx.png'),
+            '%s\t%s' % (_("Flip on X axis"), _('X')))
+        self.menuoptions_transform_flipy = self.menuoptions.addAction(
+            QtGui.QIcon(self.app.resource_location + '/flipy.png'),
+            '%s\t%s' % (_("Flip on Y axis"), _('Y')))
+        # Separator
+        self.menuoptions.addSeparator()
+
+        self.menuoptions_view_source = self.menuoptions.addAction(
+            QtGui.QIcon(self.app.resource_location + '/source32.png'),
+            '%s\t%s' % (_("View source"), _('Alt+S')))
+        self.menuoptions_tools_db = self.menuoptions.addAction(
+            QtGui.QIcon(self.app.resource_location + '/database32.png'),
+            '%s\t%s' % (_("Tools Database"), _('Ctrl+D')))
+        # Separator
+        self.menuoptions.addSeparator()
+
+        # ########################################################################
+        # ########################## View # ######################################
+        # ########################################################################
+        self.menuview = self.menu.addMenu(_('View'))
+        self.menuviewenable = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/replot16.png'),
+            '%s\t%s' % (_('Enable all'), _('Alt+1')))
+        self.menuviewdisableall = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/clear_plot16.png'),
+            '%s\t%s' % (_('Disable all'), _('Alt+2')))
+        self.menuviewenableother = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/replot16.png'),
+            '%s\t%s' % (_('Enable non-selected'), _('Alt+3')))
+        self.menuviewdisableother = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/clear_plot16.png'),
+            '%s\t%s' % (_('Disable non-selected'), _('Alt+4')))
+
+        # Separator
+        self.menuview.addSeparator()
+        self.menuview_zoom_fit = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_fit32.png'),
+            '%s\t%s' % (_("Zoom Fit"), _('V')))
+        self.menuview_zoom_in = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_in32.png'),
+            '%s\t%s' % (_("Zoom In"), _('=')))
+        self.menuview_zoom_out = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_out32.png'),
+            '%s\t%s' % (_("Zoom Out"), _('-')))
+        self.menuview.addSeparator()
+
+        # Replot all
+        self.menuview_replot = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/replot32.png'),
+            '%s\t%s' % (_("Redraw All"), _('F5')))
+        self.menuview.addSeparator()
+
+        self.menuview_toggle_code_editor = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/code_editor32.png'),
+            '%s\t%s' % (_('Toggle Code Editor'), _('Shift+E')))
+        self.menuview.addSeparator()
+        self.menuview_toggle_fscreen = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/fscreen32.png'),
+            '%s\t%s' % (_("Toggle FullScreen"), _('Alt+F10')))
+        self.menuview_toggle_parea = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/plot32.png'),
+            '%s\t%s' % (_("Toggle Plot Area"), _('Ctrl+F10')))
+        self.menuview_toggle_notebook = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/notebook32.png'),
+            '%s\t%s' % (_("Toggle Project/Properties/Tool"), _('`')))
+
+        self.menuview.addSeparator()
+        self.menuview_toggle_grid = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/grid32.png'),
+            '%s\t%s' % (_("Toggle Grid Snap"), _('G')))
+        self.menuview_toggle_grid_lines = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/grid_lines32.png'),
+            '%s\t%s' % (_("Toggle Grid Lines"), _('Shift+G')))
+        self.menuview_toggle_axis = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/axis32.png'),
+            '%s\t%s' % (_("Toggle Axis"), _('Shift+A')))
+        self.menuview_toggle_workspace = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/workspace24.png'),
+            '%s\t%s' % (_("Toggle Workspace"), _('Shift+W')))
+        self.menuview_toggle_hud = self.menuview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/hud_32.png'),
+            '%s\t%s' % (_("Toggle HUD"), _('Shift+H')))
+
+        # ########################################################################
+        # ########################## Objects # ###################################
+        # ########################################################################
+        self.menuobjects = self.menu.addMenu(_('Objects'))
+        self.menuobjects.addSeparator()
+        self.menuobjects_selall = self.menuobjects.addAction(
+            QtGui.QIcon(self.app.resource_location + '/select_all.png'),
+            '%s\t%s' % (_('Select All'), ''))
+        self.menuobjects_unselall = self.menuobjects.addAction(
+            QtGui.QIcon(self.app.resource_location + '/deselect_all32.png'),
+            '%s\t%s' % (_('Deselect All'), ''))
+
+        # ########################################################################
+        # ########################## Tool # ######################################
+        # ########################################################################
+        self.menutool = QtWidgets.QMenu(_('Tool'))
+        self.menutoolaction = self.menu.addMenu(self.menutool)
+        self.menutoolshell = self.menutool.addAction(
+            QtGui.QIcon(self.app.resource_location + '/shell16.png'),
+            '%s\t%s' % (_('Command Line'), _('S')))
+
+        # ########################################################################
+        # ########################## Help # ######################################
+        # ########################################################################
+        self.menuhelp = self.menu.addMenu(_('Help'))
+        self.menuhelp_manual = self.menuhelp.addAction(
+            QtGui.QIcon(self.app.resource_location + '/globe16.png'),
+            '%s\t%s' % (_('Online Help'), _('F1')))
+
+        self.menuhelp_bookmarks = self.menuhelp.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/bookmarks16.png'), _('Bookmarks'))
+        self.menuhelp_bookmarks.addSeparator()
+        self.menuhelp_bookmarks_manager = self.menuhelp_bookmarks.addAction(
+            QtGui.QIcon(self.app.resource_location + '/bookmarks16.png'),
+            '%s\t%s' % (_('Bookmarks Manager'), ''))
+
+        self.menuhelp.addSeparator()
+        self.menuhelp_report_bug = self.menuhelp.addAction(
+            QtGui.QIcon(self.app.resource_location + '/bug16.png'),
+            '%s\t%s' % (_('Report a bug'), ''))
+        self.menuhelp.addSeparator()
+        self.menuhelp_exc_spec = self.menuhelp.addAction(
+            QtGui.QIcon(self.app.resource_location + '/pdf_link16.png'),
+            '%s\t%s' % (_('Excellon Specification'), ''))
+        self.menuhelp_gerber_spec = self.menuhelp.addAction(
+            QtGui.QIcon(self.app.resource_location + '/pdf_link16.png'),
+            '%s\t%s' % (_('Gerber Specification'), ''))
+
+        self.menuhelp.addSeparator()
+
+        self.menuhelp_shortcut_list = self.menuhelp.addAction(
+            QtGui.QIcon(self.app.resource_location + '/shortcuts24.png'),
+            '%s\t%s' % (_('Shortcuts List'), _('F3')))
+        self.menuhelp_videohelp = self.menuhelp.addAction(
+            QtGui.QIcon(self.app.resource_location + '/youtube32.png'),
+            '%s\t%s' % (_('YouTube Channel'), _('F4')))
+
+        self.menuhelp.addSeparator()
+
+        self.menuhelp_readme = self.menuhelp.addAction(
+            QtGui.QIcon(self.app.resource_location + '/warning.png'),
+            '%s\t%s' % (_("How To"), ''))
+
+        self.menuhelp_about = self.menuhelp.addAction(
+            QtGui.QIcon(self.app.resource_location + '/about32.png'),
+            '%s\t%s' % (_('About'), ''))
+
+        # ########################################################################
+        # ########################## GEOMETRY EDITOR # ###########################
+        # ########################################################################
+        self.geo_editor_menu = QtWidgets.QMenu('>%s<' % _('Geo Editor'))
+        self.menu.addMenu(self.geo_editor_menu)
+
+        self.geo_add_circle_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/circle32.png'),
+            '%s\t%s' % (_('Add Circle'), _('O'))
+        )
+        self.geo_add_arc_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/arc16.png'),
+            '%s\t%s' % (_('Add Arc'), _('A')))
+        self.geo_editor_menu.addSeparator()
+        self.geo_add_rectangle_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rectangle32.png'),
+            '%s\t%s' % (_('Add Rectangle'), _('R'))
+        )
+        self.geo_add_polygon_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/polygon32.png'),
+            '%s\t%s' % (_('Add Polygon'), _('N'))
+        )
+        self.geo_add_path_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/path32.png'),
+            '%s\t%s' % (_('Add Path'), _('P')))
+        self.geo_editor_menu.addSeparator()
+        self.geo_add_text_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/text32.png'),
+            '%s\t%s' % (_('Add Text'), _('T')))
+        self.geo_editor_menu.addSeparator()
+        self.geo_union_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/union16.png'),
+            '%s\t%s' % (_('Polygon Union'), _('U')))
+        self.geo_intersection_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/intersection16.png'),
+            '%s\t%s' % (_('Polygon Intersection'), _('E')))
+        self.geo_subtract_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/subtract16.png'),
+            '%s\t%s' % (_('Polygon Subtraction'), _('S'))
+        )
+        self.geo_editor_menu.addSeparator()
+        self.geo_cutpath_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/cutpath16.png'),
+            '%s\t%s' % (_('Cut Path'), _('X')))
+        # self.move_menuitem = self.menu.addAction(
+        #   QtGui.QIcon(self.app.resource_location + '/move16.png'), "Move Objects 'm'")
+        self.geo_copy_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy16.png'),
+            '%s\t%s' % (_("Copy Geom"), _('C')))
+        self.geo_delete_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/deleteshape16.png'),
+            '%s\t%s' % (_("Delete Shape"), _('DEL'))
+        )
+        self.geo_editor_menu.addSeparator()
+        self.geo_move_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'),
+            '%s\t%s' % (_("Move"), _('M')))
+        self.geo_buffer_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/buffer16.png'),
+            '%s\t%s' % (_("Buffer Tool"), _('B'))
+        )
+        self.geo_paint_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/paint16.png'),
+            '%s\t%s' % (_("Paint Tool"), _('I'))
+        )
+        self.geo_transform_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'),
+            '%s\t%s' % (_("Transform Tool"), _('Alt+R'))
+        )
+        self.geo_editor_menu.addSeparator()
+        self.geo_cornersnap_menuitem = self.geo_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/corner32.png'),
+            '%s\t%s' % (_("Toggle Corner Snap"), _('K'))
+        )
+
+        # ########################################################################
+        # ########################## EXCELLON Editor # ###########################
+        # ########################################################################
+        self.exc_editor_menu = QtWidgets.QMenu('>%s<' % _('Excellon Editor'))
+        self.menu.addMenu(self.exc_editor_menu)
+
+        self.exc_add_array_drill_menuitem = self.exc_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rectangle32.png'),
+            '%s\t%s' % (_('Add Drill Array'), _('A')))
+        self.exc_add_drill_menuitem = self.exc_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/plus16.png'),
+            '%s\t%s' % (_('Add Drill'), _('D')))
+        self.exc_editor_menu.addSeparator()
+
+        self.exc_add_array_slot_menuitem = self.exc_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/slot_array26.png'),
+            '%s\t%s' % (_('Add Slot Array'), _('Q')))
+        self.exc_add_slot_menuitem = self.exc_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/slot26.png'),
+            '%s\t%s' % (_('Add Slot'), _('W')))
+        self.exc_editor_menu.addSeparator()
+
+        self.exc_resize_drill_menuitem = self.exc_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/resize16.png'),
+            '%s\t%s' % (_('Resize Drill(S)'), _('R'))
+        )
+        self.exc_copy_drill_menuitem = self.exc_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy32.png'),
+            '%s\t%s' % (_('Copy'), _('C')))
+        self.exc_delete_drill_menuitem = self.exc_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/deleteshape32.png'),
+            '%s\t%s' % (_('Delete'), _('DEL'))
+        )
+        self.exc_editor_menu.addSeparator()
+
+        self.exc_move_drill_menuitem = self.exc_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'),
+            '%s\t%s' % (_('Move Drill'), _('M')))
+
+        # ########################################################################
+        # ########################## GERBER Editor # #############################
+        # ########################################################################
+        self.grb_editor_menu = QtWidgets.QMenu('>%s<' % _('Gerber Editor'))
+        self.menu.addMenu(self.grb_editor_menu)
+
+        self.grb_add_pad_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/aperture16.png'),
+            '%s\t%s' % (_('Add Pad'), _('P')))
+        self.grb_add_pad_array_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/padarray32.png'),
+            '%s\t%s' % (_('Add Pad Array'), _('A')))
+        self.grb_add_track_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/track32.png'),
+            '%s\t%s' % (_('Add Track'), _('T')))
+        self.grb_add_region_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rectangle32.png'),
+            '%s\t%s' % (_('Add Region'), _('N')))
+        self.grb_editor_menu.addSeparator()
+
+        self.grb_convert_poly_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/poligonize32.png'),
+            '%s\t%s' % (_("Poligonize"), _('Alt+N')))
+        self.grb_add_semidisc_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/semidisc32.png'),
+            '%s\t%s' % (_("Add SemiDisc"), _('E')))
+        self.grb_add_disc_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/disc32.png'),
+            '%s\t%s' % (_("Add Disc"), _('D')))
+        self.grb_add_buffer_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'),
+            '%s\t%s' % (_('Buffer'), _('B')))
+        self.grb_add_scale_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/scale32.png'),
+            '%s\t%s' % (_('Scale'), _('S')))
+        self.grb_add_markarea_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/markarea32.png'),
+            '%s\t%s' % (_('Mark Area'), _('Alt+A')))
+        self.grb_add_eraser_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/eraser26.png'),
+            '%s\t%s' % (_('Eraser'), _('Ctrl+E')))
+        self.grb_transform_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'),
+            '%s\t%s' % (_("Transform"), _('Alt+R')))
+        self.grb_editor_menu.addSeparator()
+
+        self.grb_copy_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy32.png'),
+            '%s\t%s' % (_('Copy'), _('C')))
+        self.grb_delete_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/deleteshape32.png'),
+            '%s\t%s' % (_('Delete'), _('DEL')))
+        self.grb_editor_menu.addSeparator()
+
+        self.grb_move_menuitem = self.grb_editor_menu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'),
+            '%s\t%s' % (_('Move'), _('M')))
+
+        self.grb_editor_menu.menuAction().setVisible(False)
+        self.grb_editor_menu.setDisabled(True)
+
+        self.geo_editor_menu.menuAction().setVisible(False)
+        self.geo_editor_menu.setDisabled(True)
+
+        self.exc_editor_menu.menuAction().setVisible(False)
+        self.exc_editor_menu.setDisabled(True)
+
+        # ########################################################################
+        # ########################## Project Tab Context Menu # ##################
+        # ########################################################################
+        self.menuproject = QtWidgets.QMenu()
+
+        self.menuprojectenable = self.menuproject.addAction(
+            QtGui.QIcon(self.app.resource_location + '/replot32.png'), _('Enable Plot'))
+        self.menuprojectdisable = self.menuproject.addAction(
+            QtGui.QIcon(self.app.resource_location + '/clear_plot32.png'), _('Disable Plot'))
+        self.menuproject.addSeparator()
+
+        self.menuprojectcolor = self.menuproject.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/set_color32.png'), _('Set Color'))
+
+        self.menuproject_red = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/red32.png'), _('Red'))
+
+        self.menuproject_blue = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/blue32.png'), _('Blue'))
+
+        self.menuproject_yellow = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/yellow32.png'), _('Yellow'))
+
+        self.menuproject_green = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/green32.png'), _('Green'))
+
+        self.menuproject_purple = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/violet32.png'), _('Purple'))
+
+        self.menuproject_brown = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/brown32.png'), _('Brown'))
+
+        self.menuproject_brown = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/white32.png'), _('White'))
+
+        self.menuproject_brown = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/black32.png'), _('Black'))
+
+        self.menuprojectcolor.addSeparator()
+
+        self.menuproject_custom = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/set_color32.png'), _('Custom'))
+
+        self.menuprojectcolor.addSeparator()
+
+        self.menuproject_custom = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/set_color32.png'), _('Opacity'))
+
+        self.menuproject_custom = self.menuprojectcolor.addAction(
+            QtGui.QIcon(self.app.resource_location + '/set_color32.png'), _('Default'))
+
+        self.menuproject.addSeparator()
+
+        self.menuprojectgeneratecnc = self.menuproject.addAction(
+            QtGui.QIcon(self.app.resource_location + '/cnc32.png'), _('Create CNCJob'))
+        self.menuprojectviewsource = self.menuproject.addAction(
+            QtGui.QIcon(self.app.resource_location + '/source32.png'), _('View Source'))
+
+        self.menuprojectedit = self.menuproject.addAction(
+            QtGui.QIcon(self.app.resource_location + '/edit_ok32.png'), _('Edit'))
+        self.menuprojectcopy = self.menuproject.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy32.png'), _('Copy'))
+        self.menuprojectdelete = self.menuproject.addAction(
+            QtGui.QIcon(self.app.resource_location + '/delete32.png'), _('Delete'))
+        self.menuprojectsave = self.menuproject.addAction(
+            QtGui.QIcon(self.app.resource_location + '/save_as.png'), _('Save'))
+        self.menuproject.addSeparator()
+
+        self.menuprojectproperties = self.menuproject.addAction(
+            QtGui.QIcon(self.app.resource_location + '/properties32.png'), _('Properties'))
+
+        # ########################################################################
+        # ####################### Central Widget -> Splitter # ##################
+        # ########################################################################
+
+        # IMPORTANT #
+        # 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)
+
+        # self.notebook = QtWidgets.QTabWidget()
+        self.notebook = FCDetachableTab(protect=True, parent=self)
+        self.notebook.setTabsClosable(False)
+        self.notebook.useOldIndex(True)
+
+        self.splitter.addWidget(self.notebook)
+
+        self.splitter_left = QtWidgets.QSplitter(Qt.Vertical)
+        self.splitter.addWidget(self.splitter_left)
+        self.splitter_left.addWidget(self.notebook)
+        self.splitter_left.setHandleWidth(0)
+
+        # ########################################################################
+        # ########################## ToolBAR # ###################################
+        # ########################################################################
+
+        # ## TOOLBAR INSTALLATION ###
+        self.toolbarfile = QtWidgets.QToolBar(_('File Toolbar'))
+        self.toolbarfile.setObjectName('File_TB')
+        self.addToolBar(self.toolbarfile)
+
+        self.toolbaredit = QtWidgets.QToolBar(_('Edit Toolbar'))
+        self.toolbaredit.setObjectName('Edit_TB')
+        self.addToolBar(self.toolbaredit)
+
+        self.toolbarview = QtWidgets.QToolBar(_('View Toolbar'))
+        self.toolbarview.setObjectName('View_TB')
+        self.addToolBar(self.toolbarview)
+
+        self.toolbarshell = QtWidgets.QToolBar(_('Shell Toolbar'))
+        self.toolbarshell.setObjectName('Shell_TB')
+        self.addToolBar(self.toolbarshell)
+
+        self.toolbartools = QtWidgets.QToolBar(_('Tools Toolbar'))
+        self.toolbartools.setObjectName('Tools_TB')
+        self.addToolBar(self.toolbartools)
+
+        self.exc_edit_toolbar = QtWidgets.QToolBar(_('Excellon Editor Toolbar'))
+        self.exc_edit_toolbar.setObjectName('ExcEditor_TB')
+        self.addToolBar(self.exc_edit_toolbar)
+
+        self.addToolBarBreak()
+
+        self.geo_edit_toolbar = QtWidgets.QToolBar(_('Geometry Editor Toolbar'))
+        self.geo_edit_toolbar.setObjectName('GeoEditor_TB')
+        self.addToolBar(self.geo_edit_toolbar)
+
+        self.grb_edit_toolbar = QtWidgets.QToolBar(_('Gerber Editor Toolbar'))
+        self.grb_edit_toolbar.setObjectName('GrbEditor_TB')
+        self.addToolBar(self.grb_edit_toolbar)
+
+        # ### INFOBAR TOOLBARS ###################################################
+        self.delta_coords_toolbar = QtWidgets.QToolBar(_('Delta Coordinates Toolbar'))
+        self.delta_coords_toolbar.setObjectName('Delta_Coords_TB')
+
+        self.coords_toolbar = QtWidgets.QToolBar(_('Coordinates Toolbar'))
+        self.coords_toolbar.setObjectName('Coords_TB')
+
+        self.grid_toolbar = QtWidgets.QToolBar(_('Grid Toolbar'))
+        self.grid_toolbar.setObjectName('Snap_TB')
+        self.grid_toolbar.setStyleSheet(
+            """
+            QToolBar { padding: 0; }
+            QToolBar QToolButton { padding: -2; margin: -2; }
+            """
+        )
+
+        self.status_toolbar = QtWidgets.QToolBar(_('Status Toolbar'))
+        self.status_toolbar.setStyleSheet(
+            """
+            QToolBar { padding: 0; }
+            QToolBar QToolButton { padding: -2; margin: -2; }
+            """
+        )
+
+        # ########################################################################
+        # ########################## File Toolbar# ###############################
+        # ########################################################################
+        self.file_open_gerber_btn = self.toolbarfile.addAction(
+            QtGui.QIcon(self.app.resource_location + '/flatcam_icon32.png'), _("Open Gerber"))
+        self.file_open_excellon_btn = self.toolbarfile.addAction(
+            QtGui.QIcon(self.app.resource_location + '/drill32.png'), _("Open Excellon"))
+        self.toolbarfile.addSeparator()
+        self.file_open_btn = self.toolbarfile.addAction(
+            QtGui.QIcon(self.app.resource_location + '/folder32.png'), _("Open Project"))
+        self.file_save_btn = self.toolbarfile.addAction(
+            QtGui.QIcon(self.app.resource_location + '/project_save32.png'), _("Save project"))
+
+        # ########################################################################
+        # ########################## Edit Toolbar# ###############################
+        # ########################################################################
+        self.editgeo_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/edit_file32.png'), _("Editor"))
+        self.update_obj_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/close_edit_file32.png'), _("Save Object and close the Editor")
+        )
+
+        self.toolbaredit.addSeparator()
+        self.copy_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy"))
+        self.delete_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("Delete"))
+        self.toolbaredit.addSeparator()
+        self.distance_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/distance32.png'), _("Distance Tool"))
+        self.distance_min_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/distance_min32.png'), _("Distance Min Tool"))
+        self.origin_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/origin32.png'), _('Set Origin'))
+        self.move2origin_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/origin2_32.png'), _('Move to Origin'))
+
+        self.jmp_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location'))
+        self.locate_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/locate32.png'), _('Locate in Object'))
+
+        # ########################################################################
+        # ########################## View Toolbar# ###############################
+        # ########################################################################
+        self.replot_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/replot32.png'), _("Replot"))
+        self.clear_plot_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/clear_plot32.png'), _("Clear Plot"))
+        self.zoom_in_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_in32.png'), _("Zoom In"))
+        self.zoom_out_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_out32.png'), _("Zoom Out"))
+        self.zoom_fit_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_fit32.png'), _("Zoom Fit"))
+
+        # self.toolbarview.setVisible(False)
+
+        # ########################################################################
+        # ########################## Shell Toolbar# ##############################
+        # ########################################################################
+        self.shell_btn = self.toolbarshell.addAction(
+            QtGui.QIcon(self.app.resource_location + '/shell32.png'), _("Command Line"))
+        self.new_script_btn = self.toolbarshell.addAction(
+            QtGui.QIcon(self.app.resource_location + '/script_new24.png'), '%s ...' % _('New Script'))
+        self.open_script_btn = self.toolbarshell.addAction(
+            QtGui.QIcon(self.app.resource_location + '/open_script32.png'), '%s ...' % _('Open Script'))
+        self.run_script_btn = self.toolbarshell.addAction(
+            QtGui.QIcon(self.app.resource_location + '/script16.png'), '%s ...' % _('Run Script'))
+
+        # ########################################################################
+        # ########################## Tools Toolbar# ##############################
+        # ########################################################################
+        self.dblsided_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/doubleside32.png'), _("2-Sided Tool"))
+        self.align_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/align32.png'), _("Align Objects Tool"))
+        self.extract_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/extract_drill32.png'), _("Extract Drills Tool"))
+
+        self.cutout_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("Cutout Tool"))
+        self.ncc_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/ncc16.png'), _("NCC Tool"))
+        self.paint_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/paint20_1.png'), _("Paint Tool"))
+        self.isolation_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/iso_16.png'), _("Isolation Tool"))
+        self.drill_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/drilling_tool32.png'), _("Drilling Tool"))
+        self.toolbartools.addSeparator()
+
+        self.panelize_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/panelize32.png'), _("Panel Tool"))
+        self.film_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/film16.png'), _("Film Tool"))
+        self.solder_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/solderpastebis32.png'), _("SolderPaste Tool"))
+        self.sub_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/sub32.png'), _("Subtract Tool"))
+        self.rules_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rules32.png'), _("Rules Tool"))
+        self.optimal_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/open_excellon32.png'), _("Optimal Tool"))
+
+        self.toolbartools.addSeparator()
+
+        self.calculators_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/calculator24.png'), _("Calculators Tool"))
+        self.transform_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'), _("Transform Tool"))
+        self.qrcode_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/qrcode32.png'), _("QRCode Tool"))
+        self.copperfill_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copperfill32.png'), _("Copper Thieving Tool"))
+
+        self.fiducials_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/fiducials_32.png'), _("Fiducials Tool"))
+        self.cal_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/calibrate_32.png'), _("Calibration Tool"))
+        self.punch_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/punch32.png'), _("Punch Gerber Tool"))
+        self.invert_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/invert32.png'), _("Invert Gerber Tool"))
+        self.corners_tool_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/corners_32.png'), _("Corner Markers Tool"))
+        self.etch_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/etch_32.png'), _("Etch Compensation Tool"))
+
+        # ########################################################################
+        # ########################## Excellon Editor Toolbar# ####################
+        # ########################################################################
+        self.select_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/pointer32.png'), _("Select"))
+        self.add_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/plus16.png'), _('Add Drill'))
+        self.add_drill_array_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/addarray16.png'), _('Add Drill Array'))
+        self.add_slot_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/slot26.png'), _('Add Slot'))
+        self.add_slot_array_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/slot_array26.png'), _('Add Slot Array'))
+        self.resize_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/resize16.png'), _('Resize Drill'))
+        self.exc_edit_toolbar.addSeparator()
+
+        self.copy_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy32.png'), _('Copy Drill'))
+        self.delete_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("Delete Drill"))
+
+        self.exc_edit_toolbar.addSeparator()
+        self.move_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'), _("Move Drill"))
+
+        # ########################################################################
+        # ########################## Geometry Editor Toolbar# ####################
+        # ########################################################################
+        self.geo_select_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/pointer32.png'), _("Select"))
+        self.geo_add_circle_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/circle32.png'), _('Add Circle'))
+        self.geo_add_arc_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/arc32.png'), _('Add Arc'))
+        self.geo_add_rectangle_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rectangle32.png'), _('Add Rectangle'))
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_add_path_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/path32.png'), _('Add Path'))
+        self.geo_add_polygon_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/polygon32.png'), _('Add Polygon'))
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_add_text_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/text32.png'), _('Add Text'))
+        self.geo_add_buffer_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'), _('Add Buffer'))
+        self.geo_add_paint_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/paint20_1.png'), _('Paint Shape'))
+        self.geo_eraser_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/eraser26.png'), _('Eraser'))
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_union_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/union32.png'), _('Polygon Union'))
+        self.geo_explode_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/explode32.png'), _('Polygon Explode'))
+
+        self.geo_intersection_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/intersection32.png'), _('Polygon Intersection'))
+        self.geo_subtract_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/subtract32.png'), _('Polygon Subtraction'))
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_cutpath_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/cutpath32.png'), _('Cut Path'))
+        self.geo_copy_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy32.png'), _("Copy Shape(s)"))
+
+        self.geo_delete_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("Delete Shape"))
+        self.geo_transform_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'), _("Transformations"))
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_move_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'), _("Move Objects"))
+
+        # ########################################################################
+        # ########################## Gerber Editor Toolbar# ######################
+        # ########################################################################
+        self.grb_select_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/pointer32.png'), _("Select"))
+        self.grb_add_pad_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/aperture32.png'), _("Add Pad"))
+        self.add_pad_ar_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/padarray32.png'), _('Add Pad Array'))
+        self.grb_add_track_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/track32.png'), _("Add Track"))
+        self.grb_add_region_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/polygon32.png'), _("Add Region"))
+        self.grb_convert_poly_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/poligonize32.png'), _("Poligonize"))
+
+        self.grb_add_semidisc_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/semidisc32.png'), _("SemiDisc"))
+        self.grb_add_disc_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/disc32.png'), _("Disc"))
+        self.grb_edit_toolbar.addSeparator()
+
+        self.aperture_buffer_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'), _('Buffer'))
+        self.aperture_scale_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/scale32.png'), _('Scale'))
+        self.aperture_markarea_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/markarea32.png'), _('Mark Area'))
+
+        self.aperture_eraser_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/eraser26.png'), _('Eraser'))
+
+        self.grb_edit_toolbar.addSeparator()
+        self.aperture_copy_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy32.png'), _("Copy"))
+        self.aperture_delete_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("Delete"))
+        self.grb_transform_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'), _("Transformations"))
+        self.grb_edit_toolbar.addSeparator()
+        self.aperture_move_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'), _("Move"))
+
+        # ########################################################################
+        # ########################## GRID Toolbar# ###############################
+        # ########################################################################
+
+        # Snap GRID toolbar is always active to facilitate usage of measurements done on GRID
+        self.grid_snap_btn = self.grid_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/grid32.png'), _('Snap to grid'))
+        self.grid_gap_x_entry = FCEntry2()
+        self.grid_gap_x_entry.setMaximumWidth(70)
+        self.grid_gap_x_entry.setToolTip(_("Grid X snapping distance"))
+        self.grid_toolbar.addWidget(self.grid_gap_x_entry)
+
+        self.grid_toolbar.addWidget(FCLabel(" "))
+        self.grid_gap_link_cb = FCCheckBox()
+        self.grid_gap_link_cb.setToolTip(_("When active, value on Grid_X\n"
+                                           "is copied to the Grid_Y value."))
+        self.grid_toolbar.addWidget(self.grid_gap_link_cb)
+        self.grid_toolbar.addWidget(FCLabel(" "))
+
+        self.grid_gap_y_entry = FCEntry2()
+        self.grid_gap_y_entry.setMaximumWidth(70)
+        self.grid_gap_y_entry.setToolTip(_("Grid Y snapping distance"))
+        self.grid_toolbar.addWidget(self.grid_gap_y_entry)
+        self.grid_toolbar.addWidget(FCLabel(" "))
+
+        self.ois_grid = OptionalInputSection(self.grid_gap_link_cb, [self.grid_gap_y_entry], logic=False)
+
+        self.corner_snap_btn = self.grid_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/corner32.png'), _('Snap to corner'))
+
+        self.snap_max_dist_entry = FCEntry()
+        self.snap_max_dist_entry.setMaximumWidth(70)
+        self.snap_max_dist_entry.setToolTip(_("Max. magnet distance"))
+        self.snap_magnet = self.grid_toolbar.addWidget(self.snap_max_dist_entry)
+
+        self.corner_snap_btn.setVisible(False)
+        self.snap_magnet.setVisible(False)
+
+        # ########################################################################
+        # ########################## Status Toolbar ##############################
+        # ########################################################################
+        self.axis_status_label = FCLabel()
+        self.axis_status_label.setToolTip(_("Toggle the display of axis on canvas"))
+        self.axis_status_label.setPixmap(QtGui.QPixmap(self.app.resource_location + '/axis16.png'))
+        self.status_toolbar.addWidget(self.axis_status_label)
+        self.status_toolbar.addWidget(FCLabel(" "))
+
+        self.pref_status_label = FCLabel()
+        self.pref_status_label.setToolTip(_("Preferences"))
+        self.pref_status_label.setPixmap(QtGui.QPixmap(self.app.resource_location + '/settings18.png'))
+        self.status_toolbar.addWidget(self.pref_status_label)
+        self.status_toolbar.addWidget(FCLabel(" "))
+
+        self.shell_status_label = FCLabel()
+        self.shell_status_label.setToolTip(_("Command Line"))
+        self.shell_status_label.setPixmap(QtGui.QPixmap(self.app.resource_location + '/shell20.png'))
+        self.status_toolbar.addWidget(self.shell_status_label)
+        self.status_toolbar.addWidget(FCLabel(" "))
+
+        self.hud_label = FCLabel()
+        self.hud_label.setToolTip(_("HUD (Heads up display)"))
+        self.hud_label.setPixmap(QtGui.QPixmap(self.app.resource_location + '/hud16.png'))
+        self.status_toolbar.addWidget(self.hud_label)
+        self.status_toolbar.addWidget(FCLabel(" "))
+
+        self.wplace_label = FCLabel("A4")
+        self.wplace_label.setToolTip(_("Draw a delimiting rectangle on canvas.\n"
+                                       "The purpose is to illustrate the limits for our work.")
+                                     )
+        self.wplace_label.setMargin(2)
+        self.status_toolbar.addWidget(self.wplace_label)
+        self.status_toolbar.addWidget(FCLabel(" "))
+
+        # #######################################################################
+        # ####################### Delta Coordinates TOOLBAR #####################
+        # #######################################################################
+        self.rel_position_label = FCLabel(
+            "<b>Dx</b>: 0.0000&nbsp;&nbsp;   <b>Dy</b>: 0.0000&nbsp;&nbsp;&nbsp;&nbsp;")
+        self.rel_position_label.setMinimumWidth(110)
+        self.rel_position_label.setToolTip(_("Relative measurement.\nReference is last click position"))
+        self.delta_coords_toolbar.addWidget(self.rel_position_label)
+
+        # #######################################################################
+        # ####################### Coordinates TOOLBAR ###########################
+        # #######################################################################
+        self.position_label = FCLabel("&nbsp;<b>X</b>: 0.0000&nbsp;&nbsp;   <b>Y</b>: 0.0000&nbsp;")
+        self.position_label.setMinimumWidth(110)
+        self.position_label.setToolTip(_("Absolute measurement.\n"
+                                         "Reference is (X=0, Y= 0) position"))
+        self.coords_toolbar.addWidget(self.position_label)
+
+        # #######################################################################
+        # ####################### TCL Shell DOCK ################################
+        # #######################################################################
+        self.shell_dock = FCDock(_("TCL Shell"), close_callback=self.toggle_shell_ui)
+        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)
+
+        # ########################################################################
+        # ########################## Notebook # ##################################
+        # ########################################################################
+
+        # ########################################################################
+        # ########################## PROJECT Tab # ###############################
+        # ########################################################################
+        self.project_tab = QtWidgets.QWidget()
+        self.project_tab.setObjectName("project_tab")
+
+        self.project_frame_lay = QtWidgets.QVBoxLayout(self.project_tab)
+        self.project_frame_lay.setContentsMargins(0, 0, 0, 0)
+
+        self.project_frame = QtWidgets.QFrame()
+        self.project_frame.setContentsMargins(0, 0, 0, 0)
+        self.project_frame_lay.addWidget(self.project_frame)
+
+        self.project_tab_layout = QtWidgets.QVBoxLayout(self.project_frame)
+        self.project_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.notebook.addTab(self.project_tab, _("Project"))
+        self.project_frame.setDisabled(False)
+
+        # ########################################################################
+        # ########################## SELECTED Tab # ##############################
+        # ########################################################################
+        self.properties_tab = QtWidgets.QWidget()
+        # self.properties_tab.setMinimumWidth(270)
+        self.properties_tab.setObjectName("properties_tab")
+        self.properties_tab_layout = QtWidgets.QVBoxLayout(self.properties_tab)
+        self.properties_tab_layout.setContentsMargins(2, 2, 2, 2)
+
+        self.properties_scroll_area = VerticalScrollArea()
+        # self.properties_scroll_area.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+        self.properties_tab_layout.addWidget(self.properties_scroll_area)
+        self.notebook.addTab(self.properties_tab, _("Properties"))
+
+        # ########################################################################
+        # ########################## TOOL Tab # ##################################
+        # ########################################################################
+        self.tool_tab = QtWidgets.QWidget()
+        self.tool_tab.setObjectName("tool_tab")
+        self.tool_tab_layout = QtWidgets.QVBoxLayout(self.tool_tab)
+        self.tool_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.notebook.addTab(self.tool_tab, _("Tool"))
+        self.tool_scroll_area = VerticalScrollArea()
+        # self.tool_scroll_area.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+        self.tool_tab_layout.addWidget(self.tool_scroll_area)
+
+        # ########################################################################
+        # ########################## RIGHT Widget # ##############################
+        # ########################################################################
+        self.right_widget = QtWidgets.QWidget()
+        self.right_widget.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
+        self.splitter.addWidget(self.right_widget)
+
+        self.right_lay = QtWidgets.QVBoxLayout()
+        self.right_lay.setContentsMargins(0, 0, 0, 0)
+        self.right_widget.setLayout(self.right_lay)
+
+        # ########################################################################
+        # ########################## PLOT AREA Tab # #############################
+        # ########################################################################
+        self.plot_tab_area = FCDetachableTab2(protect=False, protect_by_name=[_('Plot Area')], parent=self)
+        self.plot_tab_area.useOldIndex(True)
+
+        self.right_lay.addWidget(self.plot_tab_area)
+        self.plot_tab_area.setTabsClosable(True)
+
+        self.plot_tab = QtWidgets.QWidget()
+        self.plot_tab.setObjectName("plotarea_tab")
+        self.plot_tab_area.addTab(self.plot_tab, _("Plot Area"))
+
+        self.right_layout = QtWidgets.QVBoxLayout()
+        self.right_layout.setObjectName("right_layout")
+        self.right_layout.setContentsMargins(2, 2, 2, 2)
+        self.plot_tab.setLayout(self.right_layout)
+
+        # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON
+        self.plot_tab_area.protectTab(0)
+
+        # ########################################################################
+        # ########################## PREFERENCES AREA Tab # ######################
+        # ########################################################################
+        self.preferences_tab = QtWidgets.QWidget()
+        self.preferences_tab.setObjectName("preferences_tab")
+        self.pref_tab_layout = QtWidgets.QVBoxLayout(self.preferences_tab)
+        self.pref_tab_layout.setContentsMargins(2, 2, 2, 2)
+
+        self.pref_tab_area = FCTab()
+        self.pref_tab_area.setTabsClosable(False)
+        self.pref_tab_area_tabBar = self.pref_tab_area.tabBar()
+        self.pref_tab_area_tabBar.setStyleSheet("QTabBar::tab{min-width:90px;}")
+        self.pref_tab_area_tabBar.setExpanding(True)
+        self.pref_tab_layout.addWidget(self.pref_tab_area)
+
+        self.general_tab = QtWidgets.QWidget()
+        self.general_tab.setObjectName("general_tab")
+        self.pref_tab_area.addTab(self.general_tab, _("General"))
+        self.general_tab_lay = QtWidgets.QVBoxLayout()
+        self.general_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.general_tab.setLayout(self.general_tab_lay)
+
+        self.hlay1 = QtWidgets.QHBoxLayout()
+        self.general_tab_lay.addLayout(self.hlay1)
+
+        self.hlay1.addStretch()
+
+        self.general_scroll_area = QtWidgets.QScrollArea()
+        self.general_tab_lay.addWidget(self.general_scroll_area)
+
+        self.gerber_tab = QtWidgets.QWidget()
+        self.gerber_tab.setObjectName("gerber_tab")
+        self.pref_tab_area.addTab(self.gerber_tab, _("GERBER"))
+        self.gerber_tab_lay = QtWidgets.QVBoxLayout()
+        self.gerber_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.gerber_tab.setLayout(self.gerber_tab_lay)
+
+        self.gerber_scroll_area = QtWidgets.QScrollArea()
+        self.gerber_tab_lay.addWidget(self.gerber_scroll_area)
+
+        self.excellon_tab = QtWidgets.QWidget()
+        self.excellon_tab.setObjectName("excellon_tab")
+        self.pref_tab_area.addTab(self.excellon_tab, _("EXCELLON"))
+        self.excellon_tab_lay = QtWidgets.QVBoxLayout()
+        self.excellon_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.excellon_tab.setLayout(self.excellon_tab_lay)
+
+        self.excellon_scroll_area = QtWidgets.QScrollArea()
+        self.excellon_tab_lay.addWidget(self.excellon_scroll_area)
+
+        self.geometry_tab = QtWidgets.QWidget()
+        self.geometry_tab.setObjectName("geometry_tab")
+        self.pref_tab_area.addTab(self.geometry_tab, _("GEOMETRY"))
+        self.geometry_tab_lay = QtWidgets.QVBoxLayout()
+        self.geometry_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.geometry_tab.setLayout(self.geometry_tab_lay)
+
+        self.geometry_scroll_area = QtWidgets.QScrollArea()
+        self.geometry_tab_lay.addWidget(self.geometry_scroll_area)
+
+        self.text_editor_tab = QtWidgets.QWidget()
+        self.text_editor_tab.setObjectName("text_editor_tab")
+        self.pref_tab_area.addTab(self.text_editor_tab, _("CNC-JOB"))
+        self.cncjob_tab_lay = QtWidgets.QVBoxLayout()
+        self.cncjob_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.text_editor_tab.setLayout(self.cncjob_tab_lay)
+
+        self.cncjob_scroll_area = QtWidgets.QScrollArea()
+        self.cncjob_tab_lay.addWidget(self.cncjob_scroll_area)
+
+        self.tools_tab = QtWidgets.QWidget()
+        self.pref_tab_area.addTab(self.tools_tab, _("TOOLS"))
+        self.tools_tab_lay = QtWidgets.QVBoxLayout()
+        self.tools_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.tools_tab.setLayout(self.tools_tab_lay)
+
+        self.tools_scroll_area = QtWidgets.QScrollArea()
+        self.tools_tab_lay.addWidget(self.tools_scroll_area)
+
+        self.tools2_tab = QtWidgets.QWidget()
+        self.pref_tab_area.addTab(self.tools2_tab, _("TOOLS 2"))
+        self.tools2_tab_lay = QtWidgets.QVBoxLayout()
+        self.tools2_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.tools2_tab.setLayout(self.tools2_tab_lay)
+
+        self.tools2_scroll_area = QtWidgets.QScrollArea()
+        self.tools2_tab_lay.addWidget(self.tools2_scroll_area)
+
+        self.fa_tab = QtWidgets.QWidget()
+        self.fa_tab.setObjectName("fa_tab")
+        self.pref_tab_area.addTab(self.fa_tab, _("UTILITIES"))
+        self.fa_tab_lay = QtWidgets.QVBoxLayout()
+        self.fa_tab_lay.setContentsMargins(2, 2, 2, 2)
+        self.fa_tab.setLayout(self.fa_tab_lay)
+
+        self.fa_scroll_area = QtWidgets.QScrollArea()
+        self.fa_tab_lay.addWidget(self.fa_scroll_area)
+
+        self.pref_tab_bottom_layout = QtWidgets.QHBoxLayout()
+        self.pref_tab_bottom_layout.setAlignment(QtCore.Qt.AlignVCenter)
+        self.pref_tab_layout.addLayout(self.pref_tab_bottom_layout)
+
+        self.pref_tab_bottom_layout_1 = QtWidgets.QHBoxLayout()
+        self.pref_tab_bottom_layout_1.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.pref_tab_bottom_layout.addLayout(self.pref_tab_bottom_layout_1)
+
+        self.pref_defaults_button = FCButton(_("Restore Defaults"))
+        self.pref_defaults_button.setIcon(QtGui.QIcon(self.app.resource_location + '/restore32.png'))
+        self.pref_defaults_button.setMinimumWidth(130)
+        self.pref_defaults_button.setToolTip(
+            _("Restore the entire set of default values\n"
+              "to the initial values loaded after first launch."))
+        self.pref_tab_bottom_layout_1.addWidget(self.pref_defaults_button)
+
+        self.pref_open_button = FCButton()
+        self.pref_open_button.setText(_("Open Pref Folder"))
+        self.pref_open_button.setIcon(QtGui.QIcon(self.app.resource_location + '/pref.png'))
+        self.pref_open_button.setMinimumWidth(130)
+        self.pref_open_button.setToolTip(
+            _("Open the folder where FlatCAM save the preferences files."))
+        self.pref_tab_bottom_layout_1.addWidget(self.pref_open_button)
+
+        # Clear Settings
+        self.clear_btn = FCButton('%s' % _('Clear GUI Settings'))
+        self.clear_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/trash32.png'))
+        self.clear_btn.setMinimumWidth(130)
+
+        self.clear_btn.setToolTip(
+            _("Clear the GUI settings for FlatCAM,\n"
+              "such as: layout, gui state, style, hdpi support etc.")
+        )
+
+        self.pref_tab_bottom_layout_1.addWidget(self.clear_btn)
+
+        self.pref_tab_bottom_layout_2 = QtWidgets.QHBoxLayout()
+        self.pref_tab_bottom_layout_2.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.pref_tab_bottom_layout.addLayout(self.pref_tab_bottom_layout_2)
+
+        self.pref_apply_button = FCButton()
+        self.pref_apply_button.setIcon(QtGui.QIcon(self.app.resource_location + '/apply32.png'))
+        self.pref_apply_button.setText(_("Apply"))
+        self.pref_apply_button.setMinimumWidth(130)
+        self.pref_apply_button.setToolTip(
+            _("Apply the current preferences without saving to a file."))
+        self.pref_tab_bottom_layout_2.addWidget(self.pref_apply_button)
+
+        self.pref_save_button = FCButton()
+        self.pref_save_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.pref_save_button.setText(_("Save"))
+        self.pref_save_button.setMinimumWidth(130)
+        self.pref_save_button.setToolTip(
+            _("Save the current settings in the 'current_defaults' file\n"
+              "which is the file storing the working default preferences."))
+        self.pref_tab_bottom_layout_2.addWidget(self.pref_save_button)
+
+        self.pref_close_button = FCButton()
+        self.pref_close_button.setText(_("Cancel"))
+        self.pref_close_button.setMinimumWidth(130)
+        self.pref_close_button.setToolTip(
+            _("Will not save the changes and will close the preferences window."))
+        self.pref_tab_bottom_layout_2.addWidget(self.pref_close_button)
+
+        # ########################################################################
+        # #################### SHORTCUT LIST AREA Tab # ##########################
+        # ########################################################################
+        self.shortcuts_tab = ShortcutsTab()
+
+        # ########################################################################
+        # ########################## PLOT AREA CONTEXT MENU  # ###################
+        # ########################################################################
+        self.popMenu = FCMenu()
+
+        self.popmenu_disable = self.popMenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/disable32.png'), _("Toggle Visibility"))
+        self.popmenu_panel_toggle = self.popMenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/notebook16.png'), _("Toggle Panel"))
+
+        self.popMenu.addSeparator()
+        self.cmenu_newmenu = self.popMenu.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/file32.png'), _("New"))
+        self.popmenu_new_geo = self.cmenu_newmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/new_file_geo16.png'), _("Geometry"))
+        self.popmenu_new_grb = self.cmenu_newmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/new_file_grb16.png'), "Gerber")
+        self.popmenu_new_exc = self.cmenu_newmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/new_file_exc16.png'), _("Excellon"))
+        self.cmenu_newmenu.addSeparator()
+        self.popmenu_new_prj = self.cmenu_newmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/file16.png'), _("Project"))
+        self.popMenu.addSeparator()
+
+        self.cmenu_gridmenu = self.popMenu.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/grid32_menu.png'), _("Grids"))
+
+        self.cmenu_viewmenu = self.popMenu.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/view64.png'), _("View"))
+        self.zoomfit = self.cmenu_viewmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_fit32.png'), _("Zoom Fit"))
+        self.clearplot = self.cmenu_viewmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/clear_plot32.png'), _("Clear Plot"))
+        self.replot = self.cmenu_viewmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/replot32.png'), _("Replot"))
+        self.popMenu.addSeparator()
+
+        self.g_editor_cmenu = self.popMenu.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/draw32.png'), _("Geo Editor"))
+        self.draw_line = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/path32.png'), _("Path"))
+        self.draw_rect = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rectangle32.png'), _("Rectangle"))
+        self.g_editor_cmenu.addSeparator()
+        self.draw_circle = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/circle32.png'), _("Circle"))
+        self.draw_poly = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/polygon32.png'), _("Polygon"))
+        self.draw_arc = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/arc32.png'), _("Arc"))
+        self.g_editor_cmenu.addSeparator()
+
+        self.draw_text = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/text32.png'), _("Text"))
+        self.draw_buffer = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'), _("Buffer"))
+        self.draw_paint = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/paint20_1.png'), _("Paint"))
+        self.draw_eraser = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/eraser26.png'), _("Eraser"))
+        self.g_editor_cmenu.addSeparator()
+
+        self.draw_union = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/union32.png'), _("Union"))
+        self.draw_intersect = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/intersection32.png'), _("Intersection"))
+        self.draw_substract = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/subtract32.png'), _("Subtraction"))
+        self.draw_cut = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/cutpath32.png'), _("Cut"))
+        self.draw_transform = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'), _("Transformations"))
+
+        self.g_editor_cmenu.addSeparator()
+        self.draw_move = self.g_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'), _("Move"))
+
+        self.grb_editor_cmenu = self.popMenu.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/draw32.png'), _("Gerber Editor"))
+        self.grb_draw_pad = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/aperture32.png'), _("Pad"))
+        self.grb_draw_pad_array = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/padarray32.png'), _("Pad Array"))
+        self.grb_editor_cmenu.addSeparator()
+
+        self.grb_draw_track = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/track32.png'), _("Track"))
+        self.grb_draw_region = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/polygon32.png'), _("Region"))
+        self.grb_draw_poligonize = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/poligonize32.png'), _("Poligonize"))
+        self.grb_draw_semidisc = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/semidisc32.png'), _("SemiDisc"))
+        self.grb_draw_disc = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/disc32.png'), _("Disc"))
+        self.grb_editor_cmenu.addSeparator()
+
+        self.grb_draw_buffer = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'), _("Buffer"))
+        self.grb_draw_scale = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/scale32.png'), _("Scale"))
+        self.grb_draw_markarea = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/markarea32.png'), _("Mark Area"))
+        self.grb_draw_eraser = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/eraser26.png'), _("Eraser"))
+        self.grb_editor_cmenu.addSeparator()
+
+        self.grb_draw_transformations = self.grb_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'), _("Transformations"))
+
+        self.e_editor_cmenu = self.popMenu.addMenu(
+            QtGui.QIcon(self.app.resource_location + '/drill32.png'), _("Exc Editor"))
+        self.drill = self.e_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/drill32.png'), _("Add Drill"))
+        self.drill_array = self.e_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/addarray32.png'), _("Add Drill Array"))
+        self.e_editor_cmenu.addSeparator()
+        self.slot = self.e_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/slot26.png'), _("Add Slot"))
+        self.slot_array = self.e_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/slot_array26.png'), _("Add Slot Array"))
+        self.e_editor_cmenu.addSeparator()
+        self.drill_resize = self.e_editor_cmenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/resize16.png'), _("Resize Drill"))
+
+        self.popMenu.addSeparator()
+        self.popmenu_copy = self.popMenu.addAction(QtGui.QIcon(self.app.resource_location + '/copy32.png'), _("Copy"))
+        self.popmenu_delete = self.popMenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/delete32.png'), _("Delete"))
+        self.popmenu_edit = self.popMenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/edit32.png'), _("Edit"))
+        self.popmenu_save = self.popMenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/power16.png'), _("Exit Editor"))
+        self.popmenu_save.setVisible(False)
+        self.popMenu.addSeparator()
+
+        self.popmenu_move = self.popMenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'), _("Move"))
+        self.popmenu_properties = self.popMenu.addAction(
+            QtGui.QIcon(self.app.resource_location + '/properties32.png'), _("Properties"))
+
+        # ########################################################################
+        # ########################## INFO BAR # ##################################
+        # ########################################################################
+        self.infobar = self.statusBar()
+        self.fcinfo = FlatCAMInfoBar(app=self.app)
+        self.infobar.addWidget(self.fcinfo, stretch=1)
+
+        self.infobar.addWidget(self.delta_coords_toolbar)
+        self.delta_coords_toolbar.setVisible(self.app.defaults["global_delta_coordsbar_show"])
+
+        self.infobar.addWidget(self.coords_toolbar)
+        self.coords_toolbar.setVisible(self.app.defaults["global_coordsbar_show"])
+
+        self.grid_toolbar.setMaximumHeight(24)
+        self.infobar.addWidget(self.grid_toolbar)
+        self.grid_toolbar.setVisible(self.app.defaults["global_gridbar_show"])
+
+        self.status_toolbar.setMaximumHeight(24)
+        self.infobar.addWidget(self.status_toolbar)
+        self.status_toolbar.setVisible(self.app.defaults["global_statusbar_show"])
+
+        self.units_label = FCLabel("[mm]")
+        self.units_label.setToolTip(_("Application units"))
+        self.units_label.setMargin(2)
+        self.infobar.addWidget(self.units_label)
+
+        # this used to be done in the APP.__init__()
+        self.activity_view = FlatCAMActivityView(app=self.app)
+        self.infobar.addWidget(self.activity_view)
+
+        # disabled
+        # self.progress_bar = QtWidgets.QProgressBar()
+        # self.progress_bar.setMinimum(0)
+        # self.progress_bar.setMaximum(100)
+        # infobar.addWidget(self.progress_bar)
+
+        # ########################################################################
+        # ########################## SET GUI Elements # ##########################
+        # ########################################################################
+        self.app_icon = QtGui.QIcon()
+        self.app_icon.addFile(self.app.resource_location + '/flatcam_icon16.png', QtCore.QSize(16, 16))
+        self.app_icon.addFile(self.app.resource_location + '/flatcam_icon24.png', QtCore.QSize(24, 24))
+        self.app_icon.addFile(self.app.resource_location + '/flatcam_icon32.png', QtCore.QSize(32, 32))
+        self.app_icon.addFile(self.app.resource_location + '/flatcam_icon48.png', QtCore.QSize(48, 48))
+        self.app_icon.addFile(self.app.resource_location + '/flatcam_icon128.png', QtCore.QSize(128, 128))
+        self.app_icon.addFile(self.app.resource_location + '/flatcam_icon256.png', QtCore.QSize(256, 256))
+        self.setWindowIcon(self.app_icon)
+
+        self.setGeometry(100, 100, 1024, 650)
+        self.setWindowTitle('FlatCAM %s %s - %s' %
+                            (self.app.version,
+                             ('BETA' if self.app.beta else ''),
+                             platform.architecture()[0])
+                            )
+
+        self.filename = ""
+        self.units = ""
+        self.setAcceptDrops(True)
+
+        # ########################################################################
+        # ########################## Build GUI # #################################
+        # ########################################################################
+        self.grid_snap_btn.setCheckable(True)
+        self.corner_snap_btn.setCheckable(True)
+        self.update_obj_btn.setEnabled(False)
+        # start with GRID activated
+        self.grid_snap_btn.trigger()
+
+        self.g_editor_cmenu.menuAction().setVisible(False)
+        self.grb_editor_cmenu.menuAction().setVisible(False)
+        self.e_editor_cmenu.menuAction().setVisible(False)
+
+        # ########################################################################
+        # ######################## BUILD PREFERENCES #############################
+        # ########################################################################
+        self.general_defaults_form = GeneralPreferencesUI(decimals=self.decimals)
+        self.gerber_defaults_form = GerberPreferencesUI(decimals=self.decimals)
+        self.excellon_defaults_form = ExcellonPreferencesUI(decimals=self.decimals)
+        self.geometry_defaults_form = GeometryPreferencesUI(decimals=self.decimals)
+        self.cncjob_defaults_form = CNCJobPreferencesUI(decimals=self.decimals)
+        self.tools_defaults_form = ToolsPreferencesUI(decimals=self.decimals)
+        self.tools2_defaults_form = Tools2PreferencesUI(decimals=self.decimals)
+        self.util_defaults_form = UtilPreferencesUI(decimals=self.decimals)
+
+        QtWidgets.qApp.installEventFilter(self)
+
+        # ########################################################################
+        # ################## RESTORE THE TOOLBAR STATE from file #################
+        # ########################################################################
+        flat_settings = QSettings("Open Source", "FlatCAM")
+        if flat_settings.contains("saved_gui_state"):
+            saved_gui_state = flat_settings.value('saved_gui_state')
+            self.restoreState(saved_gui_state)
+            log.debug("MainGUI.__init__() --> UI state restored from QSettings.")
+
+        self.corner_snap_btn.setVisible(False)
+        self.snap_magnet.setVisible(False)
+
+        if flat_settings.contains("layout"):
+            layout = flat_settings.value('layout', type=str)
+            self.exc_edit_toolbar.setDisabled(True)
+            self.geo_edit_toolbar.setDisabled(True)
+            self.grb_edit_toolbar.setDisabled(True)
+
+            log.debug("MainGUI.__init__() --> UI layout restored from QSettings. Layout = %s" % str(layout))
+        else:
+            self.exc_edit_toolbar.setDisabled(True)
+            self.geo_edit_toolbar.setDisabled(True)
+            self.grb_edit_toolbar.setDisabled(True)
+
+            flat_settings.setValue('layout', "standard")
+            # This will write the setting to the platform specific storage.
+            del flat_settings
+            log.debug("MainGUI.__init__() --> UI layout restored from defaults. QSettings set to 'standard'")
+
+        # construct the Toolbar Lock menu entry to the context menu of the QMainWindow
+        self.lock_action = QtWidgets.QAction()
+        self.lock_action.setText(_("Lock Toolbars"))
+        self.lock_action.setCheckable(True)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("toolbar_lock"):
+            lock_val = settings.value('toolbar_lock')
+            if lock_val == 'true':
+                lock_state = True
+                self.lock_action.setChecked(True)
+            else:
+
+                lock_state = False
+                self.lock_action.setChecked(False)
+        else:
+            lock_state = False
+            qsettings.setValue('toolbar_lock', lock_state)
+
+            # This will write the setting to the platform specific storage.
+            del qsettings
+
+        self.lock_toolbar(lock=lock_state)
+
+        self.lock_action.triggered[bool].connect(self.lock_toolbar)
+
+        self.pref_open_button.clicked.connect(self.on_preferences_open_folder)
+        self.clear_btn.clicked.connect(self.on_gui_clear)
+
+        self.wplace_label.clicked.connect(self.app.on_workspace_toggle)
+        self.shell_status_label.clicked.connect(self.toggle_shell_ui)
+
+        # to be used in the future
+        # self.plot_tab_area.tab_attached.connect(lambda x: print(x))
+        # self.plot_tab_area.tab_detached.connect(lambda x: print(x))
+
+        # restore the toolbar view
+        self.restore_toolbar_view()
+
+        # restore the GUI geometry
+        self.restore_main_win_geom()
+
+        # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+        # %%%%%%%%%%%%%%%%% GUI Building FINISHED %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+        # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+        # Variable to store the status of the fullscreen event
+        self.toggle_fscreen = False
+        self.x_pos = None
+        self.y_pos = None
+        self.width = None
+        self.height = None
+        self.titlebar_height = None
+
+        self.geom_update[int, int, int, int, int].connect(self.save_geometry)
+        self.final_save.connect(self.app.final_save)
+
+        self.shell_dock.visibilityChanged.connect(self.on_shelldock_toggled)
+
+        # Notebook and Plot Tab Area signals
+        # make the right click on the notebook tab and plot tab area tab raise a menu
+        self.notebook.tabBar.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+        self.plot_tab_area.tabBar.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+        self.on_tab_setup_context_menu()
+        # activate initial state
+        self.on_detachable_tab_rmb_click(self.app.defaults["global_tabs_detachable"])
+
+        # status bar activation/deactivation
+        self.infobar.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
+        self.build_infobar_context_menu()
+
+    def set_ui_title(self, name):
+        """
+        Sets the title of the main window.
+
+        :param name: String that store the project path and project name
+        :return: None
+        """
+        title = 'FlatCAM %s %s - %s - [%s]    %s' % (
+            self.app.version, ('BETA' if self.app.beta else ''), platform.architecture()[0], self.app.engine, name)
+        self.setWindowTitle(title)
+
+    def save_geometry(self, x, y, width, height, notebook_width):
+        """
+        Will save the application geometry and positions in the defaults dicitionary to be restored at the next
+        launch of the application.
+
+        :param x:               X position of the main window
+        :param y:               Y position of the main window
+        :param width:           width of the main window
+        :param height:          height of the main window
+        :param notebook_width:  the notebook width is adjustable so it get saved here, too.
+
+        :return: None
+        """
+        self.app.defaults["global_def_win_x"] = x
+        self.app.defaults["global_def_win_y"] = y
+        self.app.defaults["global_def_win_w"] = width
+        self.app.defaults["global_def_win_h"] = height
+        self.app.defaults["global_def_notebook_width"] = notebook_width
+        self.app.preferencesUiManager.save_defaults()
+
+    def restore_main_win_geom(self):
+        try:
+            self.setGeometry(self.app.defaults["global_def_win_x"],
+                             self.app.defaults["global_def_win_y"],
+                             self.app.defaults["global_def_win_w"],
+                             self.app.defaults["global_def_win_h"])
+            self.splitter.setSizes([self.app.defaults["global_def_notebook_width"], 0])
+        except KeyError as e:
+            log.debug("appGUI.MainGUI.restore_main_win_geom() --> %s" % str(e))
+
+    def restore_toolbar_view(self):
+        """
+        Some toolbars may be hidden by user and here we restore the state of the toolbars visibility that
+        was saved in the defaults dictionary.
+
+        :return: None
+        """
+        tb = self.app.defaults["global_toolbar_view"]
+
+        if tb & 1:
+            self.toolbarfile.setVisible(True)
+        else:
+            self.toolbarfile.setVisible(False)
+
+        if tb & 2:
+            self.toolbaredit.setVisible(True)
+        else:
+            self.toolbaredit.setVisible(False)
+
+        if tb & 4:
+            self.toolbarview.setVisible(True)
+        else:
+            self.toolbarview.setVisible(False)
+
+        if tb & 8:
+            self.toolbartools.setVisible(True)
+        else:
+            self.toolbartools.setVisible(False)
+
+        if tb & 16:
+            self.exc_edit_toolbar.setVisible(True)
+        else:
+            self.exc_edit_toolbar.setVisible(False)
+
+        if tb & 32:
+            self.geo_edit_toolbar.setVisible(True)
+        else:
+            self.geo_edit_toolbar.setVisible(False)
+
+        if tb & 64:
+            self.grb_edit_toolbar.setVisible(True)
+        else:
+            self.grb_edit_toolbar.setVisible(False)
+
+        # if tb & 128:
+        #     self.ui.grid_toolbar.setVisible(True)
+        # else:
+        #     self.ui.grid_toolbar.setVisible(False)
+
+        # Grid Toolbar is controlled by its own setting
+
+        if tb & 256:
+            self.toolbarshell.setVisible(True)
+        else:
+            self.toolbarshell.setVisible(False)
+
+    def on_tab_setup_context_menu(self):
+        initial_checked = self.app.defaults["global_tabs_detachable"]
+        action_name = str(_("Detachable Tabs"))
+        action = QtWidgets.QAction(self)
+        action.setCheckable(True)
+        action.setText(action_name)
+        action.setChecked(initial_checked)
+
+        self.notebook.tabBar.addAction(action)
+        self.plot_tab_area.tabBar.addAction(action)
+
+        try:
+            action.triggered.disconnect()
+        except TypeError:
+            pass
+        action.triggered.connect(self.on_detachable_tab_rmb_click)
+
+    def on_detachable_tab_rmb_click(self, checked):
+        self.notebook.set_detachable(val=checked)
+        self.app.defaults["global_tabs_detachable"] = checked
+
+        self.plot_tab_area.set_detachable(val=checked)
+        self.app.defaults["global_tabs_detachable"] = checked
+
+    def build_infobar_context_menu(self):
+        delta_coords_action_name = str(_("Delta Coordinates Toolbar"))
+        delta_coords_action = QtWidgets.QAction(self)
+        delta_coords_action.setCheckable(True)
+        delta_coords_action.setText(delta_coords_action_name)
+        delta_coords_action.setChecked(self.app.defaults["global_delta_coordsbar_show"])
+        self.infobar.addAction(delta_coords_action)
+        delta_coords_action.triggered.connect(self.toggle_delta_coords)
+
+        coords_action_name = str(_("Coordinates Toolbar"))
+        coords_action = QtWidgets.QAction(self)
+        coords_action.setCheckable(True)
+        coords_action.setText(coords_action_name)
+        coords_action.setChecked(self.app.defaults["global_coordsbar_show"])
+        self.infobar.addAction(coords_action)
+        coords_action.triggered.connect(self.toggle_coords)
+
+        grid_action_name = str(_("Grid Toolbar"))
+        grid_action = QtWidgets.QAction(self)
+        grid_action.setCheckable(True)
+        grid_action.setText(grid_action_name)
+        grid_action.setChecked(self.app.defaults["global_gridbar_show"])
+        self.infobar.addAction(grid_action)
+        grid_action.triggered.connect(self.toggle_gridbar)
+
+        status_action_name = str(_("Status Toolbar"))
+        status_action = QtWidgets.QAction(self)
+        status_action.setCheckable(True)
+        status_action.setText(status_action_name)
+        status_action.setChecked(self.app.defaults["global_statusbar_show"])
+        self.infobar.addAction(status_action)
+        status_action.triggered.connect(self.toggle_statusbar)
+
+    def toggle_coords(self, checked):
+        self.app.defaults["global_coordsbar_show"] = checked
+        self.coords_toolbar.setVisible(checked)
+
+    def toggle_delta_coords(self, checked):
+        self.app.defaults["global_delta_coordsbar_show"] = checked
+        self.delta_coords_toolbar.setVisible(checked)
+
+    def toggle_gridbar(self, checked):
+        self.app.defaults["global_gridbar_show"] = checked
+        self.grid_toolbar.setVisible(checked)
+
+    def toggle_statusbar(self, checked):
+        self.app.defaults["global_statusbar_show"] = checked
+        self.status_toolbar.setVisible(checked)
+
+    def eventFilter(self, obj, event):
+        """
+        Filter the ToolTips display based on a Preferences setting
+
+        :param obj:
+        :param event: QT event to filter
+        :return:
+        """
+        if self.app.defaults["global_toggle_tooltips"] is False:
+            if event.type() == QtCore.QEvent.ToolTip:
+                return True
+            else:
+                return False
+
+        return False
+
+    def on_preferences_open_folder(self):
+        """
+        Will open an Explorer window set to the folder path where the FlatCAM preferences files are usually saved.
+
+        :return: None
+        """
+
+        if sys.platform == 'win32':
+            subprocess.Popen('explorer %s' % self.app.data_path)
+        elif sys.platform == 'darwin':
+            os.system('open "%s"' % self.app.data_path)
+        else:
+            subprocess.Popen(['xdg-open', self.app.data_path])
+        self.app.inform.emit('[success] %s' % _("FlatCAM Preferences Folder opened."))
+
+    def on_gui_clear(self, signal=None, forced_clear=False):
+        """
+        Will clear the settings that are stored in QSettings.
+        """
+        log.debug("Clearing the settings in QSettings. GUI settings cleared.")
+
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        theme_settings.setValue('theme', 'white')
+
+        del theme_settings
+
+        resource_loc = self.app.resource_location
+
+        response = None
+        bt_yes = None
+        if forced_clear is False:
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setText(_("Are you sure you want to delete the GUI Settings? \n"))
+            msgbox.setWindowTitle(_("Clear GUI Settings"))
+            msgbox.setWindowIcon(QtGui.QIcon(resource_loc + '/trash32.png'))
+            msgbox.setIcon(QtWidgets.QMessageBox.Question)
+
+            bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.YesRole)
+            bt_no = msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole)
+
+            msgbox.setDefaultButton(bt_no)
+            msgbox.exec_()
+            response = msgbox.clickedButton()
+
+        if forced_clear is True or response == bt_yes:
+            qsettings = QSettings("Open Source", "FlatCAM")
+            for key in qsettings.allKeys():
+                qsettings.remove(key)
+            # This will write the setting to the platform specific storage.
+            del qsettings
+
+    def populate_toolbars(self):
+        """
+        Will populate the App Toolbars with their actions
+
+        :return: None
+        """
+        self.app.log.debug(" -> Add actions to new Toolbars")
+
+        # ########################################################################
+        # ##################### File Toolbar #####################################
+        # ########################################################################
+        self.file_open_gerber_btn = self.toolbarfile.addAction(
+            QtGui.QIcon(self.app.resource_location + '/flatcam_icon32.png'), _("Open Gerber"))
+        self.file_open_excellon_btn = self.toolbarfile.addAction(
+            QtGui.QIcon(self.app.resource_location + '/drill32.png'), _("Open Excellon"))
+        self.toolbarfile.addSeparator()
+        self.file_open_btn = self.toolbarfile.addAction(
+            QtGui.QIcon(self.app.resource_location + '/folder32.png'), _("Open Project"))
+        self.file_save_btn = self.toolbarfile.addAction(
+            QtGui.QIcon(self.app.resource_location + '/project_save32.png'), _("Save Project"))
+
+        # ########################################################################
+        # ######################### Edit Toolbar #################################
+        # ########################################################################
+        self.editgeo_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/edit32.png'), _("Editor"))
+        self.update_obj_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/close_edit_file32.png'),
+            _("Save Object and close the Editor")
+        )
+
+        self.toolbaredit.addSeparator()
+        self.copy_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy_file32.png'), _("Copy"))
+        self.delete_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("Delete"))
+        self.toolbaredit.addSeparator()
+        self.distance_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/distance32.png'), _("Distance Tool"))
+        self.distance_min_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/distance_min32.png'), _("Distance Min Tool"))
+        self.origin_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/origin32.png'), _('Set Origin'))
+        self.move2origin_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/origin2_32.png'), _('Move to Origin'))
+        self.jmp_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/jump_to16.png'), _('Jump to Location'))
+        self.locate_btn = self.toolbaredit.addAction(
+            QtGui.QIcon(self.app.resource_location + '/locate32.png'), _('Locate in Object'))
+
+        # ########################################################################
+        # ########################## View Toolbar# ###############################
+        # ########################################################################
+        self.replot_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/replot32.png'), _("Replot"))
+        self.clear_plot_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/clear_plot32.png'), _("Clear Plot"))
+        self.zoom_in_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_in32.png'), _("Zoom In"))
+        self.zoom_out_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_out32.png'), _("Zoom Out"))
+        self.zoom_fit_btn = self.toolbarview.addAction(
+            QtGui.QIcon(self.app.resource_location + '/zoom_fit32.png'), _("Zoom Fit"))
+
+        # ########################################################################
+        # ########################## Shell Toolbar# ##############################
+        # ########################################################################
+        self.shell_btn = self.toolbarshell.addAction(
+            QtGui.QIcon(self.app.resource_location + '/shell32.png'), _("Command Line"))
+        self.new_script_btn = self.toolbarshell.addAction(
+            QtGui.QIcon(self.app.resource_location + '/script_new24.png'), '%s ...' % _('New Script'))
+        self.open_script_btn = self.toolbarshell.addAction(
+            QtGui.QIcon(self.app.resource_location + '/open_script32.png'), '%s ...' % _('Open Script'))
+        self.run_script_btn = self.toolbarshell.addAction(
+            QtGui.QIcon(self.app.resource_location + '/script16.png'), '%s ...' % _('Run Script'))
+
+        # #########################################################################
+        # ######################### Tools Toolbar #################################
+        # #########################################################################
+        self.dblsided_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/doubleside32.png'), _("2-Sided Tool"))
+        self.align_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/align32.png'), _("Align Objects Tool"))
+        self.extract_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/extract_drill32.png'), _("Extract Drills Tool"))
+
+        self.cutout_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/cut16_bis.png'), _("Cutout Tool"))
+        self.ncc_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/ncc16.png'), _("NCC Tool"))
+        self.paint_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/paint20_1.png'), _("Paint Tool"))
+        self.isolation_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/iso_16.png'), _("Isolation Tool"))
+        self.drill_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/drilling_tool32.png'), _("Drilling Tool"))
+        self.toolbartools.addSeparator()
+
+        self.panelize_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/panelize32.png'), _("Panel Tool"))
+        self.film_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/film16.png'), _("Film Tool"))
+        self.solder_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/solderpastebis32.png'), _("SolderPaste Tool"))
+        self.sub_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/sub32.png'), _("Subtract Tool"))
+        self.rules_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rules32.png'), _("Rules Tool"))
+        self.optimal_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/open_excellon32.png'), _("Optimal Tool"))
+
+        self.toolbartools.addSeparator()
+
+        self.calculators_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/calculator24.png'), _("Calculators Tool"))
+        self.transform_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'), _("Transform Tool"))
+        self.qrcode_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/qrcode32.png'), _("QRCode Tool"))
+        self.copperfill_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copperfill32.png'), _("Copper Thieving Tool"))
+
+        self.fiducials_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/fiducials_32.png'), _("Fiducials Tool"))
+        self.cal_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/calibrate_32.png'), _("Calibration Tool"))
+        self.punch_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/punch32.png'), _("Punch Gerber Tool"))
+        self.invert_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/invert32.png'), _("Invert Gerber Tool"))
+        self.corners_tool_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/corners_32.png'), _("Corner Markers Tool"))
+        self.etch_btn = self.toolbartools.addAction(
+            QtGui.QIcon(self.app.resource_location + '/etch_32.png'), _("Etch Compensation Tool"))
+
+        # ########################################################################
+        # ################### Excellon Editor Toolbar ############################
+        # ########################################################################
+        self.select_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/pointer32.png'), _("Select"))
+        self.add_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/plus16.png'), _('Add Drill'))
+        self.add_drill_array_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/addarray16.png'), _('Add Drill Array'))
+        self.resize_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/resize16.png'), _('Resize Drill'))
+        self.add_slot_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/slot26.png'), _('Add Slot'))
+        self.add_slot_array_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/slot_array26.png'), _('Add Slot Array'))
+        self.exc_edit_toolbar.addSeparator()
+
+        self.copy_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy32.png'), _('Copy Drill'))
+        self.delete_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("Delete Drill"))
+
+        self.exc_edit_toolbar.addSeparator()
+        self.move_drill_btn = self.exc_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'), _("Move Drill"))
+
+        # ########################################################################
+        # ################### Geometry Editor Toolbar ############################
+        # ########################################################################
+        self.geo_select_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/pointer32.png'), _("Select"))
+        self.geo_add_circle_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/circle32.png'), _('Add Circle'))
+        self.geo_add_arc_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/arc32.png'), _('Add Arc'))
+        self.geo_add_rectangle_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/rectangle32.png'), _('Add Rectangle'))
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_add_path_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/path32.png'), _('Add Path'))
+        self.geo_add_polygon_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/polygon32.png'), _('Add Polygon'))
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_add_text_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/text32.png'), _('Add Text'))
+        self.geo_add_buffer_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'), _('Add Buffer'))
+        self.geo_add_paint_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/paint20_1.png'), _('Paint Shape'))
+        self.geo_eraser_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/eraser26.png'), _('Eraser'))
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_union_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/union32.png'), _('Polygon Union'))
+        self.geo_explode_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/explode32.png'), _('Polygon Explode'))
+
+        self.geo_intersection_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/intersection32.png'), _('Polygon Intersection'))
+        self.geo_subtract_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/subtract32.png'), _('Polygon Subtraction'))
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_cutpath_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/cutpath32.png'), _('Cut Path'))
+        self.geo_copy_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy32.png'), _("Copy Objects"))
+        self.geo_delete_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("Delete Shape"))
+        self.geo_transform_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'), _("Transformations"))
+
+        self.geo_edit_toolbar.addSeparator()
+        self.geo_move_btn = self.geo_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'), _("Move Objects"))
+
+        # ########################################################################
+        # ################### Gerber Editor Toolbar ##############################
+        # ########################################################################
+        self.grb_select_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/pointer32.png'), _("Select"))
+        self.grb_add_pad_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/aperture32.png'), _("Add Pad"))
+        self.add_pad_ar_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/padarray32.png'), _('Add Pad Array'))
+        self.grb_add_track_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/track32.png'), _("Add Track"))
+        self.grb_add_region_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/polygon32.png'), _("Add Region"))
+        self.grb_convert_poly_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/poligonize32.png'), _("Poligonize"))
+
+        self.grb_add_semidisc_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/semidisc32.png'), _("SemiDisc"))
+        self.grb_add_disc_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/disc32.png'), _("Disc"))
+        self.grb_edit_toolbar.addSeparator()
+
+        self.aperture_buffer_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/buffer16-2.png'), _('Buffer'))
+        self.aperture_scale_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/scale32.png'), _('Scale'))
+        self.aperture_markarea_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/markarea32.png'), _('Mark Area'))
+        self.aperture_eraser_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/eraser26.png'), _('Eraser'))
+
+        self.grb_edit_toolbar.addSeparator()
+        self.aperture_copy_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/copy32.png'), _("Copy"))
+        self.aperture_delete_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/trash32.png'), _("Delete"))
+        self.grb_transform_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/transform.png'), _("Transformations"))
+        self.grb_edit_toolbar.addSeparator()
+        self.aperture_move_btn = self.grb_edit_toolbar.addAction(
+            QtGui.QIcon(self.app.resource_location + '/move32.png'), _("Move"))
+
+        self.corner_snap_btn.setVisible(False)
+        self.snap_magnet.setVisible(False)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("layout"):
+            layout = qsettings.value('layout', type=str)
+
+            # on 'minimal' layout only some toolbars are active
+            if layout != 'minimal':
+                self.exc_edit_toolbar.setVisible(True)
+                self.exc_edit_toolbar.setDisabled(True)
+                self.geo_edit_toolbar.setVisible(True)
+                self.geo_edit_toolbar.setDisabled(True)
+                self.grb_edit_toolbar.setVisible(True)
+                self.grb_edit_toolbar.setDisabled(True)
+
+    def keyPressEvent(self, event):
+        """
+        Key event handler for the entire app.
+        Some of the key events are also treated locally in the FlatCAM editors
+
+        :param event: QT event
+        :return:
+        """
+        modifiers = QtWidgets.QApplication.keyboardModifiers()
+        active = self.app.collection.get_active()
+        selected = self.app.collection.get_selected()
+        names_list = self.app.collection.get_names()
+
+        matplotlib_key_flag = False
+
+        # events out of the self.app.collection view (it's about Project Tab) are of type int
+        if type(event) is int:
+            key = event
+        # events from the GUI are of type QKeyEvent
+        elif type(event) == QtGui.QKeyEvent:
+            key = event.key()
+        elif isinstance(event, mpl_key_event):  # MatPlotLib key events are trickier to interpret than the rest
+            matplotlib_key_flag = True
+
+            key = event.key
+            key = QtGui.QKeySequence(key)
+
+            # check for modifiers
+            key_string = key.toString().lower()
+            if '+' in key_string:
+                mod, __, key_text = key_string.rpartition('+')
+                if mod.lower() == 'ctrl':
+                    modifiers = QtCore.Qt.ControlModifier
+                elif mod.lower() == 'alt':
+                    modifiers = QtCore.Qt.AltModifier
+                elif mod.lower() == 'shift':
+                    modifiers = QtCore.Qt.ShiftModifier
+                else:
+                    modifiers = QtCore.Qt.NoModifier
+                key = QtGui.QKeySequence(key_text)
+
+        # events from Vispy are of type KeyEvent
+        else:
+            key = event.key
+
+        if self.app.call_source == 'app':
+            # CTRL + ALT
+            if modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier:
+                if key == QtCore.Qt.Key_X:
+                    self.app.abort_all_tasks()
+                    return
+            # CTRL + SHIFT
+            if modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier:
+                if key == QtCore.Qt.Key_S:
+                    self.app.f_handlers.on_file_saveprojectas()
+                    return
+            # CTRL
+            elif modifiers == QtCore.Qt.ControlModifier:
+                # Select All
+                if key == QtCore.Qt.Key_A:
+                    self.app.on_selectall()
+
+                # Copy an FlatCAM object
+                if key == QtCore.Qt.Key_C:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if widget_name == 'database_tab':
+                        # Tools DB saved, update flag
+                        self.app.tools_db_changed_flag = True
+                        self.app.tools_db_tab.on_tool_copy()
+                        return
+
+                    self.app.on_copy_command()
+
+                # Copy an FlatCAM object
+                if key == QtCore.Qt.Key_D:
+                    self.app.on_tools_database()
+
+                # Open Excellon file
+                if key == QtCore.Qt.Key_E:
+                    self.app.f_handlers.on_fileopenexcellon(signal=None)
+
+                # Open Gerber file
+                if key == QtCore.Qt.Key_G:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if 'editor' in widget_name.lower():
+                        self.app.goto_text_line()
+                    else:
+                        self.app.f_handlers.on_fileopengerber(signal=None)
+
+                # Distance Tool
+                if key == QtCore.Qt.Key_M:
+                    self.app.distance_tool.run()
+
+                # Create New Project
+                if key == QtCore.Qt.Key_N:
+                    self.app.f_handlers.on_file_new_click()
+
+                # Open Project
+                if key == QtCore.Qt.Key_O:
+                    self.app.f_handlers.on_file_openproject(signal=None)
+
+                # Open Project
+                if key == QtCore.Qt.Key_P:
+                    self.app.f_handlers.on_file_save_objects_pdf(use_thread=True)
+
+                # PDF Import
+                if key == QtCore.Qt.Key_Q:
+                    self.app.pdf_tool.run()
+
+                # Save Project
+                if key == QtCore.Qt.Key_S:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if widget_name == 'preferences_tab':
+                        self.app.preferencesUiManager.on_save_button(save_to_file=False)
+                        return
+
+                    if widget_name == 'database_tab':
+                        # Tools DB saved, update flag
+                        self.app.tools_db_changed_flag = False
+                        self.app.tools_db_tab.on_save_tools_db()
+                        return
+
+                    self.app.f_handlers.on_file_saveproject()
+
+                # Toggle Plot Area
+                if key == QtCore.Qt.Key_F10 or key == 'F10':
+                    self.on_toggle_plotarea()
+
+                return
+            # SHIFT
+            elif modifiers == QtCore.Qt.ShiftModifier:
+
+                # Toggle axis
+                if key == QtCore.Qt.Key_A:
+                    self.app.plotcanvas.on_toggle_axis()
+
+                # Copy Object Name
+                if key == QtCore.Qt.Key_C:
+                    self.app.on_copy_name()
+
+                # Toggle Code Editor
+                if key == QtCore.Qt.Key_E:
+                    self.app.on_toggle_code_editor()
+
+                # Toggle Grid lines
+                if key == QtCore.Qt.Key_G:
+                    self.app.plotcanvas.on_toggle_grid_lines()
+                    return
+
+                # Toggle HUD (Heads-Up Display)
+                if key == QtCore.Qt.Key_H:
+                    self.app.plotcanvas.on_toggle_hud()
+                # Locate in Object
+                if key == QtCore.Qt.Key_J:
+                    self.app.on_locate(obj=self.app.collection.get_active())
+
+                # Run Distance Minimum Tool
+                if key == QtCore.Qt.Key_M:
+                    self.app.distance_min_tool.run()
+                    return
+
+                # Open Preferences Window
+                if key == QtCore.Qt.Key_P:
+                    self.app.on_preferences()
+                    return
+
+                # Rotate Object by 90 degree CCW
+                if key == QtCore.Qt.Key_R:
+                    self.app.on_rotate(silent=True, preset=-float(self.app.defaults['tools_transform_rotate']))
+                    return
+
+                # Run a Script
+                if key == QtCore.Qt.Key_S:
+                    self.app.f_handlers.on_filerunscript()
+                    return
+
+                # Toggle Workspace
+                if key == QtCore.Qt.Key_W:
+                    self.app.on_workspace_toggle()
+                    return
+
+                # Skew on X axis
+                if key == QtCore.Qt.Key_X:
+                    self.app.on_skewx()
+                    return
+
+                # Skew on Y axis
+                if key == QtCore.Qt.Key_Y:
+                    self.app.on_skewy()
+                    return
+            # ALT
+            elif modifiers == QtCore.Qt.AltModifier:
+                # Eanble all plots
+                if key == Qt.Key_1:
+                    self.app.enable_all_plots()
+
+                # Disable all plots
+                if key == Qt.Key_2:
+                    self.app.disable_all_plots()
+
+                # Disable all other plots
+                if key == Qt.Key_3:
+                    self.app.enable_other_plots()
+
+                # Disable all other plots
+                if key == Qt.Key_4:
+                    self.app.disable_other_plots()
+
+                # Align in Object Tool
+                if key == QtCore.Qt.Key_A:
+                    self.app.align_objects_tool.run(toggle=True)
+
+                # Calculator Tool
+                if key == QtCore.Qt.Key_C:
+                    self.app.calculator_tool.run(toggle=True)
+
+                # 2-Sided PCB Tool
+                if key == QtCore.Qt.Key_D:
+                    self.app.dblsidedtool.run(toggle=True)
+                    return
+
+                # Extract Drills  Tool
+                if key == QtCore.Qt.Key_E:
+                    # self.app.cal_exc_tool.run(toggle=True)
+                    self.app.edrills_tool.run(toggle=True)
+                    return
+
+                # Fiducials Tool
+                if key == QtCore.Qt.Key_F:
+                    self.app.fiducial_tool.run(toggle=True)
+                    return
+
+                # Punch Gerber Tool
+                if key == QtCore.Qt.Key_G:
+                    self.app.invert_tool.run(toggle=True)
+
+                # Punch Gerber Tool
+                if key == QtCore.Qt.Key_H:
+                    self.app.punch_tool.run(toggle=True)
+
+                # Isolation Tool
+                if key == QtCore.Qt.Key_I:
+                    self.app.isolation_tool.run(toggle=True)
+
+                # Copper Thieving Tool
+                if key == QtCore.Qt.Key_J:
+                    self.app.copper_thieving_tool.run(toggle=True)
+                    return
+
+                # Solder Paste Dispensing Tool
+                if key == QtCore.Qt.Key_K:
+                    self.app.paste_tool.run(toggle=True)
+                    return
+
+                # Film Tool
+                if key == QtCore.Qt.Key_L:
+                    self.app.film_tool.run(toggle=True)
+                    return
+
+                # Corner Markers Tool
+                if key == QtCore.Qt.Key_M:
+                    self.app.corners_tool.run(toggle=True)
+                    return
+
+                # Non-Copper Clear Tool
+                if key == QtCore.Qt.Key_N:
+                    self.app.ncclear_tool.run(toggle=True)
+                    return
+
+                # Optimal Tool
+                if key == QtCore.Qt.Key_O:
+                    self.app.optimal_tool.run(toggle=True)
+                    return
+
+                # Paint Tool
+                if key == QtCore.Qt.Key_P:
+                    self.app.paint_tool.run(toggle=True)
+                    return
+
+                # QRCode Tool
+                if key == QtCore.Qt.Key_Q:
+                    self.app.qrcode_tool.run()
+                    return
+
+                # Rules Tool
+                if key == QtCore.Qt.Key_R:
+                    self.app.rules_tool.run(toggle=True)
+                    return
+
+                # View Source Object Content
+                if key == QtCore.Qt.Key_S:
+                    self.app.on_view_source()
+                    return
+
+                # Transformation Tool
+                if key == QtCore.Qt.Key_T:
+                    self.app.transform_tool.run(toggle=True)
+                    return
+
+                # Substract Tool
+                if key == QtCore.Qt.Key_W:
+                    self.app.sub_tool.run(toggle=True)
+                    return
+
+                # Cutout Tool
+                if key == QtCore.Qt.Key_X:
+                    self.app.cutout_tool.run(toggle=True)
+                    return
+
+                # Panelize Tool
+                if key == QtCore.Qt.Key_Z:
+                    self.app.panelize_tool.run(toggle=True)
+                    return
+
+                # Toggle Fullscreen
+                if key == QtCore.Qt.Key_F10 or key == 'F10':
+                    self.on_fullscreen()
+                    return
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                # Open Manual
+                if key == QtCore.Qt.Key_F1 or key == 'F1':
+                    webbrowser.open(self.app.manual_url)
+
+                # Show shortcut list
+                if key == QtCore.Qt.Key_F3 or key == 'F3':
+                    self.app.on_shortcut_list()
+
+                # Open Video Help
+                if key == QtCore.Qt.Key_F4 or key == 'F4':
+                    webbrowser.open(self.app.video_url)
+
+                # Open Video Help
+                if key == QtCore.Qt.Key_F5 or key == 'F5':
+                    self.app.plot_all()
+
+                # Switch to Project Tab
+                if key == QtCore.Qt.Key_1:
+                    self.app.on_select_tab('project')
+
+                # Switch to Selected Tab
+                if key == QtCore.Qt.Key_2:
+                    self.app.on_select_tab('properties')
+
+                # Switch to Tool Tab
+                if key == QtCore.Qt.Key_3:
+                    self.app.on_select_tab('tool')
+
+                # Delete from PyQt
+                # It's meant to make a difference between delete objects and delete tools in
+                # Geometry Selected tool table
+                if key == QtCore.Qt.Key_Delete and matplotlib_key_flag is False:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if widget_name == 'database_tab':
+                        # Tools DB saved, update flag
+                        self.app.tools_db_changed_flag = True
+                        self.app.tools_db_tab.on_tool_delete()
+                        return
+
+                    self.app.on_delete_keypress()
+
+                # Delete from canvas
+                if key == 'Delete':
+                    # Delete via the application to
+                    # ensure cleanup of the appGUI
+                    if active:
+                        active.app.on_delete()
+
+                # Escape = Deselect All
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    self.app.on_deselect_all()
+
+                    # if in full screen, exit to normal view
+                    if self.toggle_fscreen is True:
+                        self.on_fullscreen(disable=True)
+
+                    # try to disconnect the slot from Set Origin
+                    try:
+                        self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_set_zero_click)
+                    except TypeError:
+                        pass
+                    self.app.inform.emit("")
+
+                # Space = Toggle Active/Inactive
+                if key == QtCore.Qt.Key_Space:
+                    for select in selected:
+                        select.ui.plot_cb.toggle()
+                        QtWidgets.QApplication.processEvents()
+                    self.app.collection.update_view()
+                    self.app.delete_selection_shape()
+
+                # Select the object in the Tree above the current one
+                if key == QtCore.Qt.Key_Up:
+                    # make sure it works only for the Project Tab who is an instance of KeySensitiveListView
+                    focused_wdg = QtWidgets.QApplication.focusWidget()
+                    if isinstance(focused_wdg, KeySensitiveListView):
+                        self.app.collection.set_all_inactive()
+                        if active is None:
+                            return
+                        active_name = active.options['name']
+                        active_index = names_list.index(active_name)
+                        if active_index == 0:
+                            self.app.collection.set_active(names_list[-1])
+                        else:
+                            self.app.collection.set_active(names_list[active_index - 1])
+
+                # Select the object in the Tree below the current one
+                if key == QtCore.Qt.Key_Down:
+                    # make sure it works only for the Project Tab who is an instance of KeySensitiveListView
+                    focused_wdg = QtWidgets.QApplication.focusWidget()
+                    if isinstance(focused_wdg, KeySensitiveListView):
+                        self.app.collection.set_all_inactive()
+                        if active is None:
+                            return
+                        active_name = active.options['name']
+                        active_index = names_list.index(active_name)
+                        if active_index == len(names_list) - 1:
+                            self.app.collection.set_active(names_list[0])
+                        else:
+                            self.app.collection.set_active(names_list[active_index + 1])
+
+                # New Geometry
+                if key == QtCore.Qt.Key_B:
+                    self.app.app_obj.new_gerber_object()
+
+                # New Document Object
+                if key == QtCore.Qt.Key_D:
+                    self.app.app_obj.new_document_object()
+
+                # Copy Object Name
+                if key == QtCore.Qt.Key_E:
+                    self.app.object2editor()
+
+                # Grid toggle
+                if key == QtCore.Qt.Key_G:
+                    self.app.ui.grid_snap_btn.trigger()
+
+                # Jump to coords
+                if key == QtCore.Qt.Key_J:
+                    self.app.on_jump_to()
+
+                # New Excellon
+                if key == QtCore.Qt.Key_L:
+                    self.app.app_obj.new_excellon_object()
+
+                # Move tool toggle
+                if key == QtCore.Qt.Key_M:
+                    self.app.move_tool.toggle()
+
+                # New Geometry
+                if key == QtCore.Qt.Key_N:
+                    self.app.app_obj.new_geometry_object()
+
+                # Set Origin
+                if key == QtCore.Qt.Key_O:
+                    self.app.on_set_origin()
+                    return
+
+                # Properties Tool
+                if key == QtCore.Qt.Key_P:
+                    self.app.properties_tool.run()
+                    return
+
+                # Change Units
+                if key == QtCore.Qt.Key_Q:
+                    # if self.app.defaults["units"] == 'MM':
+                    #     self.app.ui.general_defaults_form.general_app_group.units_radio.set_value("IN")
+                    # else:
+                    #     self.app.ui.general_defaults_form.general_app_group.units_radio.set_value("MM")
+                    # self.app.on_toggle_units(no_pref=True)
+                    self.app.on_toggle_units_click()
+
+                # Rotate Object by 90 degree CW
+                if key == QtCore.Qt.Key_R:
+                    self.app.on_rotate(silent=True, preset=self.app.defaults['tools_transform_rotate'])
+
+                # Shell toggle
+                if key == QtCore.Qt.Key_S:
+                    self.toggle_shell_ui()
+
+                # Add a Tool from shortcut
+                if key == QtCore.Qt.Key_T:
+                    widget_name = self.plot_tab_area.currentWidget().objectName()
+                    if widget_name == 'database_tab':
+                        # Tools DB saved, update flag
+                        self.app.tools_db_changed_flag = True
+                        self.app.tools_db_tab.on_tool_add()
+                        return
+
+                    self.app.on_tool_add_keypress()
+
+                # Zoom Fit
+                if key == QtCore.Qt.Key_V:
+                    self.app.on_zoom_fit()
+
+                # Mirror on X the selected object(s)
+                if key == QtCore.Qt.Key_X:
+                    self.app.on_flipx()
+
+                # Mirror on Y the selected object(s)
+                if key == QtCore.Qt.Key_Y:
+                    self.app.on_flipy()
+
+                # Zoom In
+                if key == QtCore.Qt.Key_Equal:
+                    self.app.plotcanvas.zoom(1 / self.app.defaults['global_zoom_ratio'], self.app.mouse)
+
+                # Zoom Out
+                if key == QtCore.Qt.Key_Minus:
+                    self.app.plotcanvas.zoom(self.app.defaults['global_zoom_ratio'], self.app.mouse)
+
+                # toggle display of Notebook area
+                if key == QtCore.Qt.Key_QuoteLeft:
+                    self.on_toggle_notebook()
+
+                return
+        elif self.app.call_source == 'geo_editor':
+            # CTRL
+            if modifiers == QtCore.Qt.ControlModifier:
+                # save (update) the current geometry and return to the App
+                if key == QtCore.Qt.Key_S or key == 'S':
+                    self.app.editor2object()
+                    return
+
+                # toggle the measurement tool
+                if key == QtCore.Qt.Key_M or key == 'M':
+                    self.app.distance_tool.run()
+                    return
+
+                # Cut Action Tool
+                if key == QtCore.Qt.Key_X or key == 'X':
+                    if self.app.geo_editor.get_selected() is not None:
+                        self.app.geo_editor.cutpath()
+                    else:
+                        msg = _('Please first select a geometry item to be cutted\n'
+                                'then select the geometry item that will be cutted\n'
+                                'out of the first item. In the end press ~X~ key or\n'
+                                'the toolbar button.')
+
+                        messagebox = QtWidgets.QMessageBox()
+                        messagebox.setText(msg)
+                        messagebox.setWindowTitle(_("Warning"))
+                        messagebox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/warning.png'))
+                        messagebox.setIcon(QtWidgets.QMessageBox.Question)
+
+                        messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                        messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                        messagebox.exec_()
+                    return
+            # SHIFT
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                # Run Distance Minimum Tool
+                if key == QtCore.Qt.Key_M or key == 'M':
+                    self.app.distance_min_tool.run()
+                    return
+
+                # Skew on X axis
+                if key == QtCore.Qt.Key_X or key == 'X':
+                    self.app.geo_editor.transform_tool.on_skewx_key()
+                    return
+
+                # Skew on Y axis
+                if key == QtCore.Qt.Key_Y or key == 'Y':
+                    self.app.geo_editor.transform_tool.on_skewy_key()
+                    return
+            # ALT
+            elif modifiers == QtCore.Qt.AltModifier:
+
+                # Transformation Tool
+                if key == QtCore.Qt.Key_R or key == 'R':
+                    self.app.geo_editor.select_tool('transform')
+                    return
+
+                # Offset on X axis
+                if key == QtCore.Qt.Key_X or key == 'X':
+                    self.app.geo_editor.transform_tool.on_offx_key()
+                    return
+
+                # Offset on Y axis
+                if key == QtCore.Qt.Key_Y or key == 'Y':
+                    self.app.geo_editor.transform_tool.on_offy_key()
+                    return
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                # toggle display of Notebook area
+                if key == QtCore.Qt.Key_QuoteLeft or key == '`':
+                    self.on_toggle_notebook()
+
+                # Finish the current action. Use with tools that do not
+                # complete automatically, like a polygon or path.
+                if key == QtCore.Qt.Key_Enter or key == 'Enter':
+                    if isinstance(self.app.geo_editor.active_tool, FCShapeTool):
+                        if self.app.geo_editor.active_tool.name == 'rotate':
+                            self.app.geo_editor.active_tool.make()
+
+                            if self.app.geo_editor.active_tool.complete:
+                                self.app.geo_editor.on_shape_complete()
+                                self.app.inform.emit('[success] %s' % _("Done."))
+                            # automatically make the selection tool active after completing current action
+                            self.app.geo_editor.select_tool('select')
+                            return
+                        else:
+                            self.app.geo_editor.active_tool.click(
+                                self.app.geo_editor.snap(self.app.geo_editor.x, self.app.geo_editor.y))
+
+                            self.app.geo_editor.active_tool.make()
+
+                            if self.app.geo_editor.active_tool.complete:
+                                self.app.geo_editor.on_shape_complete()
+                                self.app.inform.emit('[success] %s' % _("Done."))
+                            # automatically make the selection tool active after completing current action
+                            self.app.geo_editor.select_tool('select')
+
+                # Abort the current action
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    # self.on_tool_select("select")
+                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+
+                    self.app.geo_editor.delete_utility_geometry()
+
+                    self.app.geo_editor.active_tool.clean_up()
+
+                    self.app.geo_editor.select_tool('select')
+
+                    # hide the notebook
+                    self.app.ui.splitter.setSizes([0, 1])
+                    return
+
+                # Delete selected object
+                if key == QtCore.Qt.Key_Delete or key == 'Delete':
+                    self.app.geo_editor.delete_selected()
+                    self.app.geo_editor.replot()
+
+                # Rotate
+                if key == QtCore.Qt.Key_Space or key == 'Space':
+                    self.app.geo_editor.transform_tool.on_rotate_key()
+
+                # Zoom Out
+                if key == QtCore.Qt.Key_Minus or key == '-':
+                    self.app.plotcanvas.zoom(1 / self.app.defaults['global_zoom_ratio'],
+                                             [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
+
+                # Zoom In
+                if key == QtCore.Qt.Key_Equal or key == '=':
+                    self.app.plotcanvas.zoom(self.app.defaults['global_zoom_ratio'],
+                                             [self.app.geo_editor.snap_x, self.app.geo_editor.snap_y])
+
+                # Switch to Project Tab
+                if key == QtCore.Qt.Key_1 or key == '1':
+                    self.app.on_select_tab('project')
+
+                # Switch to Selected Tab
+                if key == QtCore.Qt.Key_2 or key == '2':
+                    self.app.on_select_tab('selected')
+
+                # Switch to Tool Tab
+                if key == QtCore.Qt.Key_3 or key == '3':
+                    self.app.on_select_tab('tool')
+
+                # Grid Snap
+                if key == QtCore.Qt.Key_G or key == 'G':
+                    self.app.ui.grid_snap_btn.trigger()
+
+                    # make sure that the cursor shape is enabled/disabled, too
+                    if self.app.geo_editor.options['grid_snap'] is True:
+                        self.app.app_cursor.enabled = True
+                    else:
+                        self.app.app_cursor.enabled = False
+
+                # Corner Snap
+                if key == QtCore.Qt.Key_K or key == 'K':
+                    self.app.geo_editor.on_corner_snap()
+
+                if key == QtCore.Qt.Key_V or key == 'V':
+                    self.app.on_zoom_fit()
+
+                # we do this so we can reuse the following keys while inside a Tool
+                # the above keys are general enough so were left outside
+                if self.app.geo_editor.active_tool is not None and self.geo_select_btn.isChecked() is False:
+                    response = self.app.geo_editor.active_tool.on_key(key=key)
+                    if response is not None:
+                        self.app.inform.emit(response)
+                else:
+                    # Arc Tool
+                    if key == QtCore.Qt.Key_A or key == 'A':
+                        self.app.geo_editor.select_tool('arc')
+
+                    # Buffer
+                    if key == QtCore.Qt.Key_B or key == 'B':
+                        self.app.geo_editor.select_tool('buffer')
+
+                    # Copy
+                    if key == QtCore.Qt.Key_C or key == 'C':
+                        self.app.geo_editor.on_copy_click()
+
+                    # Substract Tool
+                    if key == QtCore.Qt.Key_E or key == 'E':
+                        if self.app.geo_editor.get_selected() is not None:
+                            self.app.geo_editor.intersection()
+                        else:
+                            msg = _("Please select geometry items \n"
+                                    "on which to perform Intersection Tool.")
+
+                            messagebox = QtWidgets.QMessageBox()
+                            messagebox.setText(msg)
+                            messagebox.setWindowTitle(_("Warning"))
+                            messagebox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/warning.png'))
+                            messagebox.setIcon(QtWidgets.QMessageBox.Warning)
+
+                            messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                            messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                            messagebox.exec_()
+
+                    # Paint
+                    if key == QtCore.Qt.Key_I or key == 'I':
+                        self.app.geo_editor.select_tool('paint')
+
+                    # Jump to coords
+                    if key == QtCore.Qt.Key_J or key == 'J':
+                        self.app.on_jump_to()
+
+                    # Move
+                    if key == QtCore.Qt.Key_M or key == 'M':
+                        self.app.geo_editor.on_move_click()
+
+                    # Polygon Tool
+                    if key == QtCore.Qt.Key_N or key == 'N':
+                        self.app.geo_editor.select_tool('polygon')
+
+                    # Circle Tool
+                    if key == QtCore.Qt.Key_O or key == 'O':
+                        self.app.geo_editor.select_tool('circle')
+
+                    # Path Tool
+                    if key == QtCore.Qt.Key_P or key == 'P':
+                        self.app.geo_editor.select_tool('path')
+
+                    # Rectangle Tool
+                    if key == QtCore.Qt.Key_R or key == 'R':
+                        self.app.geo_editor.select_tool('rectangle')
+
+                    # Substract Tool
+                    if key == QtCore.Qt.Key_S or key == 'S':
+                        if self.app.geo_editor.get_selected() is not None:
+                            self.app.geo_editor.subtract()
+                        else:
+                            msg = _(
+                                "Please select geometry items \n"
+                                "on which to perform Substraction Tool.")
+
+                            messagebox = QtWidgets.QMessageBox()
+                            messagebox.setText(msg)
+                            messagebox.setWindowTitle(_("Warning"))
+                            messagebox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/warning.png'))
+                            messagebox.setIcon(QtWidgets.QMessageBox.Warning)
+
+                            messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                            messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                            messagebox.exec_()
+
+                    # Add Text Tool
+                    if key == QtCore.Qt.Key_T or key == 'T':
+                        self.app.geo_editor.select_tool('text')
+
+                    # Substract Tool
+                    if key == QtCore.Qt.Key_U or key == 'U':
+                        if self.app.geo_editor.get_selected() is not None:
+                            self.app.geo_editor.union()
+                        else:
+                            msg = _("Please select geometry items \n"
+                                    "on which to perform union.")
+
+                            messagebox = QtWidgets.QMessageBox()
+                            messagebox.setText(msg)
+                            messagebox.setWindowTitle(_("Warning"))
+                            messagebox.setWindowIcon(QtGui.QIcon(self.app.resource_location + '/warning.png'))
+                            messagebox.setIcon(QtWidgets.QMessageBox.Warning)
+
+                            messagebox.setStandardButtons(QtWidgets.QMessageBox.Ok)
+                            messagebox.setDefaultButton(QtWidgets.QMessageBox.Ok)
+                            messagebox.exec_()
+
+                    # Flip on X axis
+                    if key == QtCore.Qt.Key_X or key == 'X':
+                        self.app.geo_editor.transform_tool.on_flipx()
+                        return
+
+                    # Flip on Y axis
+                    if key == QtCore.Qt.Key_Y or key == 'Y':
+                        self.app.geo_editor.transform_tool.on_flipy()
+                        return
+
+                # Show Shortcut list
+                if key == 'F3':
+                    self.app.on_shortcut_list()
+        elif self.app.call_source == 'grb_editor':
+            # CTRL
+            if modifiers == QtCore.Qt.ControlModifier:
+                # Eraser Tool
+                if key == QtCore.Qt.Key_E or key == 'E':
+                    self.app.grb_editor.on_eraser()
+                    return
+
+                # save (update) the current geometry and return to the App
+                if key == QtCore.Qt.Key_S or key == 'S':
+                    self.app.editor2object()
+                    return
+
+                # toggle the measurement tool
+                if key == QtCore.Qt.Key_M or key == 'M':
+                    self.app.distance_tool.run()
+                    return
+            # SHIFT
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                # Run Distance Minimum Tool
+                if key == QtCore.Qt.Key_M or key == 'M':
+                    self.app.distance_min_tool.run()
+                    return
+            # ALT
+            elif modifiers == QtCore.Qt.AltModifier:
+                # Mark Area Tool
+                if key == QtCore.Qt.Key_A or key == 'A':
+                    self.app.grb_editor.on_markarea()
+                    return
+
+                # Poligonize Tool
+                if key == QtCore.Qt.Key_N or key == 'N':
+                    self.app.grb_editor.on_poligonize()
+                    return
+                # Transformation Tool
+                if key == QtCore.Qt.Key_R or key == 'R':
+                    self.app.grb_editor.on_transform()
+                    return
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                # Abort the current action
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    # self.on_tool_select("select")
+                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+
+                    self.app.grb_editor.delete_utility_geometry()
+
+                    # self.app.grb_editor.plot_all()
+                    self.app.grb_editor.active_tool.clean_up()
+                    self.app.grb_editor.select_tool('select')
+                    return
+
+                # Delete selected object if delete key event comes out of canvas
+                if key == 'Delete':
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    if self.app.grb_editor.selected:
+                        self.app.grb_editor.delete_selected()
+                        self.app.grb_editor.plot_all()
+                    else:
+                        self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Nothing selected."))
+                    return
+
+                # Delete aperture in apertures table if delete key event comes from the Selected Tab
+                if key == QtCore.Qt.Key_Delete:
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    self.app.grb_editor.on_aperture_delete()
+                    return
+
+                if key == QtCore.Qt.Key_Minus or key == '-':
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    self.app.plotcanvas.zoom(1 / self.app.defaults['global_zoom_ratio'],
+                                             [self.app.grb_editor.snap_x, self.app.grb_editor.snap_y])
+                    return
+
+                if key == QtCore.Qt.Key_Equal or key == '=':
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    self.app.plotcanvas.zoom(self.app.defaults['global_zoom_ratio'],
+                                             [self.app.grb_editor.snap_x, self.app.grb_editor.snap_y])
+                    return
+
+                # toggle display of Notebook area
+                if key == QtCore.Qt.Key_QuoteLeft or key == '`':
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    self.on_toggle_notebook()
+                    return
+
+                # Switch to Project Tab
+                if key == QtCore.Qt.Key_1 or key == '1':
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    self.app.on_select_tab('project')
+                    return
+
+                # Switch to Selected Tab
+                if key == QtCore.Qt.Key_2 or key == '2':
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    self.app.on_select_tab('selected')
+                    return
+
+                # Switch to Tool Tab
+                if key == QtCore.Qt.Key_3 or key == '3':
+                    self.app.grb_editor.launched_from_shortcuts = True
+                    self.app.on_select_tab('tool')
+                    return
+
+                # we do this so we can reuse the following keys while inside a Tool
+                # the above keys are general enough so were left outside
+                if self.app.grb_editor.active_tool is not None and self.grb_select_btn.isChecked() is False:
+                    response = self.app.grb_editor.active_tool.on_key(key=key)
+                    if response is not None:
+                        self.app.inform.emit(response)
+                else:
+
+                    # Rotate
+                    if key == QtCore.Qt.Key_Space or key == 'Space':
+                        self.app.grb_editor.transform_tool.on_rotate_key()
+
+                    # Add Array of pads
+                    if key == QtCore.Qt.Key_A or key == 'A':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.inform.emit("Click on target point.")
+                        self.app.ui.add_pad_ar_btn.setChecked(True)
+
+                        self.app.grb_editor.x = self.app.mouse[0]
+                        self.app.grb_editor.y = self.app.mouse[1]
+
+                        self.app.grb_editor.select_tool('array')
+                        return
+
+                    # Scale Tool
+                    if key == QtCore.Qt.Key_B or key == 'B':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('buffer')
+                        return
+
+                    # Copy
+                    if key == QtCore.Qt.Key_C or key == 'C':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        if self.app.grb_editor.selected:
+                            self.app.inform.emit(_("Click on target point."))
+                            self.app.ui.aperture_copy_btn.setChecked(True)
+                            self.app.grb_editor.on_tool_select('copy')
+                            self.app.grb_editor.active_tool.set_origin(
+                                (self.app.grb_editor.snap_x, self.app.grb_editor.snap_y))
+                        else:
+                            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Nothing selected."))
+                        return
+
+                    # Add Disc Tool
+                    if key == QtCore.Qt.Key_D or key == 'D':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('disc')
+                        return
+
+                    # Add SemiDisc Tool
+                    if key == QtCore.Qt.Key_E or key == 'E':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('semidisc')
+                        return
+
+                    # Grid Snap
+                    if key == QtCore.Qt.Key_G or key == 'G':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        # make sure that the cursor shape is enabled/disabled, too
+                        if self.app.grb_editor.options['grid_snap'] is True:
+                            self.app.app_cursor.enabled = False
+                        else:
+                            self.app.app_cursor.enabled = True
+                        self.app.ui.grid_snap_btn.trigger()
+                        return
+
+                    # Jump to coords
+                    if key == QtCore.Qt.Key_J or key == 'J':
+                        self.app.on_jump_to()
+
+                    # Corner Snap
+                    if key == QtCore.Qt.Key_K or key == 'K':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.ui.corner_snap_btn.trigger()
+                        return
+
+                    # Move
+                    if key == QtCore.Qt.Key_M or key == 'M':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        if self.app.grb_editor.selected:
+                            self.app.inform.emit(_("Click on target point."))
+                            self.app.ui.aperture_move_btn.setChecked(True)
+                            self.app.grb_editor.on_tool_select('move')
+                            self.app.grb_editor.active_tool.set_origin(
+                                (self.app.grb_editor.snap_x, self.app.grb_editor.snap_y))
+                        else:
+                            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Nothing selected."))
+                        return
+
+                    # Add Region Tool
+                    if key == QtCore.Qt.Key_N or key == 'N':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('region')
+                        return
+
+                    # Add Pad Tool
+                    if key == QtCore.Qt.Key_P or key == 'P':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.inform.emit(_("Click on target point."))
+                        self.app.ui.add_pad_ar_btn.setChecked(True)
+
+                        self.app.grb_editor.x = self.app.mouse[0]
+                        self.app.grb_editor.y = self.app.mouse[1]
+
+                        self.app.grb_editor.select_tool('pad')
+                        return
+
+                    # Scale Tool
+                    if key == QtCore.Qt.Key_S or key == 'S':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.select_tool('scale')
+                        return
+
+                    # Add Track
+                    if key == QtCore.Qt.Key_T or key == 'T':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        # ## Current application units in Upper Case
+                        self.app.grb_editor.select_tool('track')
+                        return
+
+                    # Zoom fit
+                    if key == QtCore.Qt.Key_V or key == 'V':
+                        self.app.grb_editor.launched_from_shortcuts = True
+                        self.app.grb_editor.on_zoom_fit()
+                        return
+
+                # Show Shortcut list
+                if key == QtCore.Qt.Key_F3 or key == 'F3':
+                    self.app.on_shortcut_list()
+                    return
+        elif self.app.call_source == 'exc_editor':
+            # CTRL
+            if modifiers == QtCore.Qt.ControlModifier:
+                # save (update) the current geometry and return to the App
+                if key == QtCore.Qt.Key_S or key == 'S':
+                    self.app.editor2object()
+                    return
+
+                # toggle the measurement tool
+                if key == QtCore.Qt.Key_M or key == 'M':
+                    self.app.distance_tool.run()
+                    return
+
+                # we do this so we can reuse the following keys while inside a Tool
+                # the above keys are general enough so were left outside
+                if self.app.exc_editor.active_tool is not None and self.select_drill_btn.isChecked() is False:
+                    response = self.app.exc_editor.active_tool.on_key(key=key)
+                    if response is not None:
+                        self.app.inform.emit(response)
+                else:
+                    pass
+
+            # SHIFT
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                # Run Distance Minimum Tool
+                if key == QtCore.Qt.Key_M or key == 'M':
+                    self.app.distance_min_tool.run()
+                    return
+            # ALT
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                # Abort the current action
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
+
+                    self.app.exc_editor.delete_utility_geometry()
+
+                    self.app.exc_editor.active_tool.clean_up()
+
+                    self.app.exc_editor.select_tool('drill_select')
+                    return
+
+                # Delete selected object if delete key event comes out of canvas
+                if key == 'Delete':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    if self.app.exc_editor.selected:
+                        self.app.exc_editor.delete_selected()
+                        self.app.exc_editor.replot()
+                    else:
+                        self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Nothing selected."))
+                    return
+
+                # Delete tools in tools table if delete key event comes from the Selected Tab
+                if key == QtCore.Qt.Key_Delete:
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.app.exc_editor.on_tool_delete()
+                    return
+
+                if key == QtCore.Qt.Key_Minus or key == '-':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.app.plotcanvas.zoom(1 / self.app.defaults['global_zoom_ratio'],
+                                             [self.app.exc_editor.snap_x, self.app.exc_editor.snap_y])
+                    return
+
+                if key == QtCore.Qt.Key_Equal or key == '=':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.app.plotcanvas.zoom(self.app.defaults['global_zoom_ratio'],
+                                             [self.app.exc_editor.snap_x, self.app.exc_editor.snap_y])
+                    return
+
+                # toggle display of Notebook area
+                if key == QtCore.Qt.Key_QuoteLeft or key == '`':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.on_toggle_notebook()
+                    return
+
+                # Switch to Project Tab
+                if key == QtCore.Qt.Key_1 or key == '1':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.app.on_select_tab('project')
+                    return
+
+                # Switch to Selected Tab
+                if key == QtCore.Qt.Key_2 or key == '2':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.app.on_select_tab('selected')
+                    return
+
+                # Switch to Tool Tab
+                if key == QtCore.Qt.Key_3 or key == '3':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.app.on_select_tab('tool')
+                    return
+
+                # Grid Snap
+                if key == QtCore.Qt.Key_G or key == 'G':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    # make sure that the cursor shape is enabled/disabled, too
+                    if self.app.exc_editor.options['grid_snap'] is True:
+                        self.app.app_cursor.enabled = False
+                    else:
+                        self.app.app_cursor.enabled = True
+                    self.app.ui.grid_snap_btn.trigger()
+                    return
+
+                # Corner Snap
+                if key == QtCore.Qt.Key_K or key == 'K':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.app.ui.corner_snap_btn.trigger()
+                    return
+
+                # Zoom Fit
+                if key == QtCore.Qt.Key_V or key == 'V':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.app.on_zoom_fit()
+                    return
+
+                # Add Slot Hole Tool
+                if key == QtCore.Qt.Key_W or key == 'W':
+                    self.app.exc_editor.launched_from_shortcuts = True
+                    self.app.inform.emit(_("Click on target point."))
+                    self.app.ui.add_slot_btn.setChecked(True)
+
+                    self.app.exc_editor.x = self.app.mouse[0]
+                    self.app.exc_editor.y = self.app.mouse[1]
+
+                    self.app.exc_editor.select_tool('slot_add')
+                    return
+
+                # Show Shortcut list
+                if key == QtCore.Qt.Key_F3 or key == 'F3':
+                    self.app.on_shortcut_list()
+                    return
+
+                # Propagate to tool
+                # we do this so we can reuse the following keys while inside a Tool
+                # the above keys are general enough so were left outside
+                if self.app.exc_editor.active_tool is not None and self.select_drill_btn.isChecked() is False:
+                    response = self.app.exc_editor.active_tool.on_key(key=key)
+                    if response is not None:
+                        self.app.inform.emit(response)
+                else:
+                    # Add Array of Drill Hole Tool
+                    if key == QtCore.Qt.Key_A or key == 'A':
+                        self.app.exc_editor.launched_from_shortcuts = True
+                        self.app.inform.emit("Click on target point.")
+                        self.app.ui.add_drill_array_btn.setChecked(True)
+
+                        self.app.exc_editor.x = self.app.mouse[0]
+                        self.app.exc_editor.y = self.app.mouse[1]
+
+                        self.app.exc_editor.select_tool('drill_array')
+                        return
+
+                    # Copy
+                    if key == QtCore.Qt.Key_C or key == 'C':
+                        self.app.exc_editor.launched_from_shortcuts = True
+                        if self.app.exc_editor.selected:
+                            self.app.inform.emit(_("Click on target point."))
+                            self.app.ui.copy_drill_btn.setChecked(True)
+                            self.app.exc_editor.on_tool_select('drill_copy')
+                            self.app.exc_editor.active_tool.set_origin(
+                                (self.app.exc_editor.snap_x, self.app.exc_editor.snap_y))
+                        else:
+                            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Nothing selected."))
+                        return
+
+                    # Add Drill Hole Tool
+                    if key == QtCore.Qt.Key_D or key == 'D':
+                        self.app.exc_editor.launched_from_shortcuts = True
+                        self.app.inform.emit(_("Click on target point."))
+                        self.app.ui.add_drill_btn.setChecked(True)
+
+                        self.app.exc_editor.x = self.app.mouse[0]
+                        self.app.exc_editor.y = self.app.mouse[1]
+
+                        self.app.exc_editor.select_tool('drill_add')
+                        return
+
+                    # Jump to coords
+                    if key == QtCore.Qt.Key_J or key == 'J':
+                        self.app.on_jump_to()
+
+                    # Move
+                    if key == QtCore.Qt.Key_M or key == 'M':
+                        self.app.exc_editor.launched_from_shortcuts = True
+                        if self.app.exc_editor.selected:
+                            self.app.inform.emit(_("Click on target location ..."))
+                            self.app.ui.move_drill_btn.setChecked(True)
+                            self.app.exc_editor.on_tool_select('drill_move')
+                            self.app.exc_editor.active_tool.set_origin(
+                                (self.app.exc_editor.snap_x, self.app.exc_editor.snap_y))
+                        else:
+                            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled. Nothing selected."))
+                        return
+
+                    # Add Array of Slots Hole Tool
+                    if key == QtCore.Qt.Key_Q or key == 'Q':
+                        self.app.exc_editor.launched_from_shortcuts = True
+                        self.app.inform.emit("Click on target point.")
+                        self.app.ui.add_slot_array_btn.setChecked(True)
+
+                        self.app.exc_editor.x = self.app.mouse[0]
+                        self.app.exc_editor.y = self.app.mouse[1]
+
+                        self.app.exc_editor.select_tool('slot_array')
+                        return
+
+                    # Resize Tool
+                    if key == QtCore.Qt.Key_R or key == 'R':
+                        self.app.exc_editor.launched_from_shortcuts = True
+                        self.app.exc_editor.select_tool('drill_resize')
+                        return
+
+                    # Add Tool
+                    if key == QtCore.Qt.Key_T or key == 'T':
+                        self.app.exc_editor.launched_from_shortcuts = True
+                        # ## Current application units in Upper Case
+                        self.units = self.general_defaults_form.general_app_group.units_radio.get_value().upper()
+                        tool_add_popup = FCInputDoubleSpinner(title='%s ...' % _("New Tool"),
+                                                              text='%s:' % _('Enter a Tool Diameter'),
+                                                              min=0.0000, max=99.9999, decimals=self.decimals)
+                        tool_add_popup.set_icon(QtGui.QIcon(self.app.resource_location + '/letter_t_32.png'))
+
+                        val, ok = tool_add_popup.get_value()
+                        if ok:
+                            self.app.exc_editor.on_tool_add(tooldia=val)
+                            formated_val = '%.*f' % (self.decimals, float(val))
+                            self.app.inform.emit(
+                                '[success] %s: %s %s' % (_("Added new tool with dia"), formated_val, str(self.units))
+                            )
+                        else:
+                            self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Adding Tool cancelled"))
+                        return
+        elif self.app.call_source == 'gcode_editor':
+            # CTRL
+            if modifiers == QtCore.Qt.ControlModifier:
+                # save (update) the current geometry and return to the App
+                if key == QtCore.Qt.Key_S or key == 'S':
+                    self.app.editor2object()
+                    return
+            # SHIFT
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                pass
+            # ALT
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                pass
+        elif self.app.call_source == 'measurement':
+            if modifiers == QtCore.Qt.ControlModifier:
+                pass
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                pass
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    # abort the measurement action
+                    self.app.distance_tool.deactivate_measure_tool()
+                    self.app.inform.emit(_("Distance Tool exit..."))
+                    return
+
+                if key == QtCore.Qt.Key_G or key == 'G':
+                    self.app.ui.grid_snap_btn.trigger()
+                    return
+
+                # Jump to coords
+                if key == QtCore.Qt.Key_J or key == 'J':
+                    self.app.on_jump_to()
+        elif self.app.call_source == 'qrcode_tool':
+            # CTRL + ALT
+            if modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier:
+                if key == QtCore.Qt.Key_X:
+                    self.app.abort_all_tasks()
+                    return
+
+            elif modifiers == QtCore.Qt.ControlModifier:
+                pass
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                pass
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                # Escape = Deselect All
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    self.app.qrcode_tool.on_exit()
+
+                # Grid toggle
+                if key == QtCore.Qt.Key_G:
+                    self.app.ui.grid_snap_btn.trigger()
+
+                # Jump to coords
+                if key == QtCore.Qt.Key_J:
+                    self.app.on_jump_to()
+        elif self.app.call_source == 'copper_thieving_tool':
+            # CTRL + ALT
+            if modifiers == QtCore.Qt.ControlModifier | QtCore.Qt.AltModifier:
+                if key == QtCore.Qt.Key_X:
+                    self.app.abort_all_tasks()
+                    return
+            elif modifiers == QtCore.Qt.ControlModifier:
+                pass
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                pass
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                # Escape = Deselect All
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    self.app.copperfill_tool.on_exit()
+
+                # Grid toggle
+                if key == QtCore.Qt.Key_G:
+                    self.app.ui.grid_snap_btn.trigger()
+
+                # Jump to coords
+                if key == QtCore.Qt.Key_J:
+                    self.app.on_jump_to()
+        elif self.app.call_source == 'geometry':
+            if modifiers == QtCore.Qt.ControlModifier:
+                pass
+            elif modifiers == QtCore.Qt.AltModifier:
+                pass
+            elif modifiers == QtCore.Qt.ShiftModifier:
+                pass
+            # NO MODIFIER
+            elif modifiers == QtCore.Qt.NoModifier:
+                if key == QtCore.Qt.Key_Escape or key == 'Escape':
+                    sel_obj = self.app.collection.get_active()
+                    assert sel_obj.kind == 'geometry' or sel_obj.kind == 'excellon', \
+                        "Expected a Geometry or Excellon Object, got %s" % type(sel_obj)
+
+                    sel_obj.area_disconnect()
+                    return
+
+                if key == QtCore.Qt.Key_G or key == 'G':
+                    self.app.ui.grid_snap_btn.trigger()
+                    return
+
+                # Jump to coords
+                if key == QtCore.Qt.Key_J or key == 'J':
+                    self.app.on_jump_to()
+
+    def createPopupMenu(self):
+        menu = super().createPopupMenu()
+
+        menu.addSeparator()
+        menu.addAction(self.lock_action)
+        return menu
+
+    def lock_toolbar(self, lock=False):
+        """
+        Used to (un)lock the toolbars of the app.
+
+        :param lock: boolean, will lock all toolbars in place when set True
+        :return: None
+        """
+
+        if lock:
+            for widget in self.children():
+                if isinstance(widget, QtWidgets.QToolBar):
+                    widget.setMovable(False)
+        else:
+            for widget in self.children():
+                if isinstance(widget, QtWidgets.QToolBar):
+                    widget.setMovable(True)
+
+    def dragEnterEvent(self, event):
+        if event.mimeData().hasUrls:
+            event.accept()
+        else:
+            event.ignore()
+
+    def dragMoveEvent(self, event):
+        if event.mimeData().hasUrls:
+            event.accept()
+        else:
+            event.ignore()
+
+    def dropEvent(self, event):
+        if event.mimeData().hasUrls:
+            event.setDropAction(QtCore.Qt.CopyAction)
+            event.accept()
+            for url in event.mimeData().urls():
+                self.filename = str(url.toLocalFile())
+
+                if self.filename == "":
+                    self.app.inform.emit("Cancelled.")
+                else:
+                    extension = self.filename.lower().rpartition('.')[-1]
+
+                    if extension in self.app.grb_list:
+                        self.app.worker_task.emit({'fcn': self.app.f_handlers.open_gerber,
+                                                   'params': [self.filename]})
+                    else:
+                        event.ignore()
+
+                    if extension in self.app.exc_list:
+                        self.app.worker_task.emit({'fcn': self.app.f_handlers.open_excellon,
+                                                   'params': [self.filename]})
+                    else:
+                        event.ignore()
+
+                    if extension in self.app.gcode_list:
+                        self.app.worker_task.emit({'fcn': self.app.f_handlers.open_gcode,
+                                                   'params': [self.filename]})
+                    else:
+                        event.ignore()
+
+                    if extension in self.app.svg_list:
+                        object_type = 'geometry'
+                        self.app.worker_task.emit({'fcn': self.app.f_handlers.import_svg,
+                                                   'params': [self.filename, object_type, None]})
+
+                    if extension in self.app.dxf_list:
+                        object_type = 'geometry'
+                        self.app.worker_task.emit({'fcn': self.app.f_handlers.import_dxf,
+                                                   'params': [self.filename, object_type, None]})
+
+                    if extension in self.app.pdf_list:
+                        self.app.pdf_tool.periodic_check(1000)
+                        self.app.worker_task.emit({'fcn': self.app.pdf_tool.open_pdf,
+                                                   'params': [self.filename]})
+
+                    if extension in self.app.prj_list:
+                        # self.app.open_project() is not Thread Safe
+                        self.app.f_handlers.open_project(self.filename)
+
+                    if extension in self.app.conf_list:
+                        self.app.f_handlers.open_config_file(self.filename)
+                    else:
+                        event.ignore()
+        else:
+            event.ignore()
+
+    def closeEvent(self, event):
+        if self.app.save_in_progress:
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Application is saving the project. Please wait ..."))
+        else:
+            grect = self.geometry()
+
+            # self.splitter.sizes()[0] is actually the size of the "notebook"
+            if not self.isMaximized():
+                self.geom_update.emit(grect.x(), grect.y(), grect.width(), grect.height(), self.splitter.sizes()[0])
+
+            self.final_save.emit()
+        event.ignore()
+
+    def on_fullscreen(self, disable=False):
+        """
+
+        :param disable:
+        :return:
+        """
+        flags = self.windowFlags()
+        if self.toggle_fscreen is False and disable is False:
+            # self.ui.showFullScreen()
+            self.setWindowFlags(flags | Qt.FramelessWindowHint)
+            a = self.geometry()
+            self.x_pos = a.x()
+            self.y_pos = a.y()
+            self.width = a.width()
+            self.height = a.height()
+            self.titlebar_height = self.app.qapp.style().pixelMetric(QtWidgets.QStyle.PM_TitleBarHeight)
+
+            # set new geometry to full desktop rect
+            # Subtracting and adding the pixels below it's hack to bypass a bug in Qt5 and OpenGL that made that a
+            # window drawn with OpenGL in fullscreen will not show any other windows on top which means that menus and
+            # everything else will not work without this hack. This happen in Windows.
+            # https://bugreports.qt.io/browse/QTBUG-41309
+            desktop = self.app.qapp.desktop()
+            screen = desktop.screenNumber(QtGui.QCursor.pos())
+
+            rec = desktop.screenGeometry(screen)
+            x = rec.x() - 1
+            y = rec.y() - 1
+            h = rec.height() + 2
+            w = rec.width() + 2
+
+            self.setGeometry(x, y, w, h)
+            self.show()
+
+            # hide all Toolbars
+            for tb in self.findChildren(QtWidgets.QToolBar):
+                tb.setVisible(False)
+
+            self.coords_toolbar.setVisible(self.app.defaults["global_coordsbar_show"])
+            self.delta_coords_toolbar.setVisible(self.app.defaults["global_delta_coordsbar_show"])
+            self.grid_toolbar.setVisible(self.app.defaults["global_gridbar_show"])
+            self.status_toolbar.setVisible(self.app.defaults["global_statusbar_show"])
+
+            self.splitter.setSizes([0, 1])
+            self.toggle_fscreen = True
+        elif self.toggle_fscreen is True or disable is True:
+            self.setWindowFlags(flags & ~Qt.FramelessWindowHint)
+            # the additions are made to account for the pixels we subtracted/added above in the (x, y, h, w)
+            self.setGeometry(self.x_pos+1, self.y_pos+self.titlebar_height+4, self.width, self.height)
+            self.showNormal()
+            self.restore_toolbar_view()
+            self.toggle_fscreen = False
+
+    def on_toggle_plotarea(self):
+        """
+
+        :return:
+        """
+        try:
+            name = self.plot_tab_area.widget(0).objectName()
+        except AttributeError:
+            self.plot_tab_area.addTab(self.plot_tab, _("Plot Area"))
+            # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON
+            self.plot_tab_area.protectTab(0)
+            return
+
+        if name != 'plotarea_tab':
+            self.plot_tab_area.insertTab(0, self.plot_tab, _("Plot Area"))
+            # remove the close button from the Plot Area tab (first tab index = 0) as this one will always be ON
+            self.plot_tab_area.protectTab(0)
+        else:
+            self.plot_tab_area.closeTab(0)
+
+    def on_toggle_notebook(self):
+        """
+
+        :return:
+        """
+        if self.splitter.sizes()[0] == 0:
+            self.splitter.setSizes([1, 1])
+            self.menu_toggle_nb.setChecked(True)
+        else:
+            self.splitter.setSizes([0, 1])
+            self.menu_toggle_nb.setChecked(False)
+
+    def on_toggle_grid(self):
+        """
+
+        :return:
+        """
+        self.grid_snap_btn.trigger()
+
+    def toggle_shell_ui(self):
+        """
+        Toggle shell dock: if is visible close it, if it is closed then open it
+
+        :return: None
+        """
+
+        if self.shell_dock.isVisible():
+            self.shell_dock.hide()
+            self.app.plotcanvas.native.setFocus()
+        else:
+            self.shell_dock.show()
+
+            # I want to take the focus and give it to the Tcl Shell when the Tcl Shell is run
+            # self.shell._edit.setFocus()
+            QtCore.QTimer.singleShot(0, lambda: self.shell_dock.widget()._edit.setFocus())
+
+            # HACK - simulate a mouse click - alternative
+            # no_km = QtCore.Qt.KeyboardModifier(QtCore.Qt.NoModifier)    # no KB modifier
+            # pos = QtCore.QPoint((self.shell._edit.width() - 40), (self.shell._edit.height() - 2))
+            # e = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, pos, QtCore.Qt.LeftButton, QtCore.Qt.LeftButton,
+            #                       no_km)
+            # QtWidgets.qApp.sendEvent(self.shell._edit, e)
+            # f = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonRelease, pos, QtCore.Qt.LeftButton, QtCore.Qt.LeftButton,
+            #                       no_km)
+            # QtWidgets.qApp.sendEvent(self.shell._edit, f)
+
+    def on_shelldock_toggled(self, visibility):
+        if visibility is True:
+            self.shell_status_label.setStyleSheet("""
+                                                  QLabel
+                                                  {
+                                                      color: black;
+                                                      background-color: lightcoral;
+                                                  }
+                                                  """)
+            self.app.inform[str, bool].emit(_("Shell enabled."), False)
+        else:
+            self.shell_status_label.setStyleSheet("")
+            self.app.inform[str, bool].emit(_("Shell disabled."), False)
+
+
+class ShortcutsTab(QtWidgets.QWidget):
+
+    def __init__(self):
+        super(ShortcutsTab, self).__init__()
+
+        self.sh_tab_layout = QtWidgets.QVBoxLayout()
+        self.sh_tab_layout.setContentsMargins(2, 2, 2, 2)
+        self.setLayout(self.sh_tab_layout)
+
+        self.sh_hlay = QtWidgets.QHBoxLayout()
+
+        self.sh_title = QtWidgets.QTextEdit('<b>%s</b>' % _('Shortcut Key List'))
+        self.sh_title.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
+        self.sh_title.setFrameStyle(QtWidgets.QFrame.NoFrame)
+        self.sh_title.setMaximumHeight(30)
+
+        font = self.sh_title.font()
+        font.setPointSize(12)
+        self.sh_title.setFont(font)
+
+        self.sh_tab_layout.addWidget(self.sh_title)
+        self.sh_tab_layout.addLayout(self.sh_hlay)
+
+        self.app_sh_msg = (
+                '''<b>%s</b><br>
+            <table border="0" cellpadding="0" cellspacing="0" style="width:283px">
+                <tbody>
+                    <tr height="20">
+                        <td height="20" width="89"><strong>%s</strong></td>
+                        <td width="194"><span style="color:#006400"><strong>&nbsp;%s</strong></span></td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong%s>T</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>&#39;%s&#39;</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>&#39;%s&#39;</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>                   
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr> 
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>'%s'</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                </tbody>
+            </table>
+            ''' %
+                (
+                    _("General Shortcut list"),
+                    _('F3'), _("SHOW SHORTCUT LIST"),
+                    _('1'), _("Switch to Project Tab"),
+                    _('2'), _("Switch to Selected Tab"),
+                    _('3'), _("Switch to Tool Tab"),
+                    _('B'), _("New Gerber"),
+                    _('E'), _("Edit Object (if selected)"),
+                    _('G'), _("Grid On/Off"),
+                    _('J'), _("Jump to Coordinates"),
+                    _('L'), _("New Excellon"),
+                    _('M'), _("Move Obj"),
+                    _('N'), _("New Geometry"),
+                    _('O'), _("Set Origin"),
+                    _('Q'), _("Change Units"),
+                    _('P'), _("Open Properties Tool"),
+                    _('R'), _("Rotate by 90 degree CW"),
+                    _('S'), _("Shell Toggle"),
+                    _('T'), _("Add a Tool (when in Geometry Selected Tab or in Tools NCC or Tools Paint)"),
+                    _('V'), _("Zoom Fit"),
+                    _('X'), _("Flip on X_axis"),
+                    _('Y'), _("Flip on Y_axis"),
+                    _('-'), _("Zoom Out"),
+                    _('='), _("Zoom In"),
+
+                    # CTRL section
+                    _('Ctrl+A'), _("Select All"),
+                    _('Ctrl+C'), _("Copy Obj"),
+                    _('Ctrl+D'), _("Open Tools Database"),
+                    _('Ctrl+E'), _("Open Excellon File"),
+                    _('Ctrl+G'), _("Open Gerber File"),
+                    _('Ctrl+M'), _("Distance Tool"),
+                    _('Ctrl+N'), _("New Project"),
+                    _('Ctrl+O'), _("Open Project"),
+                    _('Ctrl+P'), _("Print (PDF)"),
+                    _('Ctrl+Q'), _("PDF Import Tool"),
+                    _('Ctrl+S'), _("Save Project"),
+                    _('Ctrl+F10'), _("Toggle Plot Area"),
+
+                    # SHIFT section
+                    _('Shift+A'), _("Toggle the axis"),
+                    _('Shift+C'), _("Copy Obj_Name"),
+                    _('Shift+E'), _("Toggle Code Editor"),
+                    _('Shift+G'), _("Toggle Grid Lines"),
+                    _('Shift+H'), _("Toggle HUD"),
+                    _('Shift+J'), _("Locate in Object"),
+                    _('Shift+M'), _("Distance Minimum Tool"),
+                    _('Shift+P'), _("Open Preferences Window"),
+                    _('Shift+R'), _("Rotate by 90 degree CCW"),
+                    _('Shift+S'), _("Run a Script"),
+                    _('Shift+W'), _("Toggle the workspace"),
+                    _('Shift+X'), _("Skew on X axis"),
+                    _('Shift+Y'), _("Skew on Y axis"),
+
+                    # ALT section
+                    _('Alt+A'), _("Align Objects Tool"),
+                    _('Alt+C'), _("Calculators Tool"),
+                    _('Alt+D'), _("2-Sided PCB Tool"),
+                    _('Alt+E'), _("Extract Drills Tool"),
+                    _('Alt+F'), _("Fiducials Tool"),
+                    _('Alt+G'), _("Invert Gerber Tool"),
+                    _('Alt+H'), _("Punch Gerber Tool"),
+                    _('Alt+I'), _("Isolation Tool"),
+                    _('Alt+J'), _("Copper Thieving Tool"),
+                    _('Alt+K'), _("Solder Paste Dispensing Tool"),
+                    _('Alt+L'), _("Film PCB Tool"),
+                    _('Alt+M'), _("Corner Markers Tool"),
+                    _('Alt+N'), _("Non-Copper Clearing Tool"),
+                    _('Alt+O'), _("Optimal Tool"),
+                    _('Alt+P'), _("Paint Area Tool"),
+                    _('Alt+Q'), _("QRCode Tool"),
+                    _('Alt+R'), _("Rules Check Tool"),
+                    _('Alt+S'), _("View File Source"),
+                    _('Alt+T'), _("Transformations Tool"),
+                    _('Alt+W'), _("Subtract Tool"),
+                    _('Alt+X'), _("Cutout PCB Tool"),
+                    _('Alt+Z'), _("Panelize PCB"),
+                    _('Alt+1'), _("Enable all"),
+                    _('Alt+2'), _("Disable all"),
+                    _('Alt+3'), _("Enable Non-selected Objects"),
+                    _('Alt+4'), _("Disable Non-selected Objects"),
+                    _('Alt+F10'), _("Toggle Full Screen"),
+
+                    # CTRL + ALT section
+                    _('Ctrl+Alt+X'), _("Abort current task (gracefully)"),
+
+                    # CTRL + SHIFT section
+                    _('Ctrl+Shift+S'), _("Save Project As"),
+                    _('Ctrl+Shift+V'), _("Paste Special. "
+                                         "Will convert a Windows path style to the one required in Tcl Shell"),
+
+                    # F keys section
+                    _('F1'), _("Open Online Manual"),
+                    _('F4'), _("Open Online Tutorials"),
+                    _('F5'), _("Refresh Plots"),
+                    _('Del'), _("Delete Object"),
+                    _('Del'), _("Alternate: Delete Tool"),
+                    _('`'), _("(left to Key_1)Toggle Notebook Area (Left Side)"),
+                    _('Space'), _("En(Dis)able Obj Plot"),
+                    _('Esc'), _("Deselects all objects")
+                )
+        )
+
+        self.sh_app = QtWidgets.QTextEdit()
+        self.sh_app.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
+
+        self.sh_app.setText(self.app_sh_msg)
+        self.sh_app.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
+        self.sh_hlay.addWidget(self.sh_app)
+
+        editor_title = """
+        <b>%s</b><br>
+        <br>
+        """ % _("Editor Shortcut list")
+
+        # GEOMETRY EDITOR SHORTCUT LIST
+        geo_sh_messages = """
+        <strong><span style="color:#0000ff">%s</span></strong><br>
+        <table border="0" cellpadding="0" cellspacing="0" style="width:283px">
+                <tbody>
+                    <tr height="20">
+                        <td height="20" width="89"><strong>%s</strong></td>
+                        <td width="194">&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20">&nbsp;</td>
+                        <td>&nbsp;</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                    <tr height="20">
+                        <td height="20"><strong>%s</strong></td>
+                        <td>&nbsp;%s</td>
+                    </tr>
+                </tbody>
+            </table>
+            <br>
+        """ % (
+            _("GEOMETRY EDITOR"),
+            _('A'), _("Draw an Arc"),
+            _('B'), _("Buffer Tool"),
+            _('C'), _("Copy Geo Item"),
+            _('D'), _("Within Add Arc will toogle the ARC direction: CW or CCW"),
+            _('E'), _("Polygon Intersection Tool"),
+            _('I'), _("Geo Paint Tool"),
+            _('J'), _("Jump to Location (x, y)"),
+            _('K'), _("Toggle Corner Snap"),
+            _('M'), _("Move Geo Item"),
+            _('M'), _("Within Add Arc will cycle through the ARC modes"),
+            _('N'), _("Draw a Polygon"),
+            _('O'), _("Draw a Circle"),
+            _('P'), _("Draw a Path"),
+            _('R'), _("Draw Rectangle"),
+            _('S'), _("Polygon Subtraction Tool"),
+            _('T'), _("Add Text Tool"),
+            _('U'), _("Polygon Union Tool"),
+            _('X'), _("Flip shape on X axis"),
+            _('Y'), _("Flip shape on Y axis"),
+            _('Shift+M'), _("Distance Minimum Tool"),
+            _('Shift+X'), _("Skew shape on X axis"),
+            _('Shift+Y'), _("Skew shape on Y axis"),
+            _('Alt+R'), _("Editor Transformation Tool"),
+            _('Alt+X'), _("Offset shape on X axis"),
+            _('Alt+Y'), _("Offset shape on Y axis"),
+            _('Ctrl+M'), _("Distance Tool"),
+            _('Ctrl+S'), _("Save Object and Exit Editor"),
+            _('Ctrl+X'), _("Polygon Cut Tool"),
+            _('Space'), _("Rotate Geometry"),
+            _('ENTER'), _("Finish drawing for certain tools"),
+            _('Esc'), _("Abort and return to Select"),
+            _('Del'), _("Delete Shape")
+        )
+
+        # EXCELLON EDITOR SHORTCUT LIST
+        exc_sh_messages = """
+        <br>
+        <strong><span style="color:#ff0000">%s</span></strong><br>
+        <table border="0" cellpadding="0" cellspacing="0" style="width:283px">
+            <tbody>
+                <tr height="20">
+                    <td height="20" width="89"><strong>%s</strong></td>
+                    <td width="194">&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20" width="89"><strong>%s</strong></td>
+                    <td width="194">&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20" width="89"><strong>%s</strong></td>
+                    <td width="194">&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20">&nbsp;</td>
+                    <td>&nbsp;</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20">&nbsp;</td>
+                    <td>&nbsp;</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20">&nbsp;</td>
+                    <td>&nbsp;</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+            </tbody>
+        </table>
+        <br>
+        """ % (
+            _("EXCELLON EDITOR"),
+            _('A'), _("Add Drill Array"),
+            _('C'), _("Copy Drill"),
+            _('D'), _("Add Drill"),
+            _('J'), _("Jump to Location (x, y)"),
+            _('M'), _("Move Drill"),
+            _('Q'), _("Add Slot Array"),
+            _('R'), _("Resize Drill"),
+            _('T'), _("Add a new Tool"),
+            _('W'), _("Add Slot"),
+            _('Shift+M'), _("Distance Minimum Tool"),
+            _('Del'), _("Delete Drill"),
+            _('Del'), _("Alternate: Delete Tool"),
+            _('Esc'), _("Abort and return to Select"),
+            _('Space'), _("Toggle Slot direction"),
+            _('Ctrl+S'), _("Save Object and Exit Editor"),
+            _('Ctrl+Space'), _("Toggle array direction")
+        )
+
+        # GERBER EDITOR SHORTCUT LIST
+        grb_sh_messages = """
+        <br>
+        <strong><span style="color:#00ff00">%s</span></strong><br>
+        <table border="0" cellpadding="0" cellspacing="0" style="width:283px">
+            <tbody>
+                <tr height="20">
+                    <td height="20" width="89"><strong>%s</strong></td>
+                    <td width="194">&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20">&nbsp;</td>
+                    <td>&nbsp;</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20">&nbsp;</td>
+                    <td>&nbsp;</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20">&nbsp;</td>
+                    <td>&nbsp;</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20">&nbsp;</td>
+                    <td>&nbsp;</td>
+                </tr>
+                 <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+                <tr height="20">
+                    <td height="20"><strong>%s</strong></td>
+                    <td>&nbsp;%s</td>
+                </tr>
+            </tbody>
+        </table>
+        <br>
+        """ % (
+            _("GERBER EDITOR"),
+            _('A'), _("Add Pad Array"),
+            _('B'), _("Buffer"),
+            _('C'), _("Copy"),
+            _('D'), _("Add Disc"),
+            _('E'), _("Add SemiDisc"),
+            _('J'), _("Jump to Location (x, y)"),
+            _('M'), _("Move"),
+            _('N'), _("Add Region"),
+            _('P'), _("Add Pad"),
+            _('R'), _("Within Track & Region Tools will cycle in REVERSE the bend modes"),
+            _('S'), _("Scale"),
+            _('T'), _("Add Track"),
+            _('T'), _("Within Track & Region Tools will cycle FORWARD the bend modes"),
+            _('Del'), _("Delete"),
+            _('Del'), _("Alternate: Delete Apertures"),
+            _('Esc'), _("Abort and return to Select"),
+            _('Space'), _("Toggle array direction"),
+            _('Shift+M'), _("Distance Minimum Tool"),
+            _('Ctrl+E'), _("Eraser Tool"),
+            _('Ctrl+S'), _("Save Object and Exit Editor"),
+            _('Alt+A'), _("Mark Area Tool"),
+            _('Alt+N'), _("Poligonize Tool"),
+            _('Alt+R'), _("Transformation Tool")
+        )
+
+        self.editor_sh_msg = editor_title + geo_sh_messages + grb_sh_messages + exc_sh_messages
+
+        self.sh_editor = QtWidgets.QTextEdit()
+        self.sh_editor.setTextInteractionFlags(QtCore.Qt.NoTextInteraction)
+        self.sh_editor.setText(self.editor_sh_msg)
+        self.sh_editor.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
+        self.sh_hlay.addWidget(self.sh_editor)
+
+# end of file

+ 2943 - 0
appGUI/ObjectUI.py

@@ -0,0 +1,2943 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://flatcam.org                                       #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+# ##########################################################
+
+# ##########################################################
+# File Modified (major mod): Marius Adrian Stanciu         #
+# Date: 3/10/2019                                          #
+# ##########################################################
+
+from appGUI.GUIElements import *
+import sys
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QtCore.QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ObjectUI(QtWidgets.QWidget):
+    """
+    Base class for the UI of FlatCAM objects. Deriving classes should
+    put UI elements in ObjectUI.custom_box (QtWidgets.QLayout).
+    """
+
+    def __init__(self, app, icon_file='assets/resources/flatcam_icon32.png', title=_('App Object'),
+                 parent=None, common=True):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+
+        self.app = app
+        self.decimals = app.decimals
+
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if theme_settings.contains("theme"):
+            theme = theme_settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            self.resource_loc = 'assets/resources'
+        else:
+            self.resource_loc = 'assets/resources'
+
+        layout = QtWidgets.QVBoxLayout()
+        self.setLayout(layout)
+
+        # ## Page Title box (spacing between children)
+        self.title_box = QtWidgets.QHBoxLayout()
+        layout.addLayout(self.title_box)
+
+        # ## Page Title icon
+        pixmap = QtGui.QPixmap(icon_file.replace('assets/resources', self.resource_loc))
+        self.icon = FCLabel()
+        self.icon.setPixmap(pixmap)
+        self.title_box.addWidget(self.icon, stretch=0)
+
+        # ## Title label
+        self.title_label = FCLabel("<font size=5><b>%s</b></font>" % title)
+        self.title_label.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.title_label, stretch=1)
+
+        # ## App Level label
+        self.level = FCLabel("")
+        self.level.setToolTip(
+            _(
+                "BASIC is suitable for a beginner. Many parameters\n"
+                "are hidden from the user in this mode.\n"
+                "ADVANCED mode will make available all parameters.\n\n"
+                "To change the application LEVEL, go to:\n"
+                "Edit -> Preferences -> General and check:\n"
+                "'APP. LEVEL' radio button."
+            )
+        )
+        self.level.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
+        self.title_box.addWidget(self.level)
+
+        # ## Box box for custom widgets
+        # This gets populated in offspring implementations.
+        self.custom_box = QtWidgets.QVBoxLayout()
+        layout.addLayout(self.custom_box)
+
+        # ###########################
+        # ## Common to all objects ##
+        # ###########################
+        if common is True:
+            self.common_grid = QtWidgets.QGridLayout()
+            self.common_grid.setColumnStretch(0, 1)
+            self.common_grid.setColumnStretch(1, 0)
+            layout.addLayout(self.common_grid)
+
+            # self.common_grid.addWidget(FCLabel(''), 1, 0, 1, 2)
+            separator_line = QtWidgets.QFrame()
+            separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+            separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+            self.common_grid.addWidget(separator_line, 1, 0, 1, 2)
+
+            self.transform_label = FCLabel('<b>%s</b>' % _('Transformations'))
+            self.transform_label.setToolTip(
+                _("Geometrical transformations of the current object.")
+            )
+
+            self.common_grid.addWidget(self.transform_label, 2, 0, 1, 2)
+
+            # ### Scale ####
+            self.scale_entry = NumericalEvalEntry(border_color='#0069A9')
+            self.scale_entry.set_value(1.0)
+            self.scale_entry.setToolTip(
+                _("Factor by which to multiply\n"
+                  "geometric features of this object.\n"
+                  "Expressions are allowed. E.g: 1/25.4")
+            )
+            # GO Button
+            self.scale_button = FCButton(_('Scale'))
+            self.scale_button.setToolTip(
+                _("Perform scaling operation.")
+            )
+            self.scale_button.setMinimumWidth(70)
+
+            self.common_grid.addWidget(self.scale_entry, 3, 0)
+            self.common_grid.addWidget(self.scale_button, 3, 1)
+
+            # ### Offset ####
+            self.offsetvector_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+            self.offsetvector_entry.setText("(0.0, 0.0)")
+            self.offsetvector_entry.setToolTip(
+                _("Amount by which to move the object\n"
+                  "in the x and y axes in (x, y) format.\n"
+                  "Expressions are allowed. E.g: (1/3.2, 0.5*3)")
+            )
+
+            self.offset_button = FCButton(_('Offset'))
+            self.offset_button.setToolTip(
+                _("Perform the offset operation.")
+            )
+            self.offset_button.setMinimumWidth(70)
+
+            self.common_grid.addWidget(self.offsetvector_entry, 4, 0)
+            self.common_grid.addWidget(self.offset_button, 4, 1)
+
+            self.transformations_button = FCButton(_('Transformations'))
+            self.transformations_button.setIcon(QtGui.QIcon(self.app.resource_location + '/transform.png'))
+            self.transformations_button.setToolTip(
+                _("Geometrical transformations of the current object.")
+            )
+            self.common_grid.addWidget(self.transformations_button, 5, 0, 1, 2)
+
+        layout.addStretch()
+    
+    def confirmation_message(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit('[WARNING_NOTCL] %s: [%.*f, %.*f]' % (_("Edited value is out of range"),
+                                                                                  self.decimals,
+                                                                                  minval,
+                                                                                  self.decimals,
+                                                                                  maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+    def confirmation_message_int(self, accepted, minval, maxval):
+        if accepted is False:
+            self.app.inform[str, bool].emit(
+                '[WARNING_NOTCL] %s: [%d, %d]' % (_("Edited value is out of range"), minval, maxval), False)
+        else:
+            self.app.inform[str, bool].emit('[success] %s' % _("Edited value is within limits."), False)
+
+
+class GerberObjectUI(ObjectUI):
+    """
+    User interface for Gerber objects.
+    """
+
+    def __init__(self, app, parent=None):
+        self.decimals = app.decimals
+        self.app = app
+
+        ObjectUI.__init__(self, title=_('Gerber Object'), parent=parent, app=self.app)
+
+        # Plot options
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.custom_box.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        self.plot_options_label = FCLabel("<b>%s:</b>" % _("Plot Options"))
+
+        grid0.addWidget(self.plot_options_label, 0, 0)
+
+        # Solid CB
+        self.solid_cb = FCCheckBox(label=_('Solid'))
+        self.solid_cb.setToolTip(
+            _("Solid color polygons.")
+        )
+        grid0.addWidget(self.solid_cb, 0, 1)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label=_('Multi-Color'))
+        self.multicolored_cb.setToolTip(
+            _("Draw polygons in different colors.")
+        )
+        grid0.addWidget(self.multicolored_cb, 0, 2)
+
+        # ## Object name
+        self.name_hlay = QtWidgets.QHBoxLayout()
+        grid0.addLayout(self.name_hlay, 1, 0, 1, 3)
+
+        name_label = FCLabel("<b>%s:</b>" % _("Name"))
+        self.name_entry = FCEntry()
+        self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
+        self.name_hlay.addWidget(name_label)
+        self.name_hlay.addWidget(self.name_entry)
+
+        # Plot CB
+        self.plot_lbl = FCLabel('%s:' % _("Plot"))
+        self.plot_lbl.setToolTip(_("Plot (show) this object."))
+        self.plot_cb = FCCheckBox()
+
+        grid0.addWidget(self.plot_lbl, 2, 0)
+        grid0.addWidget(self.plot_cb, 2, 1)
+
+        # Generate 'Follow'
+        self.follow_cb = FCCheckBox('%s' % _("Follow"))
+        self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
+                                    "This means that it will cut through\n"
+                                    "the middle of the trace."))
+        grid0.addWidget(self.follow_cb, 2, 2)
+
+        # Editor
+        self.editor_button = FCButton(_('Gerber Editor'))
+        self.editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/edit_file32.png'))
+        self.editor_button.setToolTip(
+            _("Start the Object Editor")
+        )
+        self.editor_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid0.addWidget(self.editor_button, 4, 0, 1, 3)
+
+        # PROPERTIES CB
+        self.properties_button = FCButton('%s' % _("PROPERTIES"), checkable=True)
+        self.properties_button.setIcon(QtGui.QIcon(self.app.resource_location + '/properties32.png'))
+        self.properties_button.setToolTip(_("Show the Properties."))
+        self.properties_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid0.addWidget(self.properties_button, 6, 0, 1, 3)
+
+        # PROPERTIES Frame
+        self.properties_frame = QtWidgets.QFrame()
+        self.properties_frame.setContentsMargins(0, 0, 0, 0)
+        grid0.addWidget(self.properties_frame, 7, 0, 1, 3)
+        self.properties_box = QtWidgets.QVBoxLayout()
+        self.properties_box.setContentsMargins(0, 0, 0, 0)
+        self.properties_frame.setLayout(self.properties_box)
+        self.properties_frame.hide()
+
+        self.treeWidget = FCTree(columns=2)
+
+        self.properties_box.addWidget(self.treeWidget)
+        self.properties_box.setStretch(0, 0)
+
+        # ### Gerber Apertures ####
+        self.apertures_table_label = FCLabel('%s:' % _('Apertures'))
+        self.apertures_table_label.setToolTip(
+            _("Apertures Table for the Gerber Object.")
+        )
+
+        grid0.addWidget(self.apertures_table_label, 8, 0)
+
+        # Aperture Table Visibility CB
+        self.aperture_table_visibility_cb = FCCheckBox()
+        self.aperture_table_visibility_cb.setToolTip(
+            _("Toggle the display of the Tools Table.")
+        )
+        # self.aperture_table_visibility_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
+        grid0.addWidget(self.aperture_table_visibility_cb, 8, 1)
+
+        hlay_plot = QtWidgets.QHBoxLayout()
+        grid0.addLayout(hlay_plot, 8, 2)
+
+        # Aperture Mark all CB
+        self.mark_all_cb = FCCheckBox(_('Mark All'))
+        self.mark_all_cb.setToolTip(
+            _("When checked it will display all the apertures.\n"
+              "When unchecked, it will delete all mark shapes\n"
+              "that are drawn on canvas.")
+
+        )
+        self.mark_all_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
+        hlay_plot.addStretch()
+        hlay_plot.addWidget(self.mark_all_cb)
+
+        # Apertures Table
+        self.apertures_table = FCTable()
+        grid0.addWidget(self.apertures_table, 10, 0, 1, 3)
+
+        self.apertures_table.setColumnCount(6)
+        self.apertures_table.setHorizontalHeaderLabels(['#', _('Code'), _('Type'), _('Size'), _('Dim'), 'M'])
+        self.apertures_table.setSortingEnabled(False)
+
+        self.apertures_table.horizontalHeaderItem(0).setToolTip(
+            _("Index"))
+        self.apertures_table.horizontalHeaderItem(1).setToolTip(
+            _("Aperture Code"))
+        self.apertures_table.horizontalHeaderItem(2).setToolTip(
+            _("Type of aperture: circular, rectangle, macros etc"))
+        self.apertures_table.horizontalHeaderItem(4).setToolTip(
+            _("Aperture Size:"))
+        self.apertures_table.horizontalHeaderItem(4).setToolTip(
+            _("Aperture Dimensions:\n"
+              " - (width, height) for R, O type.\n"
+              " - (dia, nVertices) for P type"))
+        self.apertures_table.horizontalHeaderItem(5).setToolTip(
+            _("Mark the aperture instances on canvas."))
+        # self.apertures_table.setColumnHidden(5, True)
+
+        # start with apertures table hidden
+        self.apertures_table.setVisible(False)
+
+        # Buffer Geometry
+        self.create_buffer_button = FCButton(_('Buffer Solid Geometry'))
+        self.create_buffer_button.setToolTip(
+            _("This button is shown only when the Gerber file\n"
+              "is loaded without buffering.\n"
+              "Clicking this will create the buffered geometry\n"
+              "required for isolation.")
+        )
+        grid0.addWidget(self.create_buffer_button, 12, 0, 1, 3)
+
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line1, 13, 0, 1, 3)
+
+        self.tool_lbl = FCLabel('<b>%s</b>' % _("TOOLS"))
+        grid0.addWidget(self.tool_lbl, 14, 0, 1, 3)
+
+        # Isolation Tool - will create isolation paths around the copper features
+        self.iso_button = FCButton(_('Isolation Routing'))
+        # self.iso_button.setIcon(QtGui.QIcon(self.app.resource_location + '/iso_16.png'))
+        self.iso_button.setToolTip(
+            _("Create a Geometry object with\n"
+              "toolpaths to cut around polygons.")
+        )
+        self.iso_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid0.addWidget(self.iso_button, 16, 0, 1, 3)
+
+        # ## Clear non-copper regions
+        self.generate_ncc_button = FCButton(_('NCC Tool'))
+        self.generate_ncc_button.setIcon(QtGui.QIcon(self.app.resource_location + '/eraser26.png'))
+        self.generate_ncc_button.setToolTip(
+            _("Create the Geometry Object\n"
+              "for non-copper routing.")
+        )
+        # self.generate_ncc_button.setStyleSheet("""
+        #                 QPushButton
+        #                 {
+        #                     font-weight: bold;
+        #                 }
+        #                 """)
+        grid0.addWidget(self.generate_ncc_button, 18, 0, 1, 3)
+
+        # ## Board cutout
+        self.generate_cutout_button = FCButton(_('Cutout Tool'))
+        self.generate_cutout_button.setIcon(QtGui.QIcon(self.app.resource_location + '/cut32_bis.png'))
+        self.generate_cutout_button.setToolTip(
+            _("Generate the geometry for\n"
+              "the board cutout.")
+        )
+        # self.generate_cutout_button.setStyleSheet("""
+        #                 QPushButton
+        #                 {
+        #                     font-weight: bold;
+        #                 }
+        #                 """)
+        grid0.addWidget(self.generate_cutout_button, 20, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 22, 0, 1, 3)
+
+        # UTILITIES BUTTON
+        self.util_button = FCButton('%s' % _("UTILTIES"), checkable=True)
+        self.util_button.setIcon(QtGui.QIcon(self.app.resource_location + '/settings18.png'))
+        self.util_button.setToolTip(_("Show the Utilties."))
+        self.util_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid0.addWidget(self.util_button, 24, 0, 1, 3)
+
+        # UTILITIES Frame
+        self.util_frame = QtWidgets.QFrame()
+        self.util_frame.setContentsMargins(0, 0, 0, 0)
+        grid0.addWidget(self.util_frame, 25, 0, 1, 3)
+        self.util_box = QtWidgets.QVBoxLayout()
+        self.util_box.setContentsMargins(0, 0, 0, 0)
+        self.util_frame.setLayout(self.util_box)
+        self.util_frame.hide()
+
+        util_grid = QtWidgets.QGridLayout()
+        util_grid.setColumnStretch(0, 0)
+        util_grid.setColumnStretch(1, 1)
+        self.util_box.addLayout(util_grid)
+
+        # ## Non-copper regions
+        self.noncopper_label = FCLabel("<b>%s</b>" % _("Non-copper regions"))
+        self.noncopper_label.setToolTip(
+            _("Create polygons covering the\n"
+              "areas without copper on the PCB.\n"
+              "Equivalent to the inverse of this\n"
+              "object. Can be used to remove all\n"
+              "copper from a specified region.")
+        )
+
+        util_grid.addWidget(self.noncopper_label, 0, 0, 1, 3)
+
+        # Margin
+        bmlabel = FCLabel('%s:' % _('Boundary Margin'))
+        bmlabel.setToolTip(
+            _("Specify the edge of the PCB\n"
+              "by drawing a box around all\n"
+              "objects with this minimum\n"
+              "distance.")
+        )
+
+        self.noncopper_margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.noncopper_margin_entry.set_range(-10000.0000, 10000.0000)
+        self.noncopper_margin_entry.set_precision(self.decimals)
+        self.noncopper_margin_entry.setSingleStep(0.1)
+
+        util_grid.addWidget(bmlabel, 2, 0)
+        util_grid.addWidget(self.noncopper_margin_entry, 2, 1, 1, 2)
+
+        # Rounded corners
+        self.noncopper_rounded_cb = FCCheckBox(label=_("Rounded"))
+        self.noncopper_rounded_cb.setToolTip(
+            _("Resulting geometry will have rounded corners.")
+        )
+
+        self.generate_noncopper_button = FCButton(_('Generate Geometry'))
+        self.generate_noncopper_button.setIcon(QtGui.QIcon(self.app.resource_location + '/geometry32.png'))
+        util_grid.addWidget(self.noncopper_rounded_cb, 4, 0)
+        util_grid.addWidget(self.generate_noncopper_button, 4, 1, 1, 2)
+
+        separator_line1 = QtWidgets.QFrame()
+        separator_line1.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line1.setFrameShadow(QtWidgets.QFrame.Sunken)
+        util_grid.addWidget(separator_line1, 6, 0, 1, 3)
+
+        # ## Bounding box
+        self.boundingbox_label = FCLabel('<b>%s</b>' % _('Bounding Box'))
+        self.boundingbox_label.setToolTip(
+            _("Create a geometry surrounding the Gerber object.\n"
+              "Square shape.")
+        )
+
+        util_grid.addWidget(self.boundingbox_label, 8, 0, 1, 3)
+
+        bbmargin = FCLabel('%s:' % _('Boundary Margin'))
+        bbmargin.setToolTip(
+            _("Distance of the edges of the box\n"
+              "to the nearest polygon.")
+        )
+        self.bbmargin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.bbmargin_entry.set_range(-10000.0000, 10000.0000)
+        self.bbmargin_entry.set_precision(self.decimals)
+        self.bbmargin_entry.setSingleStep(0.1)
+
+        util_grid.addWidget(bbmargin, 10, 0)
+        util_grid.addWidget(self.bbmargin_entry, 10, 1, 1, 2)
+
+        self.bbrounded_cb = FCCheckBox(label=_("Rounded"))
+        self.bbrounded_cb.setToolTip(
+            _("If the bounding box is \n"
+              "to have rounded corners\n"
+              "their radius is equal to\n"
+              "the margin.")
+        )
+
+        self.generate_bb_button = FCButton(_('Generate Geometry'))
+        self.generate_bb_button.setIcon(QtGui.QIcon(self.app.resource_location + '/geometry32.png'))
+        self.generate_bb_button.setToolTip(
+            _("Generate the Geometry object.")
+        )
+        util_grid.addWidget(self.bbrounded_cb, 12, 0)
+        util_grid.addWidget(self.generate_bb_button, 12, 1, 1, 2)
+
+
+class ExcellonObjectUI(ObjectUI):
+    """
+    User interface for Excellon objects.
+    """
+
+    def __init__(self, app, parent=None):
+
+        self.decimals = app.decimals
+        self.app = app
+
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if theme_settings.contains("theme"):
+            theme = theme_settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            self.resource_loc = 'assets/resources'
+        else:
+            self.resource_loc = 'assets/resources'
+
+        ObjectUI.__init__(self, title=_('Excellon Object'),
+                          icon_file=self.resource_loc + '/drill32.png',
+                          parent=parent,
+                          app=self.app)
+
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.custom_box.addLayout(grid0)
+
+        # Plot options
+        self.plot_options_label = FCLabel("<b>%s:</b>" % _("Plot Options"))
+
+        # Solid CB
+        self.solid_cb = FCCheckBox(label=_('Solid'))
+        self.solid_cb.setToolTip(
+            _("Solid circles.")
+        )
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label=_('Multi-Color'))
+        self.multicolored_cb.setToolTip(
+            _("Draw polygons in different colors.")
+        )
+
+        grid0.addWidget(self.plot_options_label, 0, 0)
+        grid0.addWidget(self.solid_cb, 0, 1)
+        grid0.addWidget(self.multicolored_cb, 0, 2)
+
+        # ## Object name
+        self.name_hlay = QtWidgets.QHBoxLayout()
+
+        name_label = FCLabel("<b>%s:</b>" % _("Name"))
+        self.name_entry = FCEntry()
+        self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
+        self.name_hlay.addWidget(name_label)
+        self.name_hlay.addWidget(self.name_entry)
+
+        grid0.addLayout(self.name_hlay, 2, 0, 1, 3)
+
+        # Editor
+        self.editor_button = FCButton(_('Excellon Editor'))
+        self.editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/edit_file32.png'))
+
+        self.editor_button.setToolTip(
+            _("Start the Object Editor")
+        )
+        self.editor_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid0.addWidget(self.editor_button, 4, 0, 1, 3)
+
+        # PROPERTIES CB
+        self.properties_button = FCButton('%s' % _("PROPERTIES"), checkable=True)
+        self.properties_button.setIcon(QtGui.QIcon(self.app.resource_location + '/properties32.png'))
+        self.properties_button.setToolTip(_("Show the Properties."))
+        self.properties_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid0.addWidget(self.properties_button, 6, 0, 1, 3)
+
+        # PROPERTIES Frame
+        self.properties_frame = QtWidgets.QFrame()
+        self.properties_frame.setContentsMargins(0, 0, 0, 0)
+        grid0.addWidget(self.properties_frame, 7, 0, 1, 3)
+        self.properties_box = QtWidgets.QVBoxLayout()
+        self.properties_box.setContentsMargins(0, 0, 0, 0)
+        self.properties_frame.setLayout(self.properties_box)
+        self.properties_frame.hide()
+
+        self.treeWidget = FCTree(columns=2)
+
+        self.properties_box.addWidget(self.treeWidget)
+        self.properties_box.setStretch(0, 0)
+
+        # ### Tools Drills ####
+        self.tools_table_label = FCLabel('<b>%s</b>' % _('Tools Table'))
+        self.tools_table_label.setToolTip(
+            _("Tools in this Excellon object\n"
+              "when are used for drilling.")
+        )
+
+        # Table Visibility CB
+        self.table_visibility_cb = FCCheckBox()
+        self.table_visibility_cb.setToolTip(
+            _("Toggle the display of the Tools Table.")
+        )
+
+        # Plot CB
+        hlay_plot = QtWidgets.QHBoxLayout()
+        self.plot_cb = FCCheckBox(_('Plot'))
+        self.plot_cb.setToolTip(
+            _("Plot (show) this object.")
+        )
+        self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
+        hlay_plot.addStretch()
+        hlay_plot.addWidget(self.plot_cb)
+
+        grid0.addWidget(self.tools_table_label, 8, 0)
+        grid0.addWidget(self.table_visibility_cb, 8, 1)
+        grid0.addLayout(hlay_plot, 8, 2)
+
+        # #############################################################################################################
+        # #############################################################################################################
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Drills widgets
+        # this way I can hide/show the frame
+        # #############################################################################################################
+        # #############################################################################################################
+
+        self.drills_frame = QtWidgets.QFrame()
+        self.drills_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.drills_frame)
+        self.tools_box = QtWidgets.QVBoxLayout()
+        self.tools_box.setContentsMargins(0, 0, 0, 0)
+        self.drills_frame.setLayout(self.tools_box)
+
+        self.tools_table = FCTable()
+        self.tools_table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+        self.tools_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        self.tools_box.addWidget(self.tools_table)
+
+        self.tools_table.setColumnCount(6)
+        self.tools_table.setHorizontalHeaderLabels(['#', _('Diameter'), _('Drills'), _('Slots'),
+                                                    "C", 'P'])
+        self.tools_table.setSortingEnabled(False)
+
+        self.tools_table.horizontalHeaderItem(0).setToolTip(
+            _("This is the Tool Number.\n"
+              "When ToolChange is checked, on toolchange event this value\n"
+              "will be showed as a T1, T2 ... Tn in the Machine Code.\n\n"
+              "Here the tools are selected for G-code generation."))
+        self.tools_table.horizontalHeaderItem(1).setToolTip(
+            _("Tool Diameter. Its value\n"
+              "is the cut width into the material."))
+        self.tools_table.horizontalHeaderItem(2).setToolTip(
+            _("The number of Drill holes. Holes that are drilled with\n"
+              "a drill bit."))
+        self.tools_table.horizontalHeaderItem(3).setToolTip(
+            _("The number of Slot holes. Holes that are created by\n"
+              "milling them with an endmill bit."))
+        self.tools_table.horizontalHeaderItem(4).setToolTip(
+            _("Show the color of the drill holes when using multi-color."))
+        self.tools_table.horizontalHeaderItem(5).setToolTip(
+            _("Toggle display of the drills for the current tool.\n"
+              "This does not select the tools for G-code generation."))
+
+        # this column is not used; reserved for future usage
+        # self.tools_table.setColumnHidden(4, True)
+
+        # Excellon Tools autoload from DB
+
+        # Auto Load Tools from DB
+        self.autoload_db_cb = FCCheckBox('%s' % _("Auto load from DB"))
+        self.autoload_db_cb.setToolTip(
+            _("Automatic replacement of the tools from related application tools\n"
+              "with tools from DB that have a close diameter value.")
+        )
+        self.tools_box.addWidget(self.autoload_db_cb)
+
+        # #################################################################
+        # ########## TOOLS GRID ###########################################
+        # #################################################################
+
+        grid2 = QtWidgets.QGridLayout()
+        self.tools_box.addLayout(grid2)
+        grid2.setColumnStretch(0, 0)
+        grid2.setColumnStretch(1, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line, 0, 0, 1, 2)
+
+        self.tool_lbl = FCLabel('<b>%s</b>' % _("TOOLS"))
+        grid2.addWidget(self.tool_lbl, 2, 0, 1, 2)
+
+        # Drilling Tool - will create GCode for drill holes
+        self.drill_button = FCButton(_('Drilling Tool'))
+        self.drill_button.setIcon(QtGui.QIcon(self.app.resource_location + '/drilling_tool32.png'))
+        self.drill_button.setToolTip(
+            _("Generate GCode from the drill holes in an Excellon object.")
+        )
+        self.drill_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid2.addWidget(self.drill_button, 4, 0, 1, 2)
+
+        # Milling Tool - will create GCode for slot holes
+        self.milling_button = FCButton(_('Milling Tool'))
+        self.milling_button.setIcon(QtGui.QIcon(self.app.resource_location + '/milling_tool32.png'))
+        self.milling_button.setToolTip(
+            _("Generate a Geometry for milling drills or slots in an Excellon object.")
+        )
+        # self.milling_button.setStyleSheet("""
+        #                 QPushButton
+        #                 {
+        #                     font-weight: bold;
+        #                 }
+        #                 """)
+        grid2.addWidget(self.milling_button, 6, 0, 1, 2)
+        # TODO until the Milling Tool is finished this stays disabled
+        self.milling_button.setDisabled(True)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line, 8, 0, 1, 2)
+
+        # UTILITIES BUTTON
+        self.util_button = FCButton('%s' % _("UTILTIES"), checkable=True)
+        self.util_button.setIcon(QtGui.QIcon(self.app.resource_location + '/settings18.png'))
+        self.util_button.setToolTip(_("Show the Utilties."))
+        self.util_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid2.addWidget(self.util_button, 10, 0, 1, 2)
+
+        # UTILITIES Frame
+        self.util_frame = QtWidgets.QFrame()
+        self.util_frame.setContentsMargins(0, 0, 0, 0)
+        grid2.addWidget(self.util_frame, 12, 0, 1, 2)
+        self.util_box = QtWidgets.QVBoxLayout()
+        self.util_box.setContentsMargins(0, 0, 0, 0)
+        self.util_frame.setLayout(self.util_box)
+        self.util_frame.hide()
+
+        util_grid = QtWidgets.QGridLayout()
+        util_grid.setColumnStretch(0, 0)
+        util_grid.setColumnStretch(1, 1)
+        self.util_box.addLayout(util_grid)
+
+        # ### Milling Holes Drills ####
+        self.mill_hole_label = FCLabel('<b>%s</b>' % _('Milling Geometry'))
+        self.mill_hole_label.setToolTip(
+            _("Create Geometry for milling holes.\n"
+              "Select from the Tools Table above the hole dias to be\n"
+              "milled. Use the # column to make the selection.")
+        )
+        util_grid.addWidget(self.mill_hole_label, 0, 0, 1, 3)
+
+        self.tdlabel = FCLabel('%s:' % _('Milling Diameter'))
+        self.tdlabel.setToolTip(
+            _("Diameter of the cutting tool.")
+        )
+
+        util_grid.addWidget(self.tdlabel, 2, 0, 1, 3)
+
+        self.tooldia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.tooldia_entry.set_precision(self.decimals)
+        self.tooldia_entry.set_range(0.0, 10000.0000)
+        self.tooldia_entry.setSingleStep(0.1)
+
+        self.generate_milling_button = FCButton(_('Mill Drills'))
+        self.generate_milling_button.setToolTip(
+            _("Create the Geometry Object\n"
+              "for milling drills.")
+        )
+        self.generate_milling_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+
+        util_grid.addWidget(self.tooldia_entry, 4, 0, 1, 2)
+        util_grid.addWidget(self.generate_milling_button, 4, 2)
+
+        self.slot_tooldia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.slot_tooldia_entry.set_precision(self.decimals)
+        self.slot_tooldia_entry.set_range(0.0, 10000.0000)
+        self.slot_tooldia_entry.setSingleStep(0.1)
+
+        self.generate_milling_slots_button = FCButton(_('Mill Slots'))
+        self.generate_milling_slots_button.setToolTip(
+            _("Create the Geometry Object\n"
+              "for milling slots.")
+        )
+        self.generate_milling_slots_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+
+        util_grid.addWidget(self.slot_tooldia_entry, 6, 0, 1, 2)
+        util_grid.addWidget(self.generate_milling_slots_button, 6, 2)
+
+    def hide_drills(self, state=True):
+        if state is True:
+            self.drills_frame.hide()
+        else:
+            self.drills_frame.show()
+
+
+class GeometryObjectUI(ObjectUI):
+    """
+    User interface for Geometry objects.
+    """
+
+    def __init__(self, app, parent=None):
+
+        self.decimals = app.decimals
+        self.app = app
+
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if theme_settings.contains("theme"):
+            theme = theme_settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            self.resource_loc = 'assets/resources'
+        else:
+            self.resource_loc = 'assets/resources'
+
+        super(GeometryObjectUI, self).__init__(
+            title=_('Geometry Object'),
+            icon_file=self.resource_loc + '/geometry32.png', parent=parent,  app=self.app
+        )
+
+        # Plot options
+        grid_header = QtWidgets.QGridLayout()
+        grid_header.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.custom_box.addLayout(grid_header)
+        grid_header.setColumnStretch(0, 0)
+        grid_header.setColumnStretch(1, 1)
+
+        self.plot_options_label = FCLabel("<b>%s:</b>" % _("Plot Options"))
+        self.plot_options_label.setMinimumWidth(90)
+
+        grid_header.addWidget(self.plot_options_label, 0, 0)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label=_('Multi-Color'))
+        self.multicolored_cb.setToolTip(
+            _("Draw polygons in different colors.")
+        )
+        self.multicolored_cb.setMinimumWidth(55)
+        grid_header.addWidget(self.multicolored_cb, 0, 2)
+
+        # ## Object name
+        self.name_hlay = QtWidgets.QHBoxLayout()
+        grid_header.addLayout(self.name_hlay, 2, 0, 1, 3)
+
+        name_label = FCLabel("<b>%s:</b>" % _("Name"))
+        self.name_entry = FCEntry()
+        self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
+        self.name_hlay.addWidget(name_label)
+        self.name_hlay.addWidget(self.name_entry)
+
+        # Editor
+        self.editor_button = FCButton(_('Geometry Editor'))
+        self.editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/edit_file32.png'))
+
+        self.editor_button.setToolTip(
+            _("Start the Object Editor")
+        )
+        self.editor_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid_header.addWidget(self.editor_button, 4, 0, 1, 3)
+
+        # PROPERTIES CB
+        self.properties_button = FCButton('%s' % _("PROPERTIES"), checkable=True)
+        self.properties_button.setIcon(QtGui.QIcon(self.app.resource_location + '/properties32.png'))
+        self.properties_button.setToolTip(_("Show the Properties."))
+        self.properties_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        grid_header.addWidget(self.properties_button, 6, 0, 1, 3)
+
+        # PROPERTIES Frame
+        self.properties_frame = QtWidgets.QFrame()
+        self.properties_frame.setContentsMargins(0, 0, 0, 0)
+        grid_header.addWidget(self.properties_frame, 7, 0, 1, 3)
+        self.properties_box = QtWidgets.QVBoxLayout()
+        self.properties_box.setContentsMargins(0, 0, 0, 0)
+        self.properties_frame.setLayout(self.properties_box)
+        self.properties_frame.hide()
+
+        self.treeWidget = FCTree(columns=2)
+
+        self.properties_box.addWidget(self.treeWidget)
+        self.properties_box.setStretch(0, 0)
+
+        # add a frame and inside add a vertical box layout. Inside this vbox layout I add all the Tools widgets
+        # this way I can hide/show the frame
+        self.geo_tools_frame = QtWidgets.QFrame()
+        self.geo_tools_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.geo_tools_frame)
+
+        self.geo_tools_box = QtWidgets.QVBoxLayout()
+        self.geo_tools_box.setContentsMargins(0, 0, 0, 0)
+        self.geo_tools_frame.setLayout(self.geo_tools_box)
+
+        # ************************************************************************
+        # ************** TABLE BOX FRAME *****************************************
+        # ************************************************************************
+        self.geo_table_frame = QtWidgets.QFrame()
+        self.geo_table_frame.setContentsMargins(0, 0, 0, 0)
+        self.geo_tools_box.addWidget(self.geo_table_frame)
+        self.geo_table_box = QtWidgets.QVBoxLayout()
+        self.geo_table_box.setContentsMargins(0, 0, 0, 0)
+        self.geo_table_frame.setLayout(self.geo_table_box)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.geo_table_box.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        # ### Tools ####
+        self.tools_table_label = FCLabel('<b>%s:</b>' % _('Tools Table'))
+        self.tools_table_label.setToolTip(
+            _("Tools in this Geometry object used for cutting.\n"
+              "The 'Offset' entry will set an offset for the cut.\n"
+              "'Offset' can be inside, outside, on path (none) and custom.\n"
+              "'Type' entry is only informative and it allow to know the \n"
+              "intent of using the current tool. \n"
+              "It can be Rough(ing), Finish(ing) or Iso(lation).\n"
+              "The 'Tool type'(TT) can be circular with 1 to 4 teeths(C1..C4),\n"
+              "ball(B), or V-Shaped(V). \n"
+              "When V-shaped is selected the 'Type' entry is automatically \n"
+              "set to Isolation, the CutZ parameter in the UI form is\n"
+              "grayed out and Cut Z is automatically calculated from the newly \n"
+              "showed UI form entries named V-Tip Dia and V-Tip Angle.")
+        )
+        grid0.addWidget(self.tools_table_label, 0, 0)
+
+        # Plot CB
+        # self.plot_cb = QtWidgets.QCheckBox('Plot')
+        self.plot_cb = FCCheckBox(_('Plot Object'))
+        self.plot_cb.setToolTip(
+            _("Plot (show) this object.")
+        )
+        self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
+        grid0.addWidget(self.plot_cb, 0, 1)
+
+        self.geo_tools_table = FCTable(drag_drop=True)
+        grid0.addWidget(self.geo_tools_table, 1, 0, 1, 2)
+        self.geo_tools_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+
+        self.geo_tools_table.setColumnCount(7)
+        self.geo_tools_table.setColumnWidth(0, 20)
+        self.geo_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Offset'), _('Type'), _('TT'), '', 'P'])
+        self.geo_tools_table.setColumnHidden(5, True)
+        # stylesheet = "::section{Background-color:rgb(239,239,245)}"
+        # self.geo_tools_table.horizontalHeader().setStyleSheet(stylesheet)
+
+        self.geo_tools_table.horizontalHeaderItem(0).setToolTip(
+            _(
+                "This is the Tool Number.\n"
+                "When ToolChange is checked, on toolchange event this value\n"
+                "will be showed as a T1, T2 ... Tn")
+            )
+        self.geo_tools_table.horizontalHeaderItem(1).setToolTip(
+            _("Tool Diameter. Its value\n"
+              "is the cut width into the material."))
+        self.geo_tools_table.horizontalHeaderItem(2).setToolTip(
+            _(
+                "The value for the Offset can be:\n"
+                "- Path -> There is no offset, the tool cut will be done through the geometry line.\n"
+                "- In(side) -> The tool cut will follow the geometry inside. It will create a 'pocket'.\n"
+                "- Out(side) -> The tool cut will follow the geometry line on the outside."
+            ))
+        self.geo_tools_table.horizontalHeaderItem(3).setToolTip(
+            _(
+                "The (Operation) Type has only informative value. Usually the UI form values \n"
+                "are choose based on the operation type and this will serve as a reminder.\n"
+                "Can be 'Roughing', 'Finishing' or 'Isolation'.\n"
+                "For Roughing we may choose a lower Feedrate and multiDepth cut.\n"
+                "For Finishing we may choose a higher Feedrate, without multiDepth.\n"
+                "For Isolation we need a lower Feedrate as it use a milling bit with a fine tip."
+            ))
+        self.geo_tools_table.horizontalHeaderItem(4).setToolTip(
+            _(
+                "The Tool Type (TT) can be:\n"
+                "- Circular with 1 ... 4 teeth -> it is informative only. Being circular the cut width in material\n"
+                "is exactly the tool diameter.\n"
+                "- Ball -> informative only and make reference to the Ball type endmill.\n"
+                "- V-Shape -> it will disable Z-Cut parameter in the UI form and enable two additional UI form\n"
+                "fields: V-Tip Dia and V-Tip Angle. Adjusting those two values will adjust the Z-Cut parameter such\n"
+                "as the cut width into material will be equal with the value in the Tool "
+                "Diameter column of this table.\n"
+                "Choosing the V-Shape Tool Type automatically will select the Operation Type as Isolation."
+            ))
+        self.geo_tools_table.horizontalHeaderItem(6).setToolTip(
+            _(
+                "Plot column. It is visible only for MultiGeo geometries, meaning geometries that holds the geometry\n"
+                "data into the tools. For those geometries, deleting the tool will delete the geometry data also,\n"
+                "so be WARNED. From the checkboxes on each row it can be enabled/disabled the plot on canvas\n"
+                "for the corresponding tool."
+            ))
+
+        # Tool Offset
+        grid1 = QtWidgets.QGridLayout()
+        self.geo_table_box.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+
+        self.tool_offset_lbl = FCLabel('%s:' % _('Tool Offset'))
+        self.tool_offset_lbl.setToolTip(
+            _(
+                "The value to offset the cut when \n"
+                "the Offset type selected is 'Offset'.\n"
+                "The value can be positive for 'outside'\n"
+                "cut and negative for 'inside' cut."
+            )
+        )
+        self.tool_offset_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.tool_offset_entry.set_precision(self.decimals)
+        self.tool_offset_entry.set_range(-10000.0000, 10000.0000)
+        self.tool_offset_entry.setSingleStep(0.1)
+
+        grid1.addWidget(self.tool_offset_lbl, 0, 0)
+        grid1.addWidget(self.tool_offset_entry, 0, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 1, 0, 1, 2)
+
+        self.tool_sel_label = FCLabel('<b>%s</b>' % _("Add from DB"))
+        grid1.addWidget(self.tool_sel_label, 2, 0, 1, 2)
+
+        self.addtool_entry_lbl = FCLabel('%s:' % _('Tool Dia'))
+        self.addtool_entry_lbl.setToolTip(
+            _("Diameter for the new tool")
+        )
+        self.addtool_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.addtool_entry.set_precision(self.decimals)
+        self.addtool_entry.set_range(0.00001, 10000.0000)
+        self.addtool_entry.setSingleStep(0.1)
+
+        grid1.addWidget(self.addtool_entry_lbl, 3, 0)
+        grid1.addWidget(self.addtool_entry, 3, 1)
+
+        bhlay = QtWidgets.QHBoxLayout()
+
+        self.search_and_add_btn = FCButton(_('Search and Add'))
+        self.search_and_add_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/plus16.png'))
+        self.search_and_add_btn.setToolTip(
+            _("Add a new tool to the Tool Table\n"
+              "with the diameter specified above.")
+        )
+
+        self.addtool_from_db_btn = FCButton(_('Pick from DB'))
+        self.addtool_from_db_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/search_db32.png'))
+        self.addtool_from_db_btn.setToolTip(
+            _("Add a new tool to the Tool Table\n"
+              "from the Tools Database.\n"
+              "Tools database administration in in:\n"
+              "Menu: Options -> Tools Database")
+        )
+
+        bhlay.addWidget(self.search_and_add_btn)
+        bhlay.addWidget(self.addtool_from_db_btn)
+
+        grid1.addLayout(bhlay, 5, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 9, 0, 1, 2)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.geo_table_box.addLayout(grid2)
+
+        self.deltool_btn = FCButton(_('Delete'))
+        self.deltool_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/trash16.png'))
+        self.deltool_btn.setToolTip(
+            _("Delete a selection of tools in the Tool Table\n"
+              "by first selecting a row in the Tool Table.")
+        )
+
+        grid2.addWidget(self.deltool_btn, 0, 0, 1, 2)
+
+        # ###########################################################
+        # ############# Create CNC Job ##############################
+        # ###########################################################
+        self.geo_param_frame = QtWidgets.QFrame()
+        self.geo_param_frame.setContentsMargins(0, 0, 0, 0)
+        self.geo_tools_box.addWidget(self.geo_param_frame)
+
+        self.geo_param_box = QtWidgets.QVBoxLayout()
+        self.geo_param_box.setContentsMargins(0, 0, 0, 0)
+        self.geo_param_frame.setLayout(self.geo_param_box)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.geo_param_box.addWidget(separator_line)
+
+        # #################################################################
+        # ################# GRID LAYOUT 3   ###############################
+        # #################################################################
+
+        self.grid3 = QtWidgets.QGridLayout()
+        self.grid3.setColumnStretch(0, 0)
+        self.grid3.setColumnStretch(1, 1)
+        self.geo_param_box.addLayout(self.grid3)
+
+        # ### Tools Data ## ##
+        self.tool_data_label = FCLabel(
+            "<b>%s: <font color='#0000FF'>%s %d</font></b>" % (_('Parameters for'), _("Tool"), int(1)))
+        self.tool_data_label.setToolTip(
+            _(
+                "The data used for creating GCode.\n"
+                "Each tool store it's own set of such data."
+            )
+        )
+        self.grid3.addWidget(self.tool_data_label, 0, 0, 1, 2)
+
+        # Tip Dia
+        self.tipdialabel = FCLabel('%s:' % _('V-Tip Dia'))
+        self.tipdialabel.setToolTip(
+            _(
+                "The tip diameter for V-Shape Tool"
+            )
+        )
+        self.tipdia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.tipdia_entry.set_precision(self.decimals)
+        self.tipdia_entry.set_range(0.00001, 10000.0000)
+        self.tipdia_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.tipdialabel, 1, 0)
+        self.grid3.addWidget(self.tipdia_entry, 1, 1)
+
+        # Tip Angle
+        self.tipanglelabel = FCLabel('%s:' % _('V-Tip Angle'))
+        self.tipanglelabel.setToolTip(
+            _(
+                "The tip angle for V-Shape Tool.\n"
+                "In degree."
+            )
+        )
+        self.tipangle_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.tipangle_entry.set_precision(self.decimals)
+        self.tipangle_entry.set_range(1.0, 180.0)
+        self.tipangle_entry.setSingleStep(1)
+
+        self.grid3.addWidget(self.tipanglelabel, 2, 0)
+        self.grid3.addWidget(self.tipangle_entry, 2, 1)
+
+        # Cut Z
+        self.cutzlabel = FCLabel('%s:' % _('Cut Z'))
+        self.cutzlabel.setToolTip(
+            _(
+                "Cutting depth (negative)\n"
+                "below the copper surface."
+            )
+        )
+        self.cutz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.cutz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.cutz_entry.set_range(-10000.0000, 0.0000)
+        else:
+            self.cutz_entry.set_range(-10000.0000, 10000.0000)
+
+        self.cutz_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.cutzlabel, 3, 0)
+        self.grid3.addWidget(self.cutz_entry, 3, 1)
+
+        # Multi-pass
+        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
+        self.mpass_cb.setToolTip(
+            _(
+                "Use multiple passes to limit\n"
+                "the cut depth in each pass. Will\n"
+                "cut multiple times until Cut Z is\n"
+                "reached."
+            )
+        )
+
+        self.maxdepth_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.maxdepth_entry.set_precision(self.decimals)
+        self.maxdepth_entry.set_range(0, 10000.0000)
+        self.maxdepth_entry.setSingleStep(0.1)
+
+        self.maxdepth_entry.setToolTip(
+            _(
+                "Depth of each pass (positive)."
+            )
+        )
+        self.ois_mpass_geo = OptionalInputSection(self.mpass_cb, [self.maxdepth_entry])
+
+        self.grid3.addWidget(self.mpass_cb, 4, 0)
+        self.grid3.addWidget(self.maxdepth_entry, 4, 1)
+
+        # Travel Z
+        self.travelzlabel = FCLabel('%s:' % _('Travel Z'))
+        self.travelzlabel.setToolTip(
+            _("Height of the tool when\n"
+              "moving without cutting.")
+        )
+        self.travelz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.travelz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.travelz_entry.set_range(0.00001, 10000.0000)
+        else:
+            self.travelz_entry.set_range(-10000.0000, 10000.0000)
+
+        self.travelz_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.travelzlabel, 5, 0)
+        self.grid3.addWidget(self.travelz_entry, 5, 1)
+
+        # Feedrate X-Y
+        self.frlabel = FCLabel('%s:' % _('Feedrate X-Y'))
+        self.frlabel.setToolTip(
+            _("Cutting speed in the XY\n"
+              "plane in units per minute")
+        )
+        self.cncfeedrate_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.cncfeedrate_entry.set_precision(self.decimals)
+        self.cncfeedrate_entry.set_range(0, 910000.0000)
+        self.cncfeedrate_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.frlabel, 10, 0)
+        self.grid3.addWidget(self.cncfeedrate_entry, 10, 1)
+
+        # Feedrate Z (Plunge)
+        self.frzlabel = FCLabel('%s:' % _('Feedrate Z'))
+        self.frzlabel.setToolTip(
+            _("Cutting speed in the XY\n"
+              "plane in units per minute.\n"
+              "It is called also Plunge.")
+        )
+        self.feedrate_z_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.feedrate_z_entry.set_precision(self.decimals)
+        self.feedrate_z_entry.set_range(0, 910000.0000)
+        self.feedrate_z_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.frzlabel, 11, 0)
+        self.grid3.addWidget(self.feedrate_z_entry, 11, 1)
+
+        # Feedrate rapids
+        self.fr_rapidlabel = FCLabel('%s:' % _('Feedrate Rapids'))
+        self.fr_rapidlabel.setToolTip(
+            _("Cutting speed in the XY plane\n"
+              "(in units per minute).\n"
+              "This is for the rapid move G00.\n"
+              "It is useful only for Marlin,\n"
+              "ignore for any other cases.")
+        )
+        self.feedrate_rapid_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.feedrate_rapid_entry.set_precision(self.decimals)
+        self.feedrate_rapid_entry.set_range(0, 910000.0000)
+        self.feedrate_rapid_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.fr_rapidlabel, 12, 0)
+        self.grid3.addWidget(self.feedrate_rapid_entry, 12, 1)
+        # default values is to hide
+        self.fr_rapidlabel.hide()
+        self.feedrate_rapid_entry.hide()
+
+        # Cut over 1st point in path
+        self.extracut_cb = FCCheckBox('%s:' % _('Re-cut'))
+        self.extracut_cb.setToolTip(
+            _("In order to remove possible\n"
+              "copper leftovers where first cut\n"
+              "meet with last cut, we generate an\n"
+              "extended cut over the first cut section.")
+        )
+
+        self.e_cut_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.e_cut_entry.set_range(0, 99999)
+        self.e_cut_entry.set_precision(self.decimals)
+        self.e_cut_entry.setSingleStep(0.1)
+        self.e_cut_entry.setWrapping(True)
+        self.e_cut_entry.setToolTip(
+            _("In order to remove possible\n"
+              "copper leftovers where first cut\n"
+              "meet with last cut, we generate an\n"
+              "extended cut over the first cut section.")
+        )
+        self.grid3.addWidget(self.extracut_cb, 13, 0)
+        self.grid3.addWidget(self.e_cut_entry, 13, 1)
+
+        # Spindlespeed
+        self.spindle_label = FCLabel('%s:' % _('Spindle speed'))
+        self.spindle_label.setToolTip(
+            _(
+                "Speed of the spindle in RPM (optional).\n"
+                "If LASER preprocessor is used,\n"
+                "this value is the power of laser."
+            )
+        )
+        self.cncspindlespeed_entry = FCSpinner(callback=self.confirmation_message_int)
+        self.cncspindlespeed_entry.set_range(0, 1000000)
+        self.cncspindlespeed_entry.set_step(100)
+
+        self.grid3.addWidget(self.spindle_label, 14, 0)
+        self.grid3.addWidget(self.cncspindlespeed_entry, 14, 1)
+
+        # Dwell
+        self.dwell_cb = FCCheckBox('%s:' % _('Dwell'))
+        self.dwell_cb.setToolTip(
+            _(
+                "Pause to allow the spindle to reach its\n"
+                "speed before cutting."
+            )
+        )
+        self.dwelltime_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.dwelltime_entry.set_precision(self.decimals)
+        self.dwelltime_entry.set_range(0, 10000.0000)
+        self.dwelltime_entry.setSingleStep(0.1)
+
+        self.dwelltime_entry.setToolTip(
+            _("Number of time units for spindle to dwell.")
+        )
+        self.ois_dwell_geo = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        self.grid3.addWidget(self.dwell_cb, 15, 0)
+        self.grid3.addWidget(self.dwelltime_entry, 15, 1)
+
+        # Probe depth
+        self.pdepth_label = FCLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label.setToolTip(
+            _("The maximum depth that the probe is allowed\n"
+              "to probe. Negative value, in current units.")
+        )
+        self.pdepth_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.pdepth_entry.set_precision(self.decimals)
+        self.pdepth_entry.set_range(-10000.0000, 10000.0000)
+        self.pdepth_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.pdepth_label, 17, 0)
+        self.grid3.addWidget(self.pdepth_entry, 17, 1)
+
+        self.pdepth_label.hide()
+        self.pdepth_entry.setVisible(False)
+
+        # Probe feedrate
+        self.feedrate_probe_label = FCLabel('%s:' % _("Feedrate Probe"))
+        self.feedrate_probe_label.setToolTip(
+            _("The feedrate used while the probe is probing.")
+        )
+        self.feedrate_probe_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.feedrate_probe_entry.set_precision(self.decimals)
+        self.feedrate_probe_entry.set_range(0.0, 10000.0000)
+        self.feedrate_probe_entry.setSingleStep(0.1)
+
+        self.grid3.addWidget(self.feedrate_probe_label, 18, 0)
+        self.grid3.addWidget(self.feedrate_probe_entry, 18, 1)
+
+        self.feedrate_probe_label.hide()
+        self.feedrate_probe_entry.setVisible(False)
+
+        # #################################################################
+        # ################# GRID LAYOUT 4   ###############################
+        # #################################################################
+
+        self.grid4 = QtWidgets.QGridLayout()
+        self.grid4.setColumnStretch(0, 0)
+        self.grid4.setColumnStretch(1, 1)
+        self.geo_param_box.addLayout(self.grid4)
+
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid4.addWidget(separator_line2, 0, 0, 1, 2)
+
+        self.apply_param_to_all = FCButton(_("Apply parameters to all tools"))
+        self.apply_param_to_all.setIcon(QtGui.QIcon(self.app.resource_location + '/param_all32.png'))
+        self.apply_param_to_all.setToolTip(
+            _("The parameters in the current form will be applied\n"
+              "on all the tools from the Tool Table.")
+        )
+        self.grid4.addWidget(self.apply_param_to_all, 1, 0, 1, 2)
+
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid4.addWidget(separator_line2, 2, 0, 1, 2)
+
+        # General Parameters
+        self.gen_param_label = FCLabel('<b>%s</b>' % _("Common Parameters"))
+        self.gen_param_label.setToolTip(
+            _("Parameters that are common for all tools.")
+        )
+        self.grid4.addWidget(self.gen_param_label, 3, 0, 1, 2)
+
+        # Tool change Z
+        self.toolchangeg_cb = FCCheckBox('%s:' % _("Tool change Z"))
+        self.toolchangeg_cb.setToolTip(
+            _(
+                "Include tool-change sequence\n"
+                "in the Machine Code (Pause for tool change)."
+            )
+        )
+        self.toolchangez_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.toolchangez_entry.set_precision(self.decimals)
+        self.toolchangez_entry.setToolTip(
+            _(
+                "Z-axis position (height) for\n"
+                "tool change."
+            )
+        )
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.set_range(0, 10000.0000)
+        else:
+            self.toolchangez_entry.set_range(-10000.0000, 10000.0000)
+
+        self.toolchangez_entry.setSingleStep(0.1)
+        self.ois_tcz_geo = OptionalInputSection(self.toolchangeg_cb, [self.toolchangez_entry])
+
+        self.grid4.addWidget(self.toolchangeg_cb, 6, 0)
+        self.grid4.addWidget(self.toolchangez_entry, 6, 1)
+
+        # The Z value for the start move
+        # startzlabel = FCLabel('Start move Z:')
+        # startzlabel.setToolTip(
+        #     "Tool height just before starting the work.\n"
+        #     "Delete the value if you don't need this feature."
+        #
+        # )
+        # grid3.addWidget(startzlabel, 8, 0)
+        # self.gstartz_entry = FloatEntry()
+        # grid3.addWidget(self.gstartz_entry, 8, 1)
+
+        # The Z value for the end move
+        self.endz_label = FCLabel('%s:' % _('End move Z'))
+        self.endz_label.setToolTip(
+            _("Height of the tool after\n"
+              "the last move at the end of the job.")
+        )
+        self.endz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.endz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.endz_entry.set_range(0, 10000.0000)
+        else:
+            self.endz_entry.set_range(-10000.0000, 10000.0000)
+
+        self.endz_entry.setSingleStep(0.1)
+
+        self.grid4.addWidget(self.endz_label, 9, 0)
+        self.grid4.addWidget(self.endz_entry, 9, 1)
+
+        # End Move X,Y
+        endmove_xy_label = FCLabel('%s:' % _('End move X,Y'))
+        endmove_xy_label.setToolTip(
+            _("End move X,Y position. In format (x,y).\n"
+              "If no value is entered then there is no move\n"
+              "on X,Y plane at the end of the job.")
+        )
+        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+        self.endxy_entry.setPlaceholderText(_("X,Y coordinates"))
+
+        self.grid4.addWidget(endmove_xy_label, 10, 0)
+        self.grid4.addWidget(self.endxy_entry, 10, 1)
+
+        # preprocessor selection
+        pp_label = FCLabel('%s:' % _("Preprocessor"))
+        pp_label.setToolTip(
+            _("The Preprocessor file that dictates\n"
+              "the Machine Code (like GCode, RML, HPGL) output.")
+        )
+        self.pp_geometry_name_cb = FCComboBox()
+        self.pp_geometry_name_cb.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+        self.grid4.addWidget(pp_label, 11, 0)
+        self.grid4.addWidget(self.pp_geometry_name_cb, 11, 1)
+
+        # self.grid4.addWidget(FCLabel(''), 12, 0, 1, 2)
+
+        # ------------------------------------------------------------------------------------------------------------
+        # ------------------------- EXCLUSION AREAS ------------------------------------------------------------------
+        # ------------------------------------------------------------------------------------------------------------
+
+        # Exclusion Areas
+        self.exclusion_cb = FCCheckBox('%s' % _("Add exclusion areas"))
+        self.exclusion_cb.setToolTip(
+            _(
+                "Include exclusion areas.\n"
+                "In those areas the travel of the tools\n"
+                "is forbidden."
+            )
+        )
+        self.grid4.addWidget(self.exclusion_cb, 12, 0, 1, 2)
+
+        self.exclusion_frame = QtWidgets.QFrame()
+        self.exclusion_frame.setContentsMargins(0, 0, 0, 0)
+        self.grid4.addWidget(self.exclusion_frame, 14, 0, 1, 2)
+
+        self.exclusion_box = QtWidgets.QVBoxLayout()
+        self.exclusion_box.setContentsMargins(0, 0, 0, 0)
+        self.exclusion_frame.setLayout(self.exclusion_box)
+
+        self.exclusion_table = FCTable()
+        self.exclusion_box.addWidget(self.exclusion_table)
+        self.exclusion_table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
+
+        self.exclusion_table.setColumnCount(4)
+        self.exclusion_table.setColumnWidth(0, 20)
+        self.exclusion_table.setHorizontalHeaderLabels(['#', _('Object'), _('Strategy'), _('Over Z')])
+
+        self.exclusion_table.horizontalHeaderItem(0).setToolTip(_("This is the Area ID."))
+        self.exclusion_table.horizontalHeaderItem(1).setToolTip(
+            _("Type of the object where the exclusion area was added."))
+        self.exclusion_table.horizontalHeaderItem(2).setToolTip(
+            _("The strategy used for exclusion area. Go around the exclusion areas or over it."))
+        self.exclusion_table.horizontalHeaderItem(3).setToolTip(
+            _("If the strategy is to go over the area then this is the height at which the tool will go to avoid the "
+              "exclusion area."))
+
+        self.exclusion_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+        grid_a1 = QtWidgets.QGridLayout()
+        grid_a1.setColumnStretch(0, 0)
+        grid_a1.setColumnStretch(1, 1)
+        self.exclusion_box.addLayout(grid_a1)
+
+        # Chose Strategy
+        self.strategy_label = FCLabel('%s:' % _("Strategy"))
+        self.strategy_label.setToolTip(_("The strategy followed when encountering an exclusion area.\n"
+                                         "Can be:\n"
+                                         "- Over -> when encountering the area, the tool will go to a set height\n"
+                                         "- Around -> will avoid the exclusion area by going around the area"))
+        self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'},
+                                        {'label': _('Around'), 'value': 'around'}])
+
+        grid_a1.addWidget(self.strategy_label, 1, 0)
+        grid_a1.addWidget(self.strategy_radio, 1, 1)
+
+        # Over Z
+        self.over_z_label = FCLabel('%s:' % _("Over Z"))
+        self.over_z_label.setToolTip(_("The height Z to which the tool will rise in order to avoid\n"
+                                       "an interdiction area."))
+        self.over_z_entry = FCDoubleSpinner()
+        self.over_z_entry.set_range(0.000, 10000.0000)
+        self.over_z_entry.set_precision(self.decimals)
+
+        grid_a1.addWidget(self.over_z_label, 2, 0)
+        grid_a1.addWidget(self.over_z_entry, 2, 1)
+
+        # Button Add Area
+        self.add_area_button = FCButton(_('Add Area:'))
+        self.add_area_button.setToolTip(_("Add an Exclusion Area."))
+
+        # Area Selection shape
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+        self.area_shape_radio.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+
+        grid_a1.addWidget(self.add_area_button, 4, 0)
+        grid_a1.addWidget(self.area_shape_radio, 4, 1)
+
+        h_lay_1 = QtWidgets.QHBoxLayout()
+        self.exclusion_box.addLayout(h_lay_1)
+
+        # Button Delete All Areas
+        self.delete_area_button = FCButton(_('Delete All'))
+        self.delete_area_button.setToolTip(_("Delete all exclusion areas."))
+
+        # Button Delete Selected Areas
+        self.delete_sel_area_button = FCButton(_('Delete Selected'))
+        self.delete_sel_area_button.setToolTip(_("Delete all exclusion areas that are selected in the table."))
+
+        h_lay_1.addWidget(self.delete_area_button)
+        h_lay_1.addWidget(self.delete_sel_area_button)
+
+        self.ois_exclusion_geo = OptionalHideInputSection(self.exclusion_cb, [self.exclusion_frame])
+        # -------------------------- EXCLUSION AREAS END -------------------------------------------------------------
+        # ------------------------------------------------------------------------------------------------------------
+
+        # Add Polish
+        self.polish_cb = FCCheckBox(label=_('Add Polish'))
+        self.polish_cb.setToolTip(_(
+            "Will add a Paint section at the end of the GCode.\n"
+            "A metallic brush will clean the material after milling."))
+        self.polish_cb.setObjectName("g_polish")
+        self.grid4.addWidget(self.polish_cb, 15, 0, 1, 2)
+
+        # Polish Tool Diameter
+        self.polish_dia_lbl = FCLabel('%s:' % _('Tool Dia'))
+        self.polish_dia_lbl.setToolTip(
+            _("Diameter for the polishing tool.")
+        )
+        self.polish_dia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.polish_dia_entry.set_precision(self.decimals)
+        self.polish_dia_entry.set_range(0.000, 10000.0000)
+        self.polish_dia_entry.setObjectName("g_polish_dia")
+
+        self.grid4.addWidget(self.polish_dia_lbl, 16, 0)
+        self.grid4.addWidget(self.polish_dia_entry, 16, 1)
+
+        # Polish Travel Z
+        self.polish_travelz_lbl = FCLabel('%s:' % _('Travel Z'))
+        self.polish_travelz_lbl.setToolTip(
+            _("Height of the tool when\n"
+              "moving without cutting.")
+        )
+        self.polish_travelz_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.polish_travelz_entry.set_precision(self.decimals)
+        self.polish_travelz_entry.set_range(0.00000, 10000.00000)
+        self.polish_travelz_entry.setSingleStep(0.1)
+        self.polish_travelz_entry.setObjectName("g_polish_travelz")
+
+        self.grid4.addWidget(self.polish_travelz_lbl, 17, 0)
+        self.grid4.addWidget(self.polish_travelz_entry, 17, 1)
+
+        # Polish Pressure
+        self.polish_pressure_lbl = FCLabel('%s:' % _('Pressure'))
+        self.polish_pressure_lbl.setToolTip(
+            _("Negative value. The higher the absolute value\n"
+              "the stronger the pressure of the brush on the material.")
+        )
+        self.polish_pressure_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.polish_pressure_entry.set_precision(self.decimals)
+        self.polish_pressure_entry.set_range(-10000.0000, 10000.0000)
+        self.polish_pressure_entry.setObjectName("g_polish_pressure")
+
+        self.grid4.addWidget(self.polish_pressure_lbl, 18, 0)
+        self.grid4.addWidget(self.polish_pressure_entry, 18, 1)
+
+        # Polish Margin
+        self.polish_margin_lbl = FCLabel('%s:' % _('Margin'))
+        self.polish_margin_lbl.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.polish_margin_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.polish_margin_entry.set_precision(self.decimals)
+        self.polish_margin_entry.set_range(-10000.0000, 10000.0000)
+        self.polish_margin_entry.setObjectName("g_polish_margin")
+
+        self.grid4.addWidget(self.polish_margin_lbl, 20, 0)
+        self.grid4.addWidget(self.polish_margin_entry, 20, 1)
+
+        # Polish Overlap
+        self.polish_over_lbl = FCLabel('%s:' % _('Overlap'))
+        self.polish_over_lbl.setToolTip(
+            _("How much (percentage) of the tool width to overlap each tool pass.")
+        )
+        self.polish_over_entry = FCDoubleSpinner(suffix='%', callback=self.confirmation_message)
+        self.polish_over_entry.set_precision(self.decimals)
+        self.polish_over_entry.setWrapping(True)
+        self.polish_over_entry.set_range(0.0000, 99.9999)
+        self.polish_over_entry.setSingleStep(0.1)
+        self.polish_over_entry.setObjectName("g_polish_overlap")
+
+        self.grid4.addWidget(self.polish_over_lbl, 22, 0)
+        self.grid4.addWidget(self.polish_over_entry, 22, 1)
+
+        # Polish Method
+        self.polish_method_lbl = FCLabel('%s:' % _('Method'))
+        self.polish_method_lbl.setToolTip(
+            _("Algorithm for polishing:\n"
+              "- Standard: Fixed step inwards.\n"
+              "- Seed-based: Outwards from seed.\n"
+              "- Line-based: Parallel lines.")
+        )
+
+        self.polish_method_combo = FCComboBox2()
+        self.polish_method_combo.addItems(
+            [_("Standard"), _("Seed"), _("Lines")]
+        )
+        self.polish_method_combo.setObjectName('g_polish_method')
+
+        self.grid4.addWidget(self.polish_method_lbl, 24, 0)
+        self.grid4.addWidget(self.polish_method_combo, 24, 1)
+
+        self.polish_dia_lbl.hide()
+        self.polish_dia_entry.hide()
+        self.polish_pressure_lbl.hide()
+        self.polish_pressure_entry.hide()
+        self.polish_travelz_lbl.hide()
+        self.polish_travelz_entry.hide()
+        self.polish_margin_lbl.hide()
+        self.polish_margin_entry.hide()
+        self.polish_over_lbl.hide()
+        self.polish_over_entry.hide()
+        self.polish_method_lbl.hide()
+        self.polish_method_combo.hide()
+
+        self.ois_polish = OptionalHideInputSection(
+            self.polish_cb,
+            [
+                self.polish_dia_lbl,
+                self.polish_dia_entry,
+                self.polish_pressure_lbl,
+                self.polish_pressure_entry,
+                self.polish_travelz_lbl,
+                self.polish_travelz_entry,
+                self.polish_margin_lbl,
+                self.polish_margin_entry,
+                self.polish_over_lbl,
+                self.polish_over_entry,
+                self.polish_method_lbl,
+                self.polish_method_combo
+            ]
+        )
+
+        separator_line2 = QtWidgets.QFrame()
+        separator_line2.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line2.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.grid4.addWidget(separator_line2, 26, 0, 1, 2)
+
+        # Button
+        self.generate_cnc_button = FCButton(_('Generate CNCJob object'))
+        self.generate_cnc_button.setIcon(QtGui.QIcon(self.app.resource_location + '/cnc16.png'))
+        self.generate_cnc_button.setToolTip('%s.\n%s' % (
+            _("Generate CNCJob object"),
+            _(
+                "Add / Select at least one tool in the tool-table.\n"
+                "Click the # header to select all, or Ctrl + LMB\n"
+                "for custom selection of tools.")))
+
+        self.generate_cnc_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.grid4.addWidget(self.generate_cnc_button, 28, 0, 1, 2)
+
+        self.grid4.addWidget(FCLabel(''), 30, 0, 1, 2)
+
+        # ##############
+        # Paint area ##
+        # ##############
+        self.tools_label = FCLabel('<b>%s</b>' % _('TOOLS'))
+        self.tools_label.setToolTip(
+            _("Launch Paint Tool in Tools Tab.")
+        )
+        self.grid4.addWidget(self.tools_label, 32, 0, 1, 2)
+
+        # Milling Tool - will create GCode for slot holes
+        self.milling_button = FCButton(_('Milling Tool'))
+        self.milling_button.setIcon(QtGui.QIcon(self.app.resource_location + '/milling_tool32.png'))
+        self.milling_button.setToolTip(
+            _("Generate a CNCJob by milling a Geometry.")
+        )
+        self.milling_button.setStyleSheet("""
+                        QPushButton
+                        {
+                            font-weight: bold;
+                        }
+                        """)
+        self.grid4.addWidget(self.milling_button, 34, 0, 1, 2)
+        # FIXME: until the Milling Tool is ready, this get disabled
+        self.milling_button.setDisabled(True)
+
+        # Paint Button
+        self.paint_tool_button = FCButton(_('Paint Tool'))
+        self.paint_tool_button.setIcon(QtGui.QIcon(self.app.resource_location + '/paint20_1.png'))
+        self.paint_tool_button.setToolTip(
+            _("Creates tool paths to cover the\n"
+              "whole area of a polygon.")
+        )
+
+        # self.paint_tool_button.setStyleSheet("""
+        #                 QPushButton
+        #                 {
+        #                     font-weight: bold;
+        #                 }
+        #                 """)
+        self.grid4.addWidget(self.paint_tool_button, 36, 0, 1, 2)
+
+        # NCC Tool
+        self.generate_ncc_button = FCButton(_('NCC Tool'))
+        self.generate_ncc_button.setIcon(QtGui.QIcon(self.app.resource_location + '/eraser26.png'))
+        self.generate_ncc_button.setToolTip(
+            _("Create the Geometry Object\n"
+              "for non-copper routing.")
+        )
+        # self.generate_ncc_button.setStyleSheet("""
+        #                 QPushButton
+        #                 {
+        #                     font-weight: bold;
+        #                 }
+        #                 """)
+        self.grid4.addWidget(self.generate_ncc_button, 38, 0, 1, 2)
+
+
+class CNCObjectUI(ObjectUI):
+    """
+    User interface for CNCJob objects.
+    """
+
+    def __init__(self, app, parent=None):
+        """
+        Creates the user interface for CNCJob objects. GUI elements should
+        be placed in ``self.custom_box`` to preserve the layout.
+        """
+
+        self.decimals = app.decimals
+        self.app = app
+
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if theme_settings.contains("theme"):
+            theme = theme_settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            self.resource_loc = 'assets/resources'
+        else:
+            self.resource_loc = 'assets/resources'
+
+        ObjectUI.__init__(
+            self, title=_('CNC Job Object'),
+            icon_file=self.resource_loc + '/cnc32.png', parent=parent,
+            app=self.app)
+
+        for i in range(0, self.common_grid.count()):
+            self.common_grid.itemAt(i).widget().hide()
+
+        f_lay = QtWidgets.QGridLayout()
+        f_lay.setColumnStretch(0, 0)
+        f_lay.setColumnStretch(1, 1)
+        self.custom_box.addLayout(f_lay)
+
+        # Plot Options
+        self.cncplot_method_label = FCLabel("<b>%s:</b>" % _("Plot Options"))
+        self.cncplot_method_label.setToolTip(
+            _(
+                "This selects the kind of geometries on the canvas to plot.\n"
+                "Those can be either of type 'Travel' which means the moves\n"
+                "above the work piece or it can be of type 'Cut',\n"
+                "which means the moves that cut into the material."
+            )
+        )
+
+        self.cncplot_method_combo = RadioSet([
+            {"label": _("All"), "value": "all"},
+            {"label": _("Travel"), "value": "travel"},
+            {"label": _("Cut"), "value": "cut"}
+        ], stretch=False)
+
+        f_lay.addWidget(self.cncplot_method_label, 0, 0)
+        f_lay.addWidget(self.cncplot_method_combo, 0, 1, 1, 2)
+
+        self.name_hlay = QtWidgets.QHBoxLayout()
+        f_lay.addLayout(self.name_hlay, 2, 0, 1, 3)
+
+        # ## Object name
+        name_label = FCLabel("<b>%s:</b>" % _("Name"))
+        self.name_entry = FCEntry()
+        self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+        self.name_hlay.addWidget(name_label)
+        self.name_hlay.addWidget(self.name_entry)
+
+        # Editor
+        self.editor_button = FCButton(_('GCode Editor'))
+        self.editor_button.setIcon(QtGui.QIcon(self.app.resource_location + '/edit_file32.png'))
+
+        self.editor_button.setToolTip(
+            _("Start the Object Editor")
+        )
+        self.editor_button.setStyleSheet("""
+                                       QPushButton
+                                       {
+                                           font-weight: bold;
+                                       }
+                                       """)
+        f_lay.addWidget(self.editor_button, 4, 0, 1, 3)
+
+        # PROPERTIES CB
+        self.properties_button = FCButton('%s' % _("PROPERTIES"), checkable=True)
+        self.properties_button.setIcon(QtGui.QIcon(self.app.resource_location + '/properties32.png'))
+        self.properties_button.setToolTip(_("Show the Properties."))
+        self.properties_button.setStyleSheet("""
+                                      QPushButton
+                                      {
+                                          font-weight: bold;
+                                      }
+                                      """)
+        f_lay.addWidget(self.properties_button, 6, 0, 1, 3)
+
+        # PROPERTIES Frame
+        self.properties_frame = QtWidgets.QFrame()
+        self.properties_frame.setContentsMargins(0, 0, 0, 0)
+        f_lay.addWidget(self.properties_frame, 7, 0, 1, 3)
+        self.properties_box = QtWidgets.QVBoxLayout()
+        self.properties_box.setContentsMargins(0, 0, 0, 0)
+        self.properties_frame.setLayout(self.properties_box)
+        self.properties_frame.hide()
+
+        self.treeWidget = FCTree(columns=2)
+
+        self.properties_box.addWidget(self.treeWidget)
+        self.properties_box.setStretch(0, 0)
+
+        # Annotation
+        self.annotation_cb = FCCheckBox(_("Display Annotation"))
+        self.annotation_cb.setToolTip(
+            _("This selects if to display text annotation on the plot.\n"
+              "When checked it will display numbers in order for each end\n"
+              "of a travel line.")
+        )
+        f_lay.addWidget(self.annotation_cb, 8, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        f_lay.addWidget(separator_line, 10, 0, 1, 3)
+
+        # Travelled Distance
+        self.t_distance_label = FCLabel("<b>%s:</b>" % _("Travelled distance"))
+        self.t_distance_label.setToolTip(
+            _("This is the total travelled distance on X-Y plane.\n"
+              "In current units.")
+        )
+        self.t_distance_entry = FCEntry()
+        self.units_label = FCLabel()
+
+        f_lay.addWidget(self.t_distance_label, 12, 0)
+        f_lay.addWidget(self.t_distance_entry, 12, 1)
+        f_lay.addWidget(self.units_label, 12, 2)
+
+        # Estimated Time
+        self.t_time_label = FCLabel("<b>%s:</b>" % _("Estimated time"))
+        self.t_time_label.setToolTip(
+            _("This is the estimated time to do the routing/drilling,\n"
+              "without the time spent in ToolChange events.")
+        )
+        self.t_time_entry = FCEntry()
+        self.units_time_label = FCLabel()
+
+        f_lay.addWidget(self.t_time_label, 14, 0)
+        f_lay.addWidget(self.t_time_entry, 14, 1)
+        f_lay.addWidget(self.units_time_label, 14, 2)
+
+        self.t_distance_label.hide()
+        self.t_distance_entry.setVisible(False)
+        self.t_time_label.hide()
+        self.t_time_entry.setVisible(False)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        f_lay.addWidget(separator_line, 16, 0, 1, 3)
+
+        hlay = QtWidgets.QHBoxLayout()
+        self.custom_box.addLayout(hlay)
+
+        # CNC Tools Table for plot
+        self.cnc_tools_table_label = FCLabel('<b>%s</b>' % _('CNC Tools Table'))
+        self.cnc_tools_table_label.setToolTip(
+            _(
+                "Tools in this CNCJob object used for cutting.\n"
+                "The tool diameter is used for plotting on canvas.\n"
+                "The 'Offset' entry will set an offset for the cut.\n"
+                "'Offset' can be inside, outside, on path (none) and custom.\n"
+                "'Type' entry is only informative and it allow to know the \n"
+                "intent of using the current tool. \n"
+                "It can be Rough(ing), Finish(ing) or Iso(lation).\n"
+                "The 'Tool type'(TT) can be circular with 1 to 4 teeths(C1..C4),\n"
+                "ball(B), or V-Shaped(V)."
+            )
+        )
+        hlay.addWidget(self.cnc_tools_table_label)
+
+        # Plot CB
+        # self.plot_cb = QtWidgets.QCheckBox('Plot')
+        self.plot_cb = FCCheckBox(_('Plot Object'))
+        self.plot_cb.setToolTip(
+            _("Plot (show) this object.")
+        )
+        self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
+        hlay.addStretch()
+        hlay.addWidget(self.plot_cb)
+
+        self.cnc_tools_table = FCTable()
+        self.custom_box.addWidget(self.cnc_tools_table)
+
+        self.cnc_tools_table.setColumnCount(7)
+        self.cnc_tools_table.setColumnWidth(0, 20)
+        self.cnc_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Offset'), _('Type'), _('TT'), '', _('P')])
+        self.cnc_tools_table.setColumnHidden(5, True)
+        # stylesheet = "::section{Background-color:rgb(239,239,245)}"
+        # self.cnc_tools_table.horizontalHeader().setStyleSheet(stylesheet)
+
+        self.exc_cnc_tools_table = FCTable()
+        self.custom_box.addWidget(self.exc_cnc_tools_table)
+
+        self.exc_cnc_tools_table.setColumnCount(7)
+        self.exc_cnc_tools_table.setColumnWidth(0, 20)
+        self.exc_cnc_tools_table.setHorizontalHeaderLabels(['#', _('Dia'), _('Drills'), _('Slots'), '', _("Cut Z"),
+                                                            _('P')])
+        self.exc_cnc_tools_table.setColumnHidden(4, True)
+
+        self.tooldia_entry = FCDoubleSpinner(callback=self.confirmation_message)
+        self.tooldia_entry.set_range(0, 10000.0000)
+        self.tooldia_entry.set_precision(self.decimals)
+        self.tooldia_entry.setSingleStep(0.1)
+        self.custom_box.addWidget(self.tooldia_entry)
+
+        # Update plot button
+        self.updateplot_button = FCButton(_('Update Plot'))
+        self.updateplot_button.setToolTip(
+            _("Update the plot.")
+        )
+        self.custom_box.addWidget(self.updateplot_button)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.custom_box.addWidget(separator_line)
+
+        # CNC Code snippets
+        self.snippets_cb = FCCheckBox(_("Use CNC Code Snippets"))
+        self.snippets_cb.setToolTip(
+            _("When selected, it will include CNC Code snippets (append and prepend)\n"
+              "defined in the Preferences.")
+        )
+        self.custom_box.addWidget(self.snippets_cb)
+
+        # Autolevelling
+        self.sal_btn = FCButton('%s' % _("Autolevelling"), checkable=True)
+        # self.sal_btn.setIcon(QtGui.QIcon(self.app.resource_location + '/properties32.png'))
+        self.sal_btn.setToolTip(
+            _("Enable the autolevelling feature.")
+        )
+        self.sal_btn.setStyleSheet("""
+                                  QPushButton
+                                  {
+                                      font-weight: bold;
+                                  }
+                                  """)
+        self.custom_box.addWidget(self.sal_btn)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.custom_box.addWidget(separator_line)
+
+        self.al_frame = QtWidgets.QFrame()
+        self.al_frame.setContentsMargins(0, 0, 0, 0)
+        self.custom_box.addWidget(self.al_frame)
+
+        self.al_box = QtWidgets.QVBoxLayout()
+        self.al_box.setContentsMargins(0, 0, 0, 0)
+        self.al_frame.setLayout(self.al_box)
+
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.al_box.addLayout(grid0)
+
+        al_title = FCLabel('<b>%s</b>' % _("Probe Points Table"))
+        al_title.setToolTip(_("Generate GCode that will obtain the height map"))
+
+        self.show_al_table = FCCheckBox(_("Show"))
+        self.show_al_table.setToolTip(_("Toggle the display of the Probe Points table."))
+        self.show_al_table.setChecked(True)
+
+        hor_lay = QtWidgets.QHBoxLayout()
+        hor_lay.addWidget(al_title)
+        hor_lay.addStretch()
+        hor_lay.addWidget(self.show_al_table, alignment=QtCore.Qt.AlignRight)
+
+        grid0.addLayout(hor_lay, 0, 0, 1, 2)
+
+        self.al_probe_points_table = FCTable()
+        self.al_probe_points_table.setColumnCount(3)
+        self.al_probe_points_table.setColumnWidth(0, 20)
+        self.al_probe_points_table.setHorizontalHeaderLabels(['#', _('X-Y Coordinates'), _('Height')])
+
+        grid0.addWidget(self.al_probe_points_table, 1, 0, 1, 2)
+
+        self.plot_probing_pts_cb = FCCheckBox(_("Plot probing points"))
+        self.plot_probing_pts_cb.setToolTip(
+            _("Plot the probing points in the table.\n"
+              "If a Voronoi method is used then\n"
+              "the Voronoi areas are also plotted.")
+        )
+        grid0.addWidget(self.plot_probing_pts_cb, 3, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 5, 0, 1, 2)
+
+        # #############################################################################################################
+        # ############### Probe GCode Generation ######################################################################
+        # #############################################################################################################
+
+        self.probe_gc_label = FCLabel('<b>%s</b>:' % _("Probe GCode Generation"))
+        self.probe_gc_label.setToolTip(
+            _("Will create a GCode which will be sent to the controller,\n"
+              "either through a file or directly, with the intent to get the height map\n"
+              "that is to modify the original GCode to level the cutting height.")
+        )
+        grid0.addWidget(self.probe_gc_label, 7, 0, 1, 2)
+
+        # Travel Z Probe
+        self.ptravelz_label = FCLabel('%s:' % _("Probe Z travel"))
+        self.ptravelz_label.setToolTip(
+            _("The safe Z for probe travelling between probe points.")
+        )
+        self.ptravelz_entry = FCDoubleSpinner()
+        self.ptravelz_entry.set_precision(self.decimals)
+        self.ptravelz_entry.set_range(0.0000, 10000.0000)
+
+        grid0.addWidget(self.ptravelz_label, 9, 0)
+        grid0.addWidget(self.ptravelz_entry, 9, 1)
+
+        # Probe depth
+        self.pdepth_label = FCLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label.setToolTip(
+            _("The maximum depth that the probe is allowed\n"
+              "to probe. Negative value, in current units.")
+        )
+        self.pdepth_entry = FCDoubleSpinner()
+        self.pdepth_entry.set_precision(self.decimals)
+        self.pdepth_entry.set_range(-910000.0000, 0.0000)
+
+        grid0.addWidget(self.pdepth_label, 11, 0)
+        grid0.addWidget(self.pdepth_entry, 11, 1)
+
+        # Probe feedrate
+        self.feedrate_probe_label = FCLabel('%s:' % _("Probe Feedrate"))
+        self.feedrate_probe_label.setToolTip(
+           _("The feedrate used while the probe is probing.")
+        )
+        self.feedrate_probe_entry = FCDoubleSpinner()
+        self.feedrate_probe_entry.set_precision(self.decimals)
+        self.feedrate_probe_entry.set_range(0, 910000.0000)
+
+        grid0.addWidget(self.feedrate_probe_label, 13, 0)
+        grid0.addWidget(self.feedrate_probe_entry, 13, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 15, 0, 1, 2)
+
+        # AUTOLEVELL MODE
+        al_mode_lbl = FCLabel('<b>%s</b>:' % _("Mode"))
+        al_mode_lbl.setToolTip(_("Choose a mode for height map generation.\n"
+                                 "- Manual: will pick a selection of probe points by clicking on canvas\n"
+                                 "- Grid: will automatically generate a grid of probe points"))
+
+        self.al_mode_radio = RadioSet(
+            [
+                {'label': _('Manual'), 'value': 'manual'},
+                {'label': _('Grid'), 'value': 'grid'}
+            ])
+        grid0.addWidget(al_mode_lbl, 16, 0)
+        grid0.addWidget(self.al_mode_radio, 16, 1)
+
+        # AUTOLEVELL METHOD
+        self.al_method_lbl = FCLabel('%s:' % _("Method"))
+        self.al_method_lbl.setToolTip(_("Choose a method for approximation of heights from autolevelling data.\n"
+                                        "- Voronoi: will generate a Voronoi diagram\n"
+                                        "- Bilinear: will use bilinear interpolation. Usable only for grid mode."))
+
+        self.al_method_radio = RadioSet(
+            [
+                {'label': _('Voronoi'), 'value': 'v'},
+                {'label': _('Bilinear'), 'value': 'b'}
+            ])
+        self.al_method_lbl.setDisabled(True)
+        self.al_method_radio.setDisabled(True)
+        self.al_method_radio.set_value('v')
+
+        grid0.addWidget(self.al_method_lbl, 17, 0)
+        grid0.addWidget(self.al_method_radio, 17, 1)
+
+        # ## Columns
+        self.al_columns_entry = FCSpinner()
+        self.al_columns_entry.setMinimum(2)
+
+        self.al_columns_label = FCLabel('%s:' % _("Columns"))
+        self.al_columns_label.setToolTip(
+            _("The number of grid columns.")
+        )
+        grid0.addWidget(self.al_columns_label, 19, 0)
+        grid0.addWidget(self.al_columns_entry, 19, 1)
+
+        # ## Rows
+        self.al_rows_entry = FCSpinner()
+        self.al_rows_entry.setMinimum(2)
+
+        self.al_rows_label = FCLabel('%s:' % _("Rows"))
+        self.al_rows_label.setToolTip(
+            _("The number of grid rows.")
+        )
+        grid0.addWidget(self.al_rows_label, 21, 0)
+        grid0.addWidget(self.al_rows_entry, 21, 1)
+
+        self.al_add_button = FCButton(_("Add Probe Points"))
+        grid0.addWidget(self.al_add_button, 23, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 25, 0, 1, 2)
+
+        self.al_controller_label = FCLabel('<b>%s</b>:' % _("Controller"))
+        self.al_controller_label.setToolTip(
+            _("The kind of controller for which to generate\n"
+              "height map gcode.")
+        )
+
+        self.al_controller_combo = FCComboBox()
+        self.al_controller_combo.addItems(["MACH3", "MACH4", "LinuxCNC", "GRBL"])
+        grid0.addWidget(self.al_controller_label, 27, 0)
+        grid0.addWidget(self.al_controller_combo, 27, 1)
+
+        # #############################################################################################################
+        # ########################## GRBL frame #######################################################################
+        # #############################################################################################################
+        self.grbl_frame = QtWidgets.QFrame()
+        self.grbl_frame.setContentsMargins(0, 0, 0, 0)
+        grid0.addWidget(self.grbl_frame, 29, 0, 1, 2)
+
+        self.grbl_box = QtWidgets.QVBoxLayout()
+        self.grbl_box.setContentsMargins(0, 0, 0, 0)
+        self.grbl_frame.setLayout(self.grbl_box)
+
+        # #############################################################################################################
+        # ########################## GRBL TOOLBAR #####################################################################
+        # #############################################################################################################
+        self.al_toolbar = FCDetachableTab(protect=True, parent=self)
+        self.al_toolbar.setTabsClosable(False)
+        self.al_toolbar.useOldIndex(True)
+        self.al_toolbar.set_detachable(val=False)
+        self.grbl_box.addWidget(self.al_toolbar)
+
+        # GRBL Connect TAB
+        self.gr_conn_tab = QtWidgets.QWidget()
+        self.gr_conn_tab.setObjectName("connect_tab")
+        self.gr_conn_tab_layout = QtWidgets.QVBoxLayout(self.gr_conn_tab)
+        self.gr_conn_tab_layout.setContentsMargins(2, 2, 2, 2)
+        # self.gr_conn_scroll_area = VerticalScrollArea()
+        # self.gr_conn_tab_layout.addWidget(self.gr_conn_scroll_area)
+        self.al_toolbar.addTab(self.gr_conn_tab, _("Connect"))
+
+        # GRBL Control TAB
+        self.gr_ctrl_tab = QtWidgets.QWidget()
+        self.gr_ctrl_tab.setObjectName("connect_tab")
+        self.gr_ctrl_tab_layout = QtWidgets.QVBoxLayout(self.gr_ctrl_tab)
+        self.gr_ctrl_tab_layout.setContentsMargins(2, 2, 2, 2)
+
+        # self.gr_ctrl_scroll_area = VerticalScrollArea()
+        # self.gr_ctrl_tab_layout.addWidget(self.gr_ctrl_scroll_area)
+        self.al_toolbar.addTab(self.gr_ctrl_tab, _("Control"))
+
+        # GRBL Sender TAB
+        self.gr_send_tab = QtWidgets.QWidget()
+        self.gr_send_tab.setObjectName("connect_tab")
+        self.gr_send_tab_layout = QtWidgets.QVBoxLayout(self.gr_send_tab)
+        self.gr_send_tab_layout.setContentsMargins(2, 2, 2, 2)
+
+        # self.gr_send_scroll_area = VerticalScrollArea()
+        # self.gr_send_tab_layout.addWidget(self.gr_send_scroll_area)
+        self.al_toolbar.addTab(self.gr_send_tab, _("Sender"))
+
+        for idx in range(self.al_toolbar.count()):
+            if self.al_toolbar.tabText(idx) == _("Connect"):
+                self.al_toolbar.tabBar.setTabTextColor(idx, QtGui.QColor('red'))
+            if self.al_toolbar.tabText(idx) == _("Control"):
+                self.al_toolbar.tabBar.setTabEnabled(idx, False)
+            if self.al_toolbar.tabText(idx) == _("Sender"):
+                self.al_toolbar.tabBar.setTabEnabled(idx, False)
+        # #############################################################################################################
+
+        # #############################################################################################################
+        # GRBL CONNECT
+        # #############################################################################################################
+        grbl_conn_grid = QtWidgets.QGridLayout()
+        grbl_conn_grid.setColumnStretch(0, 0)
+        grbl_conn_grid.setColumnStretch(1, 1)
+        grbl_conn_grid.setColumnStretch(2, 0)
+        self.gr_conn_tab_layout.addLayout(grbl_conn_grid)
+
+        # COM list
+        self.com_list_label = FCLabel('%s:' % _("COM list"))
+        self.com_list_label.setToolTip(
+            _("Lists the available serial ports.")
+        )
+
+        self.com_list_combo = FCComboBox()
+        self.com_search_button = FCButton(_("Search"))
+        self.com_search_button.setToolTip(
+            _("Search for the available serial ports.")
+        )
+        grbl_conn_grid.addWidget(self.com_list_label, 2, 0)
+        grbl_conn_grid.addWidget(self.com_list_combo, 2, 1)
+        grbl_conn_grid.addWidget(self.com_search_button, 2, 2)
+
+        # BAUDRATES list
+        self.baudrates_list_label = FCLabel('%s:' % _("Baud rates"))
+        self.baudrates_list_label.setToolTip(
+            _("Lists the available serial ports.")
+        )
+
+        self.baudrates_list_combo = FCComboBox()
+        cbmodel = QtCore.QStringListModel()
+        self.baudrates_list_combo.setModel(cbmodel)
+        self.baudrates_list_combo.addItems(
+            ['9600', '19200', '38400', '57600', '115200', '230400', '460800', '500000', '576000', '921600', '1000000',
+             '1152000', '1500000', '2000000'])
+        self.baudrates_list_combo.setCurrentText('115200')
+
+        grbl_conn_grid.addWidget(self.baudrates_list_label, 4, 0)
+        grbl_conn_grid.addWidget(self.baudrates_list_combo, 4, 1)
+
+        # New baudrate
+        self.new_bd_label = FCLabel('%s:' % _("New"))
+        self.new_bd_label.setToolTip(
+            _("New, custom baudrate.")
+        )
+
+        self.new_baudrate_entry = FCSpinner()
+        self.new_baudrate_entry.set_range(40, 9999999)
+
+        self.add_bd_button = FCButton(_("Add"))
+        self.add_bd_button.setToolTip(
+            _("Add the specified custom baudrate to the list.")
+        )
+        grbl_conn_grid.addWidget(self.new_bd_label, 6, 0)
+        grbl_conn_grid.addWidget(self.new_baudrate_entry, 6, 1)
+        grbl_conn_grid.addWidget(self.add_bd_button, 6, 2)
+
+        self.del_bd_button = FCButton(_("Delete selected baudrate"))
+        grbl_conn_grid.addWidget(self.del_bd_button, 8, 0, 1, 3)
+
+        ctrl_hlay = QtWidgets.QHBoxLayout()
+        self.controller_reset_button = FCButton(_("Reset"))
+        self.controller_reset_button.setToolTip(
+            _("Software reset of the controller.")
+        )
+        self.controller_reset_button.setDisabled(True)
+        ctrl_hlay.addWidget(self.controller_reset_button)
+
+        self.com_connect_button = FCButton()
+        self.com_connect_button.setText(_("Disconnected"))
+        self.com_connect_button.setToolTip(
+            _("Connect to the selected port with the selected baud rate.")
+        )
+        self.com_connect_button.setStyleSheet("QPushButton {background-color: red;}")
+        ctrl_hlay.addWidget(self.com_connect_button)
+
+        grbl_conn_grid.addWidget(FCLabel(""), 9, 0, 1, 3)
+        grbl_conn_grid.setRowStretch(9, 1)
+        grbl_conn_grid.addLayout(ctrl_hlay, 10, 0, 1, 3)
+
+        # #############################################################################################################
+        # GRBL CONTROL
+        # #############################################################################################################
+        grbl_ctrl_grid = QtWidgets.QGridLayout()
+        grbl_ctrl_grid.setColumnStretch(0, 0)
+        grbl_ctrl_grid.setColumnStretch(1, 1)
+        grbl_ctrl_grid.setColumnStretch(2, 0)
+        self.gr_ctrl_tab_layout.addLayout(grbl_ctrl_grid)
+
+        grbl_ctrl2_grid = QtWidgets.QGridLayout()
+        grbl_ctrl2_grid.setColumnStretch(0, 0)
+        grbl_ctrl2_grid.setColumnStretch(1, 1)
+        self.gr_ctrl_tab_layout.addLayout(grbl_ctrl2_grid)
+
+        self.gr_ctrl_tab_layout.addStretch(1)
+
+        jog_title_label = FCLabel(_("Jog"))
+        jog_title_label.setStyleSheet("""
+                                FCLabel
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+
+        zero_title_label = FCLabel(_("Zero Axes"))
+        zero_title_label.setStyleSheet("""
+                                FCLabel
+                                {
+                                    font-weight: bold;
+                                }
+                                """)
+
+        grbl_ctrl_grid.addWidget(jog_title_label, 0, 0)
+        grbl_ctrl_grid.addWidget(zero_title_label, 0, 2)
+
+        self.jog_wdg = FCJog(self.app)
+        self.jog_wdg.setStyleSheet("""
+                            FCJog
+                            {
+                                border: 1px solid lightgray;
+                                border-radius: 5px;
+                            }
+                            """)
+
+        self.zero_axs_wdg = FCZeroAxes(self.app)
+        self.zero_axs_wdg.setStyleSheet("""
+                            FCZeroAxes
+                            {
+                                border: 1px solid lightgray;
+                                border-radius: 5px
+                            }
+                            """)
+        grbl_ctrl_grid.addWidget(self.jog_wdg, 2, 0)
+        grbl_ctrl_grid.addWidget(self.zero_axs_wdg, 2, 2)
+
+        self.pause_resume_button = RotatedToolButton()
+        self.pause_resume_button.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+        self.pause_resume_button.setText(_("Pause/Resume"))
+        self.pause_resume_button.setCheckable(True)
+        self.pause_resume_button.setStyleSheet("""
+                            RotatedToolButton:checked
+                            {
+                                background-color: red;
+                                color: white;
+                                border: none;
+                            }
+                            """)
+
+        pause_frame = QtWidgets.QFrame()
+        pause_frame.setContentsMargins(0, 0, 0, 0)
+        pause_frame.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Expanding)
+        pause_hlay = QtWidgets.QHBoxLayout()
+        pause_hlay.setContentsMargins(0, 0, 0, 0)
+
+        pause_hlay.addWidget(self.pause_resume_button)
+        pause_frame.setLayout(pause_hlay)
+        grbl_ctrl_grid.addWidget(pause_frame, 2, 1)
+
+        # JOG Step
+        self.jog_step_label = FCLabel('%s:' % _("Step"))
+        self.jog_step_label.setToolTip(
+            _("Each jog action will move the axes with this value.")
+        )
+
+        self.jog_step_entry = FCSliderWithDoubleSpinner()
+        self.jog_step_entry.set_precision(self.decimals)
+        self.jog_step_entry.setSingleStep(0.1)
+        self.jog_step_entry.set_range(0, 500)
+
+        grbl_ctrl2_grid.addWidget(self.jog_step_label, 0, 0)
+        grbl_ctrl2_grid.addWidget(self.jog_step_entry, 0, 1)
+
+        # JOG Feedrate
+        self.jog_fr_label = FCLabel('%s:' % _("Feedrate"))
+        self.jog_fr_label.setToolTip(
+            _("Feedrate when jogging.")
+        )
+
+        self.jog_fr_entry = FCSliderWithDoubleSpinner()
+        self.jog_fr_entry.set_precision(self.decimals)
+        self.jog_fr_entry.setSingleStep(10)
+        self.jog_fr_entry.set_range(0, 10000)
+
+        grbl_ctrl2_grid.addWidget(self.jog_fr_label, 1, 0)
+        grbl_ctrl2_grid.addWidget(self.jog_fr_entry, 1, 1)
+
+        # #############################################################################################################
+        # GRBL SENDER
+        # #############################################################################################################
+        grbl_send_grid = QtWidgets.QGridLayout()
+        grbl_send_grid.setColumnStretch(0, 1)
+        grbl_send_grid.setColumnStretch(1, 0)
+        self.gr_send_tab_layout.addLayout(grbl_send_grid)
+
+        # Send CUSTOM COMMAND
+        self.grbl_command_label = FCLabel('%s:' % _("Send Command"))
+        self.grbl_command_label.setToolTip(
+            _("Send a custom command to GRBL.")
+        )
+        grbl_send_grid.addWidget(self.grbl_command_label, 2, 0, 1, 2)
+
+        self.grbl_command_entry = FCEntry()
+        self.grbl_command_entry.setPlaceholderText(_("Type GRBL command ..."))
+
+        self.grbl_send_button = QtWidgets.QToolButton()
+        self.grbl_send_button.setText(_("Send"))
+        self.grbl_send_button.setToolTip(
+            _("Send a custom command to GRBL.")
+        )
+        grbl_send_grid.addWidget(self.grbl_command_entry, 4, 0)
+        grbl_send_grid.addWidget(self.grbl_send_button, 4, 1)
+
+        # Get Parameter
+        self.grbl_get_param_label = FCLabel('%s:' % _("Get Config parameter"))
+        self.grbl_get_param_label.setToolTip(
+            _("A GRBL configuration parameter.")
+        )
+        grbl_send_grid.addWidget(self.grbl_get_param_label, 6, 0, 1, 2)
+
+        self.grbl_parameter_entry = FCEntry()
+        self.grbl_parameter_entry.setPlaceholderText(_("Type GRBL parameter ..."))
+
+        self.grbl_get_param_button = QtWidgets.QToolButton()
+        self.grbl_get_param_button.setText(_("Get"))
+        self.grbl_get_param_button.setToolTip(
+            _("Get the value of a specified GRBL parameter.")
+        )
+        grbl_send_grid.addWidget(self.grbl_parameter_entry, 8, 0)
+        grbl_send_grid.addWidget(self.grbl_get_param_button, 8, 1)
+
+        grbl_send_grid.setRowStretch(9, 1)
+
+        # GET Report
+        self.grbl_report_button = FCButton(_("Get Report"))
+        self.grbl_report_button.setToolTip(
+            _("Print in shell the GRBL report.")
+        )
+        grbl_send_grid.addWidget(self.grbl_report_button, 10, 0, 1, 2)
+
+        hm_lay = QtWidgets.QHBoxLayout()
+        # GET HEIGHT MAP
+        self.grbl_get_heightmap_button = FCButton(_("Apply AutoLevelling"))
+        self.grbl_get_heightmap_button.setToolTip(
+            _("Will send the probing GCode to the GRBL controller,\n"
+              "wait for the Z probing data and then apply this data\n"
+              "over the original GCode therefore doing autolevelling.")
+        )
+        hm_lay.addWidget(self.grbl_get_heightmap_button, stretch=1)
+
+        self.grbl_save_height_map_button = QtWidgets.QToolButton()
+        self.grbl_save_height_map_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.grbl_save_height_map_button.setToolTip(
+            _("Will save the GRBL height map.")
+        )
+        hm_lay.addWidget(self.grbl_save_height_map_button, stretch=0, alignment=Qt.AlignRight)
+
+        grbl_send_grid.addLayout(hm_lay, 12, 0, 1, 2)
+
+        self.grbl_frame.hide()
+        # #############################################################################################################
+
+        height_lay = QtWidgets.QHBoxLayout()
+        self.h_gcode_button = FCButton(_("Save Probing GCode"))
+        self.h_gcode_button.setToolTip(
+            _("Will save the probing GCode.")
+        )
+        self.h_gcode_button.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding)
+
+        height_lay.addWidget(self.h_gcode_button)
+        self.view_h_gcode_button = QtWidgets.QToolButton()
+        self.view_h_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/edit_file32.png'))
+        # self.view_h_gcode_button.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
+        self.view_h_gcode_button.setToolTip(
+            _("View/Edit the probing GCode.")
+        )
+        # height_lay.addStretch()
+        height_lay.addWidget(self.view_h_gcode_button)
+
+        grid0.addLayout(height_lay, 31, 0, 1, 2)
+
+        self.import_heights_button = FCButton(_("Import Height Map"))
+        self.import_heights_button.setToolTip(
+            _("Import the file that has the Z heights\n"
+              "obtained through probing and then apply this data\n"
+              "over the original GCode therefore\n"
+              "doing autolevelling.")
+        )
+        grid0.addWidget(self.import_heights_button, 33, 0, 1, 2)
+
+        self.h_gcode_button.hide()
+        self.import_heights_button.hide()
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 35, 0, 1, 2)
+
+        # #############################################################################################################
+        # ## Export G-Code ##
+        # #############################################################################################################
+        self.export_gcode_label = FCLabel("<b>%s:</b>" % _("Export CNC Code"))
+        self.export_gcode_label.setToolTip(
+            _("Export and save G-Code to\n"
+              "make this object to a file.")
+        )
+        self.custom_box.addWidget(self.export_gcode_label)
+
+        g_export_lay = QtWidgets.QHBoxLayout()
+        self.custom_box.addLayout(g_export_lay)
+
+        # Save Button
+        self.export_gcode_button = FCButton(_('Save CNC Code'))
+        self.export_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/save_as.png'))
+        self.export_gcode_button.setToolTip(
+            _("Opens dialog to save G-Code\n"
+              "file.")
+        )
+        self.export_gcode_button.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+        g_export_lay.addWidget(self.export_gcode_button)
+
+        self.review_gcode_button = QtWidgets.QToolButton()
+        self.review_gcode_button.setToolTip(_("Review CNC Code."))
+        self.review_gcode_button.setIcon(QtGui.QIcon(self.app.resource_location + '/find32.png'))
+        g_export_lay.addWidget(self.review_gcode_button)
+
+        self.custom_box.addStretch()
+
+        self.al_probe_points_table.setRowCount(0)
+        self.al_probe_points_table.resizeColumnsToContents()
+        self.al_probe_points_table.resizeRowsToContents()
+        v_header = self.al_probe_points_table.verticalHeader()
+        v_header.hide()
+        self.al_probe_points_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        h_header = self.al_probe_points_table.horizontalHeader()
+        h_header.setMinimumSectionSize(10)
+        h_header.setDefaultSectionSize(70)
+        h_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        h_header.resizeSection(0, 20)
+        h_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        h_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+
+        self.al_probe_points_table.setMinimumHeight(self.al_probe_points_table.getHeight())
+        self.al_probe_points_table.setMaximumHeight(self.al_probe_points_table.getHeight())
+
+        # Set initial UI
+        self.al_frame.hide()
+        self.al_rows_entry.setDisabled(True)
+        self.al_rows_label.setDisabled(True)
+        self.al_columns_entry.setDisabled(True)
+        self.al_columns_label.setDisabled(True)
+        self.al_method_lbl.setDisabled(True)
+        self.al_method_radio.setDisabled(True)
+        self.al_method_radio.set_value('v')
+        # self.on_mode_radio(val='grid')
+
+
+class ScriptObjectUI(ObjectUI):
+    """
+    User interface for Script  objects.
+    """
+
+    def __init__(self, app, parent=None):
+        """
+        Creates the user interface for Script objects. GUI elements should
+        be placed in ``self.custom_box`` to preserve the layout.
+        """
+
+        self.decimals = app.decimals
+        self.app = app
+
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if theme_settings.contains("theme"):
+            theme = theme_settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            self.resource_loc = 'assets/resources'
+        else:
+            self.resource_loc = 'assets/resources'
+
+        ObjectUI.__init__(self, title=_('Script Object'),
+                          icon_file=self.resource_loc + '/script_new24.png',
+                          parent=parent,
+                          common=False,
+                          app=self.app)
+
+        # ## Object name
+        self.name_hlay = QtWidgets.QHBoxLayout()
+        self.custom_box.addLayout(self.name_hlay)
+
+        name_label = FCLabel("<b>%s:</b>" % _("Name"))
+        self.name_entry = FCEntry()
+        self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
+        self.name_hlay.addWidget(name_label)
+        self.name_hlay.addWidget(self.name_entry)
+
+        h_lay = QtWidgets.QHBoxLayout()
+        h_lay.setAlignment(QtCore.Qt.AlignVCenter)
+        self.custom_box.addLayout(h_lay)
+
+        self.autocomplete_cb = FCCheckBox("%s" % _("Auto Completer"))
+        self.autocomplete_cb.setToolTip(
+            _("This selects if the auto completer is enabled in the Script Editor.")
+        )
+        self.autocomplete_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        h_lay.addWidget(self.autocomplete_cb)
+        h_lay.addStretch()
+
+        # Plot CB - this is added only for compatibility; other FlatCAM objects expect it and the mechanism is already
+        # established and I don't want to changed it right now
+        self.plot_cb = FCCheckBox()
+        self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
+        self.custom_box.addWidget(self.plot_cb)
+        self.plot_cb.hide()
+
+        self.custom_box.addStretch()
+
+
+class DocumentObjectUI(ObjectUI):
+    """
+    User interface for Notes objects.
+    """
+
+    def __init__(self, app, parent=None):
+        """
+        Creates the user interface for Notes objects. GUI elements should
+        be placed in ``self.custom_box`` to preserve the layout.
+        """
+
+        self.decimals = app.decimals
+        self.app = app
+
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if theme_settings.contains("theme"):
+            theme = theme_settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            self.resource_loc = 'assets/resources'
+        else:
+            self.resource_loc = 'assets/resources'
+
+        ObjectUI.__init__(self, title=_('Document Object'),
+                          icon_file=self.resource_loc + '/notes16_1.png',
+                          parent=parent,
+                          common=False,
+                          app=self.app)
+
+        # ## Object name
+        self.name_hlay = QtWidgets.QHBoxLayout()
+        self.custom_box.addLayout(self.name_hlay)
+
+        name_label = FCLabel("<b>%s:</b>" % _("Name"))
+        self.name_entry = FCEntry()
+        self.name_entry.setFocusPolicy(QtCore.Qt.StrongFocus)
+        self.name_hlay.addWidget(name_label)
+        self.name_hlay.addWidget(self.name_entry)
+
+        # Plot CB - this is added only for compatibility; other FlatCAM objects expect it and the mechanism is already
+        # established and I don't want to changed it right now
+        self.plot_cb = FCCheckBox()
+        self.plot_cb.setLayoutDirection(QtCore.Qt.RightToLeft)
+        self.custom_box.addWidget(self.plot_cb)
+        self.plot_cb.hide()
+
+        h_lay = QtWidgets.QHBoxLayout()
+        h_lay.setAlignment(QtCore.Qt.AlignVCenter)
+        self.custom_box.addLayout(h_lay)
+
+        self.autocomplete_cb = FCCheckBox("%s" % _("Auto Completer"))
+        self.autocomplete_cb.setToolTip(
+            _("This selects if the auto completer is enabled in the Document Editor.")
+        )
+        self.autocomplete_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        h_lay.addWidget(self.autocomplete_cb)
+        h_lay.addStretch()
+
+        # ##############################################################
+        # ############ FORM LAYOUT #####################################
+        # ##############################################################
+
+        self.form_box = QtWidgets.QFormLayout()
+        self.custom_box.addLayout(self.form_box)
+
+        # Font
+        self.font_type_label = FCLabel('%s:' % _("Font Type"))
+
+        if sys.platform == "win32":
+            f_current = QtGui.QFont("Arial")
+        elif sys.platform == "linux":
+            f_current = QtGui.QFont("FreeMono")
+        else:
+            f_current = QtGui.QFont("Helvetica Neue")
+
+        self.font_name = f_current.family()
+
+        self.font_type_cb = QtWidgets.QFontComboBox(self)
+        self.font_type_cb.setCurrentFont(f_current)
+
+        self.form_box.addRow(self.font_type_label, self.font_type_cb)
+
+        # Font Size
+        self.font_size_label = FCLabel('%s:' % _("Font Size"))
+
+        self.font_size_cb = FCComboBox()
+        self.font_size_cb.setEditable(True)
+        self.font_size_cb.setMinimumContentsLength(3)
+        self.font_size_cb.setMaximumWidth(70)
+
+        font_sizes = ['6', '7', '8', '9', '10', '11', '12', '13', '14',
+                      '15', '16', '18', '20', '22', '24', '26', '28',
+                      '32', '36', '40', '44', '48', '54', '60', '66',
+                      '72', '80', '88', '96']
+
+        for i in font_sizes:
+            self.font_size_cb.addItem(i)
+
+        size_hlay = QtWidgets.QHBoxLayout()
+        size_hlay.addWidget(self.font_size_cb)
+        size_hlay.addStretch()
+
+        self.font_bold_tb = QtWidgets.QToolButton()
+        self.font_bold_tb.setCheckable(True)
+        self.font_bold_tb.setIcon(QtGui.QIcon(self.resource_loc + '/bold32.png'))
+        size_hlay.addWidget(self.font_bold_tb)
+
+        self.font_italic_tb = QtWidgets.QToolButton()
+        self.font_italic_tb.setCheckable(True)
+        self.font_italic_tb.setIcon(QtGui.QIcon(self.resource_loc + '/italic32.png'))
+        size_hlay.addWidget(self.font_italic_tb)
+        self.font_under_tb = QtWidgets.QToolButton()
+        self.font_under_tb.setCheckable(True)
+        self.font_under_tb.setIcon(QtGui.QIcon(self.resource_loc + '/underline32.png'))
+        size_hlay.addWidget(self.font_under_tb)
+
+        self.form_box.addRow(self.font_size_label, size_hlay)
+
+        # Alignment Choices
+        self.alignment_label = FCLabel('%s:' % _("Alignment"))
+
+        al_hlay = QtWidgets.QHBoxLayout()
+
+        self.al_left_tb = QtWidgets.QToolButton()
+        self.al_left_tb.setToolTip(_("Align Left"))
+        self.al_left_tb.setIcon(QtGui.QIcon(self.resource_loc + '/align_left32.png'))
+        al_hlay.addWidget(self.al_left_tb)
+
+        self.al_center_tb = QtWidgets.QToolButton()
+        self.al_center_tb.setToolTip(_("Center"))
+        self.al_center_tb.setIcon(QtGui.QIcon(self.resource_loc + '/align_center32.png'))
+        al_hlay.addWidget(self.al_center_tb)
+
+        self.al_right_tb = QtWidgets.QToolButton()
+        self.al_right_tb.setToolTip(_("Align Right"))
+        self.al_right_tb.setIcon(QtGui.QIcon(self.resource_loc + '/align_right32.png'))
+        al_hlay.addWidget(self.al_right_tb)
+
+        self.al_justify_tb = QtWidgets.QToolButton()
+        self.al_justify_tb.setToolTip(_("Justify"))
+        self.al_justify_tb.setIcon(QtGui.QIcon(self.resource_loc + '/align_justify32.png'))
+        al_hlay.addWidget(self.al_justify_tb)
+
+        self.form_box.addRow(self.alignment_label, al_hlay)
+
+        # Font Color
+        self.font_color_label = FCLabel('%s:' % _('Font Color'))
+        self.font_color_label.setToolTip(
+           _("Set the font color for the selected text")
+        )
+        self.font_color_entry = FCEntry()
+        self.font_color_button = FCButton()
+        self.font_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_1 = QtWidgets.QHBoxLayout()
+        self.form_box_child_1.addWidget(self.font_color_entry)
+        self.form_box_child_1.addWidget(self.font_color_button)
+        self.form_box_child_1.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        self.form_box.addRow(self.font_color_label, self.form_box_child_1)
+
+        # Selection Color
+        self.sel_color_label = FCLabel('%s:' % _('Selection Color'))
+        self.sel_color_label.setToolTip(
+           _("Set the selection color when doing text selection.")
+        )
+        self.sel_color_entry = FCEntry()
+        self.sel_color_button = FCButton()
+        self.sel_color_button.setFixedSize(15, 15)
+
+        self.form_box_child_2 = QtWidgets.QHBoxLayout()
+        self.form_box_child_2.addWidget(self.sel_color_entry)
+        self.form_box_child_2.addWidget(self.sel_color_button)
+        self.form_box_child_2.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+
+        self.form_box.addRow(self.sel_color_label, self.form_box_child_2)
+
+        # Tab size
+        self.tab_size_label = FCLabel('%s:' % _('Tab Size'))
+        self.tab_size_label.setToolTip(
+            _("Set the tab size. In pixels. Default value is 80 pixels.")
+        )
+        self.tab_size_spinner = FCSpinner(callback=self.confirmation_message_int)
+        self.tab_size_spinner.set_range(0, 1000)
+
+        self.form_box.addRow(self.tab_size_label, self.tab_size_spinner)
+
+        self.custom_box.addStretch()
+
+# end of file

+ 587 - 0
appGUI/PlotCanvas.py

@@ -0,0 +1,587 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# Author: Dennis Hayrullin (c)                             #
+# Date: 2016                                               #
+# MIT Licence                                              #
+# ##########################################################
+
+from PyQt5 import QtCore
+
+import logging
+from appGUI.VisPyCanvas import VisPyCanvas, Color
+from appGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
+from vispy.scene.visuals import InfiniteLine, Line, Rectangle, Text
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+import numpy as np
+from vispy.geometry import Rect
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class PlotCanvas(QtCore.QObject, VisPyCanvas):
+    """
+    Class handling the plotting area in the application.
+    """
+
+    def __init__(self, container, fcapp):
+        """
+        The constructor configures the VisPy figure that
+        will contain all plots, creates the base axes and connects
+        events to the plotting area.
+
+        :param container: The parent container in which to draw plots.
+        :rtype: PlotCanvas
+        """
+
+        # super(PlotCanvas, self).__init__()
+        # QtCore.QObject.__init__(self)
+        # VisPyCanvas.__init__(self)
+        super().__init__()
+
+        # VisPyCanvas does not allow new attributes. Override.
+        self.unfreeze()
+
+        self.fcapp = fcapp
+
+        # Parent container
+        self.container = container
+
+        settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if settings.contains("theme"):
+            theme = settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            self.line_color = (0.3, 0.0, 0.0, 1.0)
+            self.rect_hud_color = Color('#0000FF10')
+            self.text_hud_color = 'black'
+        else:
+            self.line_color = (0.4, 0.4, 0.4, 1.0)
+            self.rect_hud_color = Color('#80808040')
+            self.text_hud_color = 'white'
+
+        # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
+        # which might decrease performance
+        # self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
+        self.workspace_line = None
+
+        self.pagesize_dict = {}
+        self.pagesize_dict.update(
+            {
+                'A0': (841, 1189),
+                'A1': (594, 841),
+                'A2': (420, 594),
+                'A3': (297, 420),
+                'A4': (210, 297),
+                'A5': (148, 210),
+                'A6': (105, 148),
+                'A7': (74, 105),
+                'A8': (52, 74),
+                'A9': (37, 52),
+                'A10': (26, 37),
+
+                'B0': (1000, 1414),
+                'B1': (707, 1000),
+                'B2': (500, 707),
+                'B3': (353, 500),
+                'B4': (250, 353),
+                'B5': (176, 250),
+                'B6': (125, 176),
+                'B7': (88, 125),
+                'B8': (62, 88),
+                'B9': (44, 62),
+                'B10': (31, 44),
+
+                'C0': (917, 1297),
+                'C1': (648, 917),
+                'C2': (458, 648),
+                'C3': (324, 458),
+                'C4': (229, 324),
+                'C5': (162, 229),
+                'C6': (114, 162),
+                'C7': (81, 114),
+                'C8': (57, 81),
+                'C9': (40, 57),
+                'C10': (28, 40),
+
+                # American paper sizes
+                'LETTER': (8.5*25.4, 11*25.4),
+                'LEGAL': (8.5*25.4, 14*25.4),
+                'ELEVENSEVENTEEN': (11*25.4, 17*25.4),
+
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5*25.4, 8*25.4),
+                'HALF_LETTER': (5.5*25.4, 8*25.4),
+                'GOV_LETTER': (8*25.4, 10.5*25.4),
+                'GOV_LEGAL': (8.5*25.4, 13*25.4),
+                'LEDGER': (17*25.4, 11*25.4),
+            }
+        )
+
+        # <VisPyCanvas>
+        self.create_native()
+        self.native.setParent(self.fcapp.ui)
+
+        # <QtCore.QObject>
+        self.container.addWidget(self.native)
+
+        # ## AXIS # ##
+        self.v_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=True,
+                                   parent=self.view.scene)
+
+        self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=False,
+                                   parent=self.view.scene)
+
+        self.line_parent = None
+        if self.fcapp.defaults["global_cursor_color_enabled"]:
+            c_color = Color(self.fcapp.defaults["global_cursor_color"]).rgba
+        else:
+            c_color = self.line_color
+
+        self.cursor_v_line = InfiniteLine(pos=None, color=c_color, vertical=True,
+                                          parent=self.line_parent)
+
+        self.cursor_h_line = InfiniteLine(pos=None, color=c_color, vertical=False,
+                                          parent=self.line_parent)
+
+        # font size
+        qsettings = QtCore.QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("hud_font_size"):
+            fsize = qsettings.value('hud_font_size', type=int)
+        else:
+            fsize = 8
+
+        # units
+        units = self.fcapp.defaults["units"].lower()
+
+        # coordinates and anchors
+        height = fsize * 11     # 90. Constant 11 is something that works
+        width = height * 2      # width is double the height = it is something that works
+        center_x = (width / 2) + 5
+        center_y = (height / 2) + 5
+
+        # text
+        self.text_hud = Text('', color=self.text_hud_color, pos=(10, center_y), method='gpu', anchor_x='left',
+                             parent=None)
+        self.text_hud.font_size = fsize
+        self.text_hud.text = 'Dx:\t%s [%s]\nDy:\t%s [%s]\n\nX:  \t%s [%s]\nY:  \t%s [%s]' % \
+                             ('0.0000', units, '0.0000', units, '0.0000', units, '0.0000', units)
+
+        # rectangle
+        self.rect_hud = Rectangle(center=(center_x, center_y), width=width, height=height, radius=[5, 5, 5, 5],
+                                  border_color=self.rect_hud_color, color=self.rect_hud_color, parent=None)
+        self.rect_hud.set_gl_state(depth_test=False)
+
+        # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
+        # all CNC have a limited workspace
+        if self.fcapp.defaults['global_workspace'] is True:
+            self.draw_workspace(workspace_size=self.fcapp.defaults["global_workspaceT"])
+
+        # HUD Display
+        self.hud_enabled = False
+
+        # enable the HUD if it is activated in FlatCAM Preferences
+        if self.fcapp.defaults['global_hud'] is True:
+            self.on_toggle_hud(state=True, silent=True)
+
+        # Axis Display
+        self.axis_enabled = True
+
+        # enable Axis
+        self.on_toggle_axis(state=True, silent=True)
+
+        # enable Grid lines
+        self.grid_lines_enabled = True
+
+        self.shape_collections = []
+
+        self.shape_collection = self.new_shape_collection()
+        self.fcapp.pool_recreated.connect(self.on_pool_recreated)
+        self.text_collection = self.new_text_collection()
+
+        self.text_collection.enabled = True
+
+        self.c = None
+        self.big_cursor = None
+        # Keep VisPy canvas happy by letting it be "frozen" again.
+        self.freeze()
+        self.fit_view()
+
+        self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
+
+    def on_toggle_axis(self, signal=None, state=None, silent=None):
+        if not state:
+            state = not self.axis_enabled
+
+        if state:
+            self.axis_enabled = True
+            self.fcapp.defaults['global_axis'] = True
+            self.v_line.parent = self.view.scene
+            self.h_line.parent = self.view.scene
+            self.fcapp.ui.axis_status_label.setStyleSheet("""
+                                                          QLabel
+                                                          {
+                                                              color: black;
+                                                              background-color: orange;
+                                                          }
+                                                          """)
+            if silent is None:
+                self.fcapp.inform[str, bool].emit(_("Axis enabled."), False)
+        else:
+            self.axis_enabled = False
+            self.fcapp.defaults['global_axis'] = False
+            self.v_line.parent = None
+            self.h_line.parent = None
+            self.fcapp.ui.axis_status_label.setStyleSheet("")
+            if silent is None:
+                self.fcapp.inform[str, bool].emit(_("Axis disabled."), False)
+
+    def on_toggle_hud(self, signal=None, state=None, silent=None):
+        if state is None:
+            state = not self.hud_enabled
+
+        if state:
+            self.hud_enabled = True
+            self.rect_hud.parent = self.view
+            self.text_hud.parent = self.view
+            self.fcapp.defaults['global_hud'] = True
+            self.fcapp.ui.hud_label.setStyleSheet("""
+                                                  QLabel
+                                                  {
+                                                      color: black;
+                                                      background-color: mediumpurple;
+                                                  }
+                                                  """)
+            if silent is None:
+                self.fcapp.inform[str, bool].emit(_("HUD enabled."), False)
+
+        else:
+            self.hud_enabled = False
+            self.rect_hud.parent = None
+            self.text_hud.parent = None
+            self.fcapp.defaults['global_hud'] = False
+            self.fcapp.ui.hud_label.setStyleSheet("")
+            if silent is None:
+                self.fcapp.inform[str, bool].emit(_("HUD disabled."), False)
+
+    def on_toggle_grid_lines(self, signal=None, silent=None):
+        state = not self.grid_lines_enabled
+
+        if state:
+            self.fcapp.defaults['global_grid_lines'] = True
+            self.grid_lines_enabled = True
+            self.grid.parent = self.view.scene
+            if silent is None:
+                self.fcapp.inform[str, bool].emit(_("Grid enabled."), False)
+        else:
+            self.fcapp.defaults['global_grid_lines'] = False
+            self.grid_lines_enabled = False
+            self.grid.parent = None
+            if silent is None:
+                self.fcapp.inform[str, bool].emit(_("Grid disabled."), False)
+
+        # HACK: enabling/disabling the cursor seams to somehow update the shapes on screen
+        # - perhaps is a bug in VisPy implementation
+        if self.fcapp.grid_status():
+            self.fcapp.app_cursor.enabled = False
+            self.fcapp.app_cursor.enabled = True
+        else:
+            self.fcapp.app_cursor.enabled = True
+            self.fcapp.app_cursor.enabled = False
+
+    def draw_workspace(self, workspace_size):
+        """
+        Draw a rectangular shape on canvas to specify our valid workspace.
+        :param workspace_size: the workspace size; tuple
+        :return:
+        """
+        try:
+            if self.fcapp.defaults['units'].upper() == 'MM':
+                dims = self.pagesize_dict[workspace_size]
+            else:
+                dims = (self.pagesize_dict[workspace_size][0]/25.4, self.pagesize_dict[workspace_size][1]/25.4)
+        except Exception as e:
+            log.debug("PlotCanvas.draw_workspace() --> %s" % str(e))
+            return
+
+        if self.fcapp.defaults['global_workspace_orientation'] == 'l':
+            dims = (dims[1], dims[0])
+
+        a = np.array([(0, 0), (dims[0], 0), (dims[0], dims[1]), (0, dims[1])])
+
+        # if not self.workspace_line:
+        #     self.workspace_line = Line(pos=np.array((a[0], a[1], a[2], a[3], a[0])), color=(0.70, 0.3, 0.3, 0.7),
+        #                                antialias=True, method='agg', parent=self.view.scene)
+        # else:
+        #     self.workspace_line.parent = self.view.scene
+        self.workspace_line = Line(pos=np.array((a[0], a[1], a[2], a[3], a[0])), color=(0.70, 0.3, 0.3, 0.7),
+                                   antialias=True, method='agg', parent=self.view.scene)
+
+        self.fcapp.ui.wplace_label.set_value(workspace_size[:3])
+        self.fcapp.ui.wplace_label.setToolTip(workspace_size)
+        self.fcapp.ui.wplace_label.setStyleSheet("""
+                        QLabel
+                        {
+                            color: black;
+                            background-color: olivedrab;
+                        }
+                        """)
+
+    def delete_workspace(self):
+        try:
+            self.workspace_line.parent = None
+        except Exception:
+            pass
+        self.fcapp.ui.wplace_label.setStyleSheet("")
+
+    # redraw the workspace lines on the plot by re adding them to the parent view.scene
+    def restore_workspace(self):
+        try:
+            self.workspace_line.parent = self.view.scene
+        except Exception:
+            pass
+
+    def graph_event_connect(self, event_name, callback):
+        return getattr(self.events, event_name).connect(callback)
+
+    def graph_event_disconnect(self, event_name, callback=None):
+        if callback is None:
+            getattr(self.events, event_name).disconnect()
+        else:
+            getattr(self.events, event_name).disconnect(callback)
+
+    def zoom(self, factor, center=None):
+        """
+        Zooms the plot by factor around a given
+        center point. Takes care of re-drawing.
+
+        :param factor: Number by which to scale the plot.
+        :type factor: float
+        :param center: Coordinates [x, y] of the point around which to scale the plot.
+        :type center: list
+        :return: None
+        """
+        self.view.camera.zoom(factor, center)
+
+    def new_shape_group(self, shape_collection=None):
+        if shape_collection:
+            return ShapeGroup(shape_collection)
+        return ShapeGroup(self.shape_collection)
+
+    def new_shape_collection(self, **kwargs):
+        # sc = ShapeCollection(parent=self.view.scene, pool=self.app.pool, **kwargs)
+        # self.shape_collections.append(sc)
+        # return sc
+        return ShapeCollection(parent=self.view.scene, pool=self.fcapp.pool, **kwargs)
+
+    def new_cursor(self, big=None):
+        """
+        Will create a mouse cursor pointer on canvas
+
+        :param big: if True will create a mouse cursor made out of infinite lines
+        :return: the mouse cursor object
+        """
+        if big is True:
+            self.big_cursor = True
+            self.c = CursorBig(app=self.fcapp)
+
+            # in case there are multiple new_cursor calls, best to disconnect first the signals
+            try:
+                self.c.mouse_state_updated.disconnect(self.on_mouse_state)
+            except (TypeError, AttributeError):
+                pass
+            try:
+                self.c.mouse_position_updated.disconnect(self.on_mouse_position)
+            except (TypeError, AttributeError):
+                pass
+
+            self.c.mouse_state_updated.connect(self.on_mouse_state)
+            self.c.mouse_position_updated.connect(self.on_mouse_position)
+        else:
+            self.big_cursor = False
+            self.c = Cursor(pos=np.empty((0, 2)), parent=self.view.scene)
+            self.c.antialias = 0
+
+        return self.c
+
+    def on_mouse_state(self, state):
+        if state:
+            self.cursor_h_line.parent = self.view.scene
+            self.cursor_v_line.parent = self.view.scene
+        else:
+            self.cursor_h_line.parent = None
+            self.cursor_v_line.parent = None
+
+    def on_mouse_position(self, pos):
+
+        if self.fcapp.defaults['global_cursor_color_enabled']:
+            color = Color(self.fcapp.defaults['global_cursor_color']).rgba
+        else:
+            color = self.line_color
+
+        self.cursor_h_line.set_data(pos=pos[1], color=color)
+        self.cursor_v_line.set_data(pos=pos[0], color=color)
+        self.view.scene.update()
+
+    def on_mouse_scroll(self, event):
+        # key modifiers
+        modifiers = event.modifiers
+
+        pan_delta_x = self.fcapp.defaults["global_gridx"]
+        pan_delta_y = self.fcapp.defaults["global_gridy"]
+        curr_pos = event.pos
+
+        # Controlled pan by mouse wheel
+        if 'Shift' in modifiers:
+            p1 = np.array(curr_pos)[:2]
+
+            if event.delta[1] > 0:
+                curr_pos[0] -= pan_delta_x
+            else:
+                curr_pos[0] += pan_delta_x
+            p2 = np.array(curr_pos)[:2]
+            self.view.camera.pan(p2 - p1)
+        elif 'Control' in modifiers:
+            p1 = np.array(curr_pos)[:2]
+
+            if event.delta[1] > 0:
+                curr_pos[1] += pan_delta_y
+            else:
+                curr_pos[1] -= pan_delta_y
+            p2 = np.array(curr_pos)[:2]
+            self.view.camera.pan(p2 - p1)
+
+        if self.fcapp.grid_status():
+            pos_canvas = self.translate_coords(curr_pos)
+            pos = self.fcapp.geo_editor.snap(pos_canvas[0], pos_canvas[1])
+
+            # Update cursor
+            self.fcapp.app_cursor.set_data(np.asarray([(pos[0], pos[1])]),
+                                           symbol='++', edge_color=self.fcapp.cursor_color_3D,
+                                           edge_width=self.fcapp.defaults["global_cursor_width"],
+                                           size=self.fcapp.defaults["global_cursor_size"])
+
+    def new_text_group(self, collection=None):
+        if collection:
+            return TextGroup(collection)
+        else:
+            return TextGroup(self.text_collection)
+
+    def new_text_collection(self, **kwargs):
+        return TextCollection(parent=self.view.scene, **kwargs)
+
+    def fit_view(self, rect=None):
+
+        # Lock updates in other threads
+        self.shape_collection.lock_updates()
+
+        if not rect:
+            rect = Rect(-1, -1, 20, 20)
+            try:
+                rect.left, rect.right = self.shape_collection.bounds(axis=0)
+                rect.bottom, rect.top = self.shape_collection.bounds(axis=1)
+            except TypeError:
+                pass
+
+        # adjust the view camera to be slightly bigger than the bounds so the shape collection can be seen clearly
+        # otherwise the shape collection boundary will have no border
+        dx = rect.right - rect.left
+        dy = rect.top - rect.bottom
+        x_factor = dx * 0.02
+        y_factor = dy * 0.02
+
+        rect.left -= x_factor
+        rect.bottom -= y_factor
+        rect.right += x_factor
+        rect.top += y_factor
+
+        # rect.left *= 0.96
+        # rect.bottom *= 0.96
+        # rect.right *= 1.04
+        # rect.top *= 1.04
+
+        # units = self.fcapp.defaults['units'].upper()
+        # if units == 'MM':
+        #     compensation = 0.5
+        # else:
+        #     compensation = 0.5 / 25.4
+        # rect.left -= compensation
+        # rect.bottom -= compensation
+        # rect.right += compensation
+        # rect.top += compensation
+
+        self.view.camera.rect = rect
+
+        self.shape_collection.unlock_updates()
+
+    def fit_center(self, loc, rect=None):
+
+        # Lock updates in other threads
+        self.shape_collection.lock_updates()
+
+        if not rect:
+            try:
+                rect = Rect(loc[0]-20, loc[1]-20, 40, 40)
+            except TypeError:
+                pass
+
+        self.view.camera.rect = rect
+
+        self.shape_collection.unlock_updates()
+
+    def clear(self):
+        pass
+
+    def redraw(self):
+        self.shape_collection.redraw([])
+        self.text_collection.redraw()
+
+    def on_pool_recreated(self, pool):
+        self.shape_collection.pool = pool
+
+
+class CursorBig(QtCore.QObject):
+    """
+    This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
+    This way I don't have to chane (disable) things related to the cursor all over when
+    using the low performance Matplotlib 2D graphic engine.
+    """
+
+    mouse_state_updated = QtCore.pyqtSignal(bool)
+    mouse_position_updated = QtCore.pyqtSignal(list)
+
+    def __init__(self, app):
+        super().__init__()
+        self.app = app
+        self._enabled = None
+
+    @property
+    def enabled(self):
+        return True if self._enabled else False
+
+    @enabled.setter
+    def enabled(self, value):
+        self._enabled = value
+        self.mouse_state_updated.emit(value)
+
+    def set_data(self, pos, **kwargs):
+        """Internal event handler to draw the cursor when the mouse moves."""
+        # if 'edge_color' in kwargs:
+        #     color = kwargs['edge_color']
+        # else:
+        #     if self.app.defaults['global_theme'] == 'white':
+        #         color = '#000000FF'
+        #     else:
+        #         color = '#FFFFFFFF'
+
+        position = [pos[0][0], pos[0][1]]
+        self.mouse_position_updated.emit(position)

+ 1656 - 0
appGUI/PlotCanvasLegacy.py

@@ -0,0 +1,1656 @@
+############################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# http://caram.cl/software/flatcam                         #
+# Author: Juan Pablo Caram (c)                             #
+# Date: 2/5/2014                                           #
+# MIT Licence                                              #
+# Modified by Marius Stanciu 09/21/2019                    #
+############################################################
+
+from PyQt5 import QtCore
+from PyQt5.QtCore import pyqtSignal
+
+# needed for legacy mode
+# Used for solid polygons in Matplotlib
+from descartes.patch import PolygonPatch
+
+from shapely.geometry import Polygon, LineString, LinearRing
+
+from copy import deepcopy
+import logging
+
+import numpy as np
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+# Prevent conflict with Qt5 and above.
+from matplotlib import use as mpl_use
+mpl_use("Qt5Agg")
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.lines import Line2D
+from matplotlib.offsetbox import AnchoredText
+# from matplotlib.widgets import Cursor
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+log = logging.getLogger('base')
+
+
+class CanvasCache(QtCore.QObject):
+    """
+
+    Case story #1:
+
+    1) No objects in the project.
+    2) Object is created (app_obj.new_object() emits object_created(obj)).
+       on_object_created() adds (i) object to collection and emits
+       (ii) app_obj.new_object_available() then calls (iii) object.plot()
+    3) object.plot() creates axes if necessary on
+       app.collection.figure. Then plots on it.
+    4) Plots on a cache-size canvas (in background).
+    5) Plot completes. Bitmap is generated.
+    6) Visible canvas is painted.
+
+    """
+
+    # Signals:
+    # A bitmap is ready to be displayed.
+    new_screen = QtCore.pyqtSignal()
+
+    def __init__(self, plotcanvas, app, dpi=50):
+
+        super(CanvasCache, self).__init__()
+
+        self.app = app
+
+        self.plotcanvas = plotcanvas
+        self.dpi = dpi
+
+        self.figure = Figure(dpi=dpi)
+
+        self.axes = self.figure.add_axes([0.0, 0.0, 1.0, 1.0], alpha=1.0)
+        self.axes.set_frame_on(False)
+        self.axes.set_xticks([])
+        self.axes.set_yticks([])
+
+        if self.app.defaults['global_theme'] == 'white':
+            self.axes.set_facecolor('#FFFFFF')
+        else:
+            self.axes.set_facecolor('#000000')
+
+        self.canvas = FigureCanvas(self.figure)
+
+        self.cache = None
+
+    def run(self):
+
+        log.debug("CanvasCache Thread Started!")
+        self.plotcanvas.update_screen_request.connect(self.on_update_req)
+
+    def on_update_req(self, extents):
+        """
+        Event handler for an updated display request.
+
+        :param extents: [xmin, xmax, ymin, ymax, zoom(optional)]
+        """
+
+        # log.debug("Canvas update requested: %s" % str(extents))
+
+        # Note: This information below might be out of date. Establish
+        # a protocol regarding when to change the canvas in the main
+        # thread and when to check these values here in the background,
+        # or pass this data in the signal (safer).
+        # log.debug("Size: %s [px]" % str(self.plotcanvas.get_axes_pixelsize()))
+        # log.debug("Density: %s [units/px]" % str(self.plotcanvas.get_density()))
+
+        # Move the requested screen portion to the main thread
+        # and inform about the update:
+
+        self.new_screen.emit()
+
+        # Continue to update the cache.
+
+    # def on_app_obj.new_object_available(self):
+    #
+    #     log.debug("A new object is available. Should plot it!")
+
+
+class PlotCanvasLegacy(QtCore.QObject):
+    """
+    Class handling the plotting area in the application.
+    """
+
+    # Signals:
+    # Request for new bitmap to display. The parameter
+    # is a list with [xmin, xmax, ymin, ymax, zoom(optional)]
+    update_screen_request = QtCore.pyqtSignal(list)
+
+    double_click = QtCore.pyqtSignal(object)
+
+    def __init__(self, container, app):
+        """
+        The constructor configures the Matplotlib figure that
+        will contain all plots, creates the base axes and connects
+        events to the plotting area.
+
+        :param container: The parent container in which to draw plots.
+        :rtype: PlotCanvas
+        """
+
+        super(PlotCanvasLegacy, self).__init__()
+
+        self.app = app
+
+        if self.app.defaults['global_theme'] == 'white':
+            theme_color = '#FFFFFF'
+            tick_color = '#000000'
+            self.rect_hud_color = '#0000FF10'
+            self.text_hud_color = '#000000'
+        else:
+            theme_color = '#000000'
+            tick_color = '#FFFFFF'
+            self.rect_hud_color = '#80808040'
+            self.text_hud_color = '#FFFFFF'
+
+        # workspace lines; I didn't use the rectangle because I didn't want to add another VisPy Node,
+        # which might decrease performance
+        # self.b_line, self.r_line, self.t_line, self.l_line = None, None, None, None
+        self.workspace_line = None
+
+        self.pagesize_dict = {}
+        self.pagesize_dict.update(
+            {
+                'A0': (841, 1189),
+                'A1': (594, 841),
+                'A2': (420, 594),
+                'A3': (297, 420),
+                'A4': (210, 297),
+                'A5': (148, 210),
+                'A6': (105, 148),
+                'A7': (74, 105),
+                'A8': (52, 74),
+                'A9': (37, 52),
+                'A10': (26, 37),
+
+                'B0': (1000, 1414),
+                'B1': (707, 1000),
+                'B2': (500, 707),
+                'B3': (353, 500),
+                'B4': (250, 353),
+                'B5': (176, 250),
+                'B6': (125, 176),
+                'B7': (88, 125),
+                'B8': (62, 88),
+                'B9': (44, 62),
+                'B10': (31, 44),
+
+                'C0': (917, 1297),
+                'C1': (648, 917),
+                'C2': (458, 648),
+                'C3': (324, 458),
+                'C4': (229, 324),
+                'C5': (162, 229),
+                'C6': (114, 162),
+                'C7': (81, 114),
+                'C8': (57, 81),
+                'C9': (40, 57),
+                'C10': (28, 40),
+
+                # American paper sizes
+                'LETTER': (8.5*25.4, 11*25.4),
+                'LEGAL': (8.5*25.4, 14*25.4),
+                'ELEVENSEVENTEEN': (11*25.4, 17*25.4),
+
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5*25.4, 8*25.4),
+                'HALF_LETTER': (5.5*25.4, 8*25.4),
+                'GOV_LETTER': (8*25.4, 10.5*25.4),
+                'GOV_LEGAL': (8.5*25.4, 13*25.4),
+                'LEDGER': (17*25.4, 11*25.4),
+            }
+        )
+
+        # Options
+        self.x_margin = 15  # pixels
+        self.y_margin = 25  # Pixels
+
+        # Parent container
+        self.container = container
+
+        # Plots go onto a single matplotlib.figure
+        self.figure = Figure(dpi=50)
+        self.figure.patch.set_visible(True)
+        self.figure.set_facecolor(theme_color)
+
+        # These axes show the ticks and grid. No plotting done here.
+        # New axes must have a label, otherwise mpl returns an existing one.
+        self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
+        self.axes.set_aspect(1)
+        self.axes.grid(True, color='gray')
+        self.h_line = self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
+        self.v_line = self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
+
+        self.axes.tick_params(axis='x', color=tick_color, labelcolor=tick_color)
+        self.axes.tick_params(axis='y', color=tick_color, labelcolor=tick_color)
+        self.axes.spines['bottom'].set_color(tick_color)
+        self.axes.spines['top'].set_color(tick_color)
+        self.axes.spines['right'].set_color(tick_color)
+        self.axes.spines['left'].set_color(tick_color)
+
+        self.axes.set_facecolor(theme_color)
+
+        self.ch_line = None
+        self.cv_line = None
+
+        # The canvas is the top level container (FigureCanvasQTAgg)
+        self.canvas = FigureCanvas(self.figure)
+
+        self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
+        self.canvas.setFocus()
+        self.native = self.canvas
+
+        self.adjust_axes(-10, -10, 100, 100)
+        # self.canvas.set_can_focus(True)  # For key press
+
+        # Attach to parent
+        # self.container.attach(self.canvas, 0, 0, 600, 400)
+        self.container.addWidget(self.canvas)  # Qt
+
+        # Copy a bitmap of the canvas for quick animation.
+        # Update every time the canvas is re-drawn.
+        self.background = self.canvas.copy_from_bbox(self.axes.bbox)
+
+        # ################### NOT IMPLEMENTED YET - EXPERIMENTAL #######################
+        # ## Bitmap Cache
+        # self.cache = CanvasCache(self, self.app)
+        # self.cache_thread = QtCore.QThread()
+        # self.cache.moveToThread(self.cache_thread)
+        # # super(PlotCanvas, self).connect(self.cache_thread, QtCore.SIGNAL("started()"), self.cache.run)
+        # self.cache_thread.started.connect(self.cache.run)
+        #
+        # self.cache_thread.start()
+        # self.cache.new_screen.connect(self.on_new_screen)
+        # ##############################################################################
+
+        # Events
+        self.mp = self.graph_event_connect('button_press_event', self.on_mouse_press)
+        self.mr = self.graph_event_connect('button_release_event', self.on_mouse_release)
+        self.mm = self.graph_event_connect('motion_notify_event', self.on_mouse_move)
+        # self.canvas.connect('configure-event', self.auto_adjust_axes)
+        self.aaa = self.graph_event_connect('resize_event', self.auto_adjust_axes)
+        # self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
+        # self.canvas.connect("scroll-event", self.on_scroll)
+        self.osc = self.graph_event_connect('scroll_event', self.on_scroll)
+        # self.graph_event_connect('key_press_event', self.on_key_down)
+        # self.graph_event_connect('key_release_event', self.on_key_up)
+        self.odr = self.graph_event_connect('draw_event', self.on_draw)
+
+        self.key = None
+
+        self.pan_axes = []
+        self.panning = False
+        self.mouse = [0, 0]
+        self.big_cursor = False
+        self.big_cursor_isdisabled = None
+
+        # signal is the mouse is dragging
+        self.is_dragging = False
+
+        self.mouse_press_pos = None
+
+        # signal if there is a doubleclick
+        self.is_dblclk = False
+
+        # HUD Display
+        self.hud_enabled = False
+        self.text_hud = self.Thud(plotcanvas=self)
+
+        if self.app.defaults['global_hud'] is True:
+            self.on_toggle_hud(state=True, silent=None)
+
+        # enable Grid lines
+        self.grid_lines_enabled = True
+
+        # draw a rectangle made out of 4 lines on the canvas to serve as a hint for the work area
+        # all CNC have a limited workspace
+        if self.app.defaults['global_workspace'] is True:
+            self.draw_workspace(workspace_size=self.app.defaults["global_workspaceT"])
+
+        # Axis Display
+        self.axis_enabled = True
+
+        # enable Axis
+        self.on_toggle_axis(state=True, silent=True)
+        self.app.ui.axis_status_label.setStyleSheet("""
+                                                    QLabel
+                                                    {
+                                                        color: black;
+                                                        background-color: orange;
+                                                    }
+                                                    """)
+
+    def on_toggle_axis(self, signal=None, state=None, silent=None):
+        if not state:
+            state = not self.axis_enabled
+
+        if state:
+            self.axis_enabled = True
+            self.app.defaults['global_axis'] = True
+            if self.h_line not in self.axes.lines and self.v_line not in self.axes.lines:
+                self.h_line = self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
+                self.v_line = self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
+                self.app.ui.axis_status_label.setStyleSheet("""
+                                                            QLabel
+                                                            {
+                                                                color: black;
+                                                                background-color: orange;
+                                                            }
+                                                            """)
+                if silent is None:
+                    self.app.inform[str, bool].emit(_("Axis enabled."), False)
+        else:
+            self.axis_enabled = False
+            self.app.defaults['global_axis'] = False
+            if self.h_line in self.axes.lines and self.v_line in self.axes.lines:
+                self.axes.lines.remove(self.h_line)
+                self.axes.lines.remove(self.v_line)
+                self.app.ui.axis_status_label.setStyleSheet("")
+                if silent is None:
+                    self.app.inform[str, bool].emit(_("Axis disabled."), False)
+
+        self.canvas.draw()
+
+    def on_toggle_hud(self, signal=None, state=None, silent=None):
+        if state is None:
+            state = not self.hud_enabled
+
+        if state:
+            self.hud_enabled = True
+            self.text_hud.add_artist()
+            self.app.defaults['global_hud'] = True
+
+            self.app.ui.hud_label.setStyleSheet("""
+                                                QLabel
+                                                {
+                                                    color: black;
+                                                    background-color: mediumpurple;
+                                                }
+                                                """)
+            if silent is None:
+                self.app.inform[str, bool].emit(_("HUD enabled."), False)
+        else:
+            self.hud_enabled = False
+            self.text_hud.remove_artist()
+            self.app.defaults['global_hud'] = False
+            self.app.ui.hud_label.setStyleSheet("")
+            if silent is None:
+                self.app.inform[str, bool].emit(_("HUD disabled."), False)
+
+        self.canvas.draw()
+
+    class Thud(QtCore.QObject):
+        text_changed = QtCore.pyqtSignal(str)
+
+        def __init__(self, plotcanvas):
+            super().__init__()
+
+            self.p = plotcanvas
+            units = self.p.app.defaults['units']
+            self._text = 'Dx:    %s [%s]\nDy:    %s [%s]\n\nX:      %s [%s]\nY:      %s [%s]' % \
+                         ('0.0000', units, '0.0000', units, '0.0000', units, '0.0000', units)
+
+            # set font size
+            qsettings = QtCore.QSettings("Open Source", "FlatCAM")
+            if qsettings.contains("hud_font_size"):
+                # I multiply with 2.5 because this seems to be the difference between the value taken by the VisPy (3D)
+                # and Matplotlib (Legacy2D FlatCAM graphic engine)
+                fsize = int(qsettings.value('hud_font_size', type=int) * 2.5)
+            else:
+                fsize = 20
+
+            self.hud_holder = AnchoredText(self._text, prop=dict(size=fsize), frameon=True, loc='upper left')
+            self.hud_holder.patch.set_boxstyle("round,pad=0.,rounding_size=0.2")
+
+            fc_color = self.p.rect_hud_color[:-2]
+            fc_alpha = int(self.p.rect_hud_color[-2:], 16) / 255
+            text_color = self.p.text_hud_color
+
+            self.hud_holder.patch.set_facecolor(fc_color)
+            self.hud_holder.patch.set_alpha(fc_alpha)
+            self.hud_holder.patch.set_edgecolor((0, 0, 0, 0))
+
+            self. hud_holder.txt._text.set_color(color=text_color)
+            self.text_changed.connect(self.on_text_changed)
+
+        @property
+        def text(self):
+            return self._text
+
+        @text.setter
+        def text(self, val):
+            self.text_changed.emit(val)
+            self._text = val
+
+        def on_text_changed(self, txt):
+            try:
+                txt = txt.replace('\t', '    ')
+                self.hud_holder.txt.set_text(txt)
+                self.p.canvas.draw()
+            except Exception:
+                pass
+
+        def add_artist(self):
+            if self.hud_holder not in self.p.axes.artists:
+                self.p.axes.add_artist(self.hud_holder)
+
+        def remove_artist(self):
+            if self.hud_holder in self.p.axes.artists:
+                self.p.axes.artists.remove(self.hud_holder)
+
+    def on_toggle_grid_lines(self, signal=None, silent=None):
+        state = not self.grid_lines_enabled
+
+        if state:
+            self.app.defaults['global_grid_lines'] = True
+            self.grid_lines_enabled = True
+            self.axes.grid(True)
+            try:
+                self.canvas.draw()
+            except IndexError:
+                pass
+            if silent is None:
+                self.app.inform[str, bool].emit(_("Grid enabled."), False)
+        else:
+            self.app.defaults['global_grid_lines'] = False
+            self.grid_lines_enabled = False
+            self.axes.grid(False)
+            try:
+                self.canvas.draw()
+            except IndexError:
+                pass
+            if silent is None:
+                self.app.inform[str, bool].emit(_("Grid disabled."), False)
+
+    def draw_workspace(self, workspace_size):
+        """
+        Draw a rectangular shape on canvas to specify our valid workspace.
+        :param workspace_size: the workspace size; tuple
+        :return:
+        """
+        try:
+            if self.app.defaults['units'].upper() == 'MM':
+                dims = self.pagesize_dict[workspace_size]
+            else:
+                dims = (self.pagesize_dict[workspace_size][0]/25.4, self.pagesize_dict[workspace_size][1]/25.4)
+        except Exception as e:
+            log.debug("PlotCanvasLegacy.draw_workspace() --> %s" % str(e))
+            return
+
+        if self.app.defaults['global_workspace_orientation'] == 'l':
+            dims = (dims[1], dims[0])
+
+        xdata = [0, dims[0], dims[0], 0, 0]
+        ydata = [0, 0, dims[1], dims[1], 0]
+
+        if self.workspace_line not in self.axes.lines:
+            self.workspace_line = Line2D(xdata=xdata, ydata=ydata, linewidth=2, antialiased=True, color='#b34d4d')
+            self.axes.add_line(self.workspace_line)
+            self.canvas.draw()
+
+        self.app.ui.wplace_label.set_value(workspace_size[:3])
+        self.app.ui.wplace_label.setToolTip(workspace_size)
+        self.fcapp.ui.wplace_label.setStyleSheet("""
+                        QLabel
+                        {
+                            color: black;
+                            background-color: olivedrab;
+                        }
+                        """)
+
+    def delete_workspace(self):
+        try:
+            self.axes.lines.remove(self.workspace_line)
+            self.canvas.draw()
+        except Exception:
+            pass
+        self.fcapp.ui.wplace_label.setStyleSheet("")
+
+    def graph_event_connect(self, event_name, callback):
+        """
+        Attach an event handler to the canvas through the Matplotlib interface.
+
+        :param event_name: Name of the event
+        :type event_name: str
+        :param callback: Function to call
+        :type callback: func
+        :return: Connection id
+        :rtype: int
+        """
+        if event_name == 'mouse_move':
+            event_name = 'motion_notify_event'
+        if event_name == 'mouse_press':
+            event_name = 'button_press_event'
+        if event_name == 'mouse_release':
+            event_name = 'button_release_event'
+        if event_name == 'mouse_double_click':
+            return self.double_click.connect(callback)
+
+        if event_name == 'key_press':
+            event_name = 'key_press_event'
+
+        return self.canvas.mpl_connect(event_name, callback)
+
+    def graph_event_disconnect(self, cid):
+        """
+        Disconnect callback with the give id.
+        :param cid: Callback id.
+        :return: None
+        """
+
+        self.canvas.mpl_disconnect(cid)
+
+    def on_new_screen(self):
+        pass
+        # log.debug("Cache updated the screen!")
+
+    def new_cursor(self, axes=None, big=None):
+        # if axes is None:
+        #     c = MplCursor(axes=self.axes, color='black', linewidth=1)
+        # else:
+        #     c = MplCursor(axes=axes, color='black', linewidth=1)
+
+        if self.app.defaults["global_cursor_color_enabled"]:
+            color = self.app.defaults["global_cursor_color"]
+        else:
+            if self.app.defaults['global_theme'] == 'white':
+                color = '#000000'
+            else:
+                color = '#FFFFFF'
+
+        if big is True:
+            self.big_cursor = True
+            self.ch_line = self.axes.axhline(color=color, linewidth=self.app.defaults["global_cursor_width"])
+            self.cv_line = self.axes.axvline(color=color, linewidth=self.app.defaults["global_cursor_width"])
+            self.big_cursor_isdisabled = False
+        else:
+            self.big_cursor = False
+
+        c = FakeCursor()
+        c.mouse_state_updated.connect(self.clear_cursor)
+
+        return c
+
+    def draw_cursor(self, x_pos, y_pos, color=None):
+        """
+        Draw a cursor at the mouse grid snapped position
+
+        :param x_pos: mouse x position
+        :param y_pos: mouse y position
+        :param color: custom color of the mouse
+        :return:
+        """
+
+        # there is no point in drawing mouse cursor when panning as it jumps in a confusing way
+        if self.app.app_cursor.enabled is True and self.panning is False:
+            if color:
+                color = color
+            else:
+                if self.app.defaults['global_theme'] == 'white':
+                    color = '#000000'
+                else:
+                    color = '#FFFFFF'
+
+            if self.big_cursor is False:
+                try:
+                    x, y = self.snap(x_pos, y_pos)
+
+                    # Pointer (snapped)
+                    # The size of the cursor is multiplied by 1.65 because that value made the cursor similar with the
+                    # one in the OpenGL(3D) graphic engine
+                    pointer_size = int(float(self.app.defaults["global_cursor_size"]) * 1.65)
+                    elements = self.axes.plot(x, y, '+', color=color, ms=pointer_size,
+                                              mew=self.app.defaults["global_cursor_width"], animated=True)
+                    for el in elements:
+                        self.axes.draw_artist(el)
+                except Exception as e:
+                    # this happen at app initialization since self.app.geo_editor does not exist yet
+                    # I could reshuffle the object instantiating order but what's the point?
+                    # I could crash something else and that's pythonic, too
+                    log.debug("PlotCanvasLegacy.draw_cursor() big_cursor is False --> %s" % str(e))
+            else:
+                try:
+                    self.ch_line.set_markeredgewidth(self.app.defaults["global_cursor_width"])
+                    self.cv_line.set_markeredgewidth(self.app.defaults["global_cursor_width"])
+                except Exception:
+                    pass
+
+                try:
+                    x, y = self.app.geo_editor.snap(x_pos, y_pos)
+                    self.ch_line.set_ydata(y)
+                    self.cv_line.set_xdata(x)
+                except Exception:
+                    # this happen at app initialization since self.app.geo_editor does not exist yet
+                    # I could reshuffle the object instantiating order but what's the point?
+                    # I could crash something else and that's pythonic, too
+                    pass
+                self.canvas.draw_idle()
+
+            self.canvas.blit(self.axes.bbox)
+
+    def clear_cursor(self, state):
+        if state is True:
+            if self.big_cursor is True and self.big_cursor_isdisabled is True:
+                if self.app.defaults["global_cursor_color_enabled"]:
+                    color = self.app.defaults["global_cursor_color"]
+                else:
+                    if self.app.defaults['global_theme'] == 'white':
+                        color = '#000000'
+                    else:
+                        color = '#FFFFFF'
+
+                self.ch_line = self.axes.axhline(color=color, linewidth=self.app.defaults["global_cursor_width"])
+                self.cv_line = self.axes.axvline(color=color, linewidth=self.app.defaults["global_cursor_width"])
+                self.big_cursor_isdisabled = False
+            if self.app.defaults["global_cursor_color_enabled"] is True:
+                self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1], color=self.app.cursor_color_3D)
+            else:
+                self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
+        else:
+            if self.big_cursor is True:
+                self.big_cursor_isdisabled = True
+                try:
+                    self.ch_line.remove()
+                    self.cv_line.remove()
+                    self.canvas.draw_idle()
+                except Exception as e:
+                    log.debug("PlotCanvasLegacy.clear_cursor() big_cursor is True --> %s" % str(e))
+            self.canvas.restore_region(self.background)
+            self.canvas.blit(self.axes.bbox)
+
+    def on_key_down(self, event):
+        """
+
+        :param event:
+        :return:
+        """
+        log.debug('on_key_down(): ' + str(event.key))
+        self.key = event.key
+
+    def on_key_up(self, event):
+        """
+
+        :param event:
+        :return:
+        """
+        self.key = None
+
+    def connect(self, event_name, callback):
+        """
+        Attach an event handler to the canvas through the native Qt interface.
+
+        :param event_name: Name of the event
+        :type event_name: str
+        :param callback: Function to call
+        :type callback: function
+        :return: Nothing
+        """
+        self.canvas.connect(event_name, callback)
+
+    def clear(self):
+        """
+        Clears axes and figure.
+
+        :return: None
+        """
+
+        # Clear
+        self.axes.cla()
+        try:
+            self.figure.clf()
+        except KeyError:
+            log.warning("KeyError in MPL figure.clf()")
+
+        # Re-build
+        self.figure.add_axes(self.axes)
+        self.axes.set_aspect(1)
+        self.axes.grid(True)
+        self.axes.axhline(color=(0.70, 0.3, 0.3), linewidth=2)
+        self.axes.axvline(color=(0.70, 0.3, 0.3), linewidth=2)
+
+        self.adjust_axes(-10, -10, 100, 100)
+
+        # Re-draw
+        self.canvas.draw_idle()
+
+    def redraw(self):
+        """
+        Created only to serve for compatibility with the VisPy plotcanvas (the other graphic engine, 3D)
+        :return:
+        """
+        self.clear()
+
+    def adjust_axes(self, xmin, ymin, xmax, ymax):
+        """
+        Adjusts all axes while maintaining the use of the whole canvas
+        and an aspect ratio to 1:1 between x and y axes. The parameters are an original
+        request that will be modified to fit these restrictions.
+
+        :param xmin: Requested minimum value for the X axis.
+        :type xmin: float
+        :param ymin: Requested minimum value for the Y axis.
+        :type ymin: float
+        :param xmax: Requested maximum value for the X axis.
+        :type xmax: float
+        :param ymax: Requested maximum value for the Y axis.
+        :type ymax: float
+        :return: None
+        """
+
+        # FlatCAMApp.App.log.debug("PC.adjust_axes()")
+
+        if not self.app.collection.get_list():
+            xmin = -10
+            ymin = -10
+            xmax = 100
+            ymax = 100
+
+        width = xmax - xmin
+        height = ymax - ymin
+        try:
+            r = width / height
+        except ZeroDivisionError:
+            log.error("Height is %f" % height)
+            return
+        canvas_w, canvas_h = self.canvas.get_width_height()
+        canvas_r = float(canvas_w) / canvas_h
+        x_ratio = float(self.x_margin) / canvas_w
+        y_ratio = float(self.y_margin) / canvas_h
+
+        if r > canvas_r:
+            ycenter = (ymin + ymax) / 2.0
+            newheight = height * r / canvas_r
+            ymin = ycenter - newheight / 2.0
+            ymax = ycenter + newheight / 2.0
+        else:
+            xcenter = (xmax + xmin) / 2.0
+            newwidth = width * canvas_r / r
+            xmin = xcenter - newwidth / 2.0
+            xmax = xcenter + newwidth / 2.0
+
+        # Adjust axes
+        for ax in self.figure.get_axes():
+            if ax._label != 'base':
+                ax.set_frame_on(False)  # No frame
+                ax.set_xticks([])  # No tick
+                ax.set_yticks([])  # No ticks
+                ax.patch.set_visible(False)  # No background
+                ax.set_aspect(1)
+            ax.set_xlim((xmin, xmax))
+            ax.set_ylim((ymin, ymax))
+            ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
+
+        # Sync re-draw to proper paint on form resize
+        self.canvas.draw()
+
+        # #### Temporary place-holder for cached update #####
+        self.update_screen_request.emit([0, 0, 0, 0, 0])
+
+    def auto_adjust_axes(self, *args):
+        """
+        Calls ``adjust_axes()`` using the extents of the base axes.
+
+        :rtype : None
+        :return: None
+        """
+
+        xmin, xmax = self.axes.get_xlim()
+        ymin, ymax = self.axes.get_ylim()
+        self.adjust_axes(xmin, ymin, xmax, ymax)
+
+    def fit_view(self):
+        self.auto_adjust_axes()
+
+    def fit_center(self, loc, rect=None):
+        x = loc[0]
+        y = loc[1]
+
+        xmin, xmax = self.axes.get_xlim()
+        ymin, ymax = self.axes.get_ylim()
+        half_width = (xmax - xmin) / 2
+        half_height = (ymax - ymin) / 2
+
+        # Adjust axes
+        for ax in self.figure.get_axes():
+            ax.set_xlim((x - half_width, x + half_width))
+            ax.set_ylim((y - half_height, y + half_height))
+
+        # Re-draw
+        self.canvas.draw()
+
+        # #### Temporary place-holder for cached update #####
+        self.update_screen_request.emit([0, 0, 0, 0, 0])
+
+    def zoom(self, factor, center=None):
+        """
+        Zooms the plot by factor around a given
+        center point. Takes care of re-drawing.
+
+        :param factor: Number by which to scale the plot.
+        :type factor: float
+        :param center: Coordinates [x, y] of the point around which to scale the plot.
+        :type center: list
+        :return: None
+        """
+
+        factor = 1 / factor
+
+        xmin, xmax = self.axes.get_xlim()
+        ymin, ymax = self.axes.get_ylim()
+
+        width = xmax - xmin
+        height = ymax - ymin
+
+        if center is None or center == [None, None]:
+            center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
+
+        # For keeping the point at the pointer location
+        relx = (xmax - center[0]) / width
+        rely = (ymax - center[1]) / height
+
+        new_width = width / factor
+        new_height = height / factor
+
+        xmin = center[0] - new_width * (1 - relx)
+        xmax = center[0] + new_width * relx
+        ymin = center[1] - new_height * (1 - rely)
+        ymax = center[1] + new_height * rely
+
+        # Adjust axes
+        for ax in self.figure.get_axes():
+            ax.set_xlim((xmin, xmax))
+            ax.set_ylim((ymin, ymax))
+        # Async re-draw
+        self.canvas.draw_idle()
+
+        # #### Temporary place-holder for cached update #####
+        self.update_screen_request.emit([0, 0, 0, 0, 0])
+
+    def pan(self, x, y, idle=True):
+        xmin, xmax = self.axes.get_xlim()
+        ymin, ymax = self.axes.get_ylim()
+        width = xmax - xmin
+        height = ymax - ymin
+
+        # Adjust axes
+        for ax in self.figure.get_axes():
+            ax.set_xlim((xmin + x * width, xmax + x * width))
+            ax.set_ylim((ymin + y * height, ymax + y * height))
+
+        # Re-draw
+        if idle:
+            self.canvas.draw_idle()
+        else:
+            self.canvas.draw()
+
+        # #### Temporary place-holder for cached update #####
+        self.update_screen_request.emit([0, 0, 0, 0, 0])
+
+    def new_axes(self, name):
+        """
+        Creates and returns an Axes object attached to this object's Figure.
+
+        :param name: Unique label for the axes.
+        :return: Axes attached to the figure.
+        :rtype: Axes
+        """
+        new_ax = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
+        return new_ax
+
+    def remove_current_axes(self):
+        """
+
+        :return: The name of the deleted axes
+        """
+
+        axes_to_remove = self.figure.axes.gca()
+        current_axes_name = deepcopy(axes_to_remove._label)
+        self.figure.axes.remove(axes_to_remove)
+
+        return current_axes_name
+
+    def on_scroll(self, event):
+        """
+        Scroll event handler.
+
+        :param event: Event object containing the event information.
+        :return: None
+        """
+
+        # So it can receive key presses
+        # self.canvas.grab_focus()
+        self.canvas.setFocus()
+
+        # Event info
+        # z, direction = event.get_scroll_direction()
+
+        if self.key is None:
+
+            if event.button == 'up':
+                self.zoom(1 / 1.5, self.mouse)
+            else:
+                self.zoom(1.5, self.mouse)
+            return
+
+        if self.key == 'shift':
+
+            if event.button == 'up':
+                self.pan(0.3, 0)
+            else:
+                self.pan(-0.3, 0)
+            return
+
+        if self.key == 'control':
+
+            if event.button == 'up':
+                self.pan(0, 0.3)
+            else:
+                self.pan(0, -0.3)
+            return
+
+    def on_mouse_press(self, event):
+
+        self.is_dragging = True
+        self.mouse_press_pos = (event.x, event.y)
+
+        # Check for middle mouse button press
+        if self.app.defaults["global_pan_button"] == '2':
+            pan_button = 3  # right button for Matplotlib
+        else:
+            pan_button = 2  # middle button for Matplotlib
+
+        if event.button == pan_button:
+            # Prepare axes for pan (using 'matplotlib' pan function)
+            self.pan_axes = []
+            for a in self.figure.get_axes():
+                if (event.x is not None and event.y is not None and a.in_axes(event) and
+                        a.get_navigate() and a.can_pan()):
+                    a.start_pan(event.x, event.y, 1)
+                    self.pan_axes.append(a)
+
+            # Set pan view flag
+            if len(self.pan_axes) > 0:
+                self.panning = True
+
+        if event.dblclick:
+            self.double_click.emit(event)
+
+    def on_mouse_release(self, event):
+
+        mouse_release_pos = (event.x, event.y)
+        delta = 0.05
+
+        if abs(self.distance(self.mouse_press_pos, mouse_release_pos)) < delta:
+            self.is_dragging = False
+
+        # Check for middle mouse button release to complete pan procedure
+        # Check for middle mouse button press
+        if self.app.defaults["global_pan_button"] == '2':
+            pan_button = 3  # right button for Matplotlib
+        else:
+            pan_button = 2  # middle button for Matplotlib
+
+        if event.button == pan_button:
+            for a in self.pan_axes:
+                a.end_pan()
+
+            # Clear pan flag
+            self.panning = False
+
+            # And update the cursor
+            if self.app.defaults["global_cursor_color_enabled"] is True:
+                self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1], color=self.app.cursor_color_3D)
+            else:
+                self.draw_cursor(x_pos=self.mouse[0], y_pos=self.mouse[1])
+
+    def on_mouse_move(self, event):
+        """
+        Mouse movement event handler. Stores the coordinates. Updates view on pan.
+
+        :param event: Contains information about the event.
+        :return: None
+        """
+
+        try:
+            x = float(event.xdata)
+            y = float(event.ydata)
+        except TypeError:
+            return
+
+        self.mouse = [event.xdata, event.ydata]
+
+        self.canvas.restore_region(self.background)
+
+        # Update pan view on mouse move
+        if self.panning is True:
+            for a in self.pan_axes:
+                a.drag_pan(1, event.key, event.x, event.y)
+
+            # x_pan, y_pan = self.app.geo_editor.snap(event.xdata, event.ydata)
+            # self.draw_cursor(x_pos=x_pan, y_pos=y_pan)
+
+            # Async re-draw (redraws only on thread idle state, uses timer on backend)
+            self.canvas.draw_idle()
+
+            # #### Temporary place-holder for cached update #####
+            # self.update_screen_request.emit([0, 0, 0, 0, 0])
+
+        if self.app.defaults["global_cursor_color_enabled"] is True:
+            self.draw_cursor(x_pos=x, y_pos=y, color=self.app.cursor_color_3D)
+        else:
+            self.draw_cursor(x_pos=x, y_pos=y)
+        # self.canvas.blit(self.axes.bbox)
+
+    def translate_coords(self, position):
+        """
+        This does not do much. It's just for code compatibility
+
+        :param position: Mouse event position
+        :return: Tuple with mouse position
+        """
+        return position[0], position[1]
+
+    def on_draw(self, renderer):
+
+        # Store background on canvas redraw
+        self.background = self.canvas.copy_from_bbox(self.axes.bbox)
+
+    def get_axes_pixelsize(self):
+        """
+        Axes size in pixels.
+
+        :return: Pixel width and height
+        :rtype: tuple
+        """
+        bbox = self.axes.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
+        width, height = bbox.width, bbox.height
+        width *= self.figure.dpi
+        height *= self.figure.dpi
+        return width, height
+
+    def get_density(self):
+        """
+        Returns unit length per pixel on horizontal
+        and vertical axes.
+
+        :return: X and Y density
+        :rtype: tuple
+        """
+        xpx, ypx = self.get_axes_pixelsize()
+
+        xmin, xmax = self.axes.get_xlim()
+        ymin, ymax = self.axes.get_ylim()
+        width = xmax - xmin
+        height = ymax - ymin
+
+        return width / xpx, height / ypx
+
+    def snap(self, x, y):
+        """
+        Adjusts coordinates to snap settings.
+
+        :param x: Input coordinate X
+        :param y: Input coordinate Y
+        :return: Snapped (x, y)
+        """
+
+        snap_x, snap_y = (x, y)
+        snap_distance = np.Inf
+
+        # ### Grid snap
+        if self.app.grid_status():
+            if self.app.defaults["global_gridx"] != 0:
+                try:
+                    snap_x_ = round(x / float(self.app.defaults["global_gridx"])) * \
+                              float(self.app.defaults["global_gridx"])
+                except TypeError:
+                    snap_x_ = x
+            else:
+                snap_x_ = x
+
+            # If the Grid_gap_linked on Grid Toolbar is checked then the snap distance on GridY entry will be ignored
+            # and it will use the snap distance from GridX entry
+            if self.app.ui.grid_gap_link_cb.isChecked():
+                if self.app.defaults["global_gridx"] != 0:
+                    try:
+                        snap_y_ = round(y / float(self.app.defaults["global_gridx"])) * \
+                                  float(self.app.defaults["global_gridx"])
+                    except TypeError:
+                        snap_y_ = y
+                else:
+                    snap_y_ = y
+            else:
+                if self.app.defaults["global_gridy"] != 0:
+                    try:
+                        snap_y_ = round(y / float(self.app.defaults["global_gridy"])) * \
+                                  float(self.app.defaults["global_gridy"])
+                    except TypeError:
+                        snap_y_ = y
+                else:
+                    snap_y_ = y
+            nearest_grid_distance = self.distance((x, y), (snap_x_, snap_y_))
+            if nearest_grid_distance < snap_distance:
+                snap_x, snap_y = (snap_x_, snap_y_)
+
+        return snap_x, snap_y
+
+    @staticmethod
+    def distance(pt1, pt2):
+        return np.sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
+
+
+class FakeCursor(QtCore.QObject):
+    """
+    This is a fake cursor to ensure compatibility with the OpenGL engine (VisPy).
+    This way I don't have to chane (disable) things related to the cursor all over when
+    using the low performance Matplotlib 2D graphic engine.
+    """
+
+    mouse_state_updated = pyqtSignal(bool)
+
+    def __init__(self):
+        super().__init__()
+        self._enabled = True
+
+    @property
+    def enabled(self):
+        return True if self._enabled else False
+
+    @enabled.setter
+    def enabled(self, value):
+        self._enabled = value
+        self.mouse_state_updated.emit(value)
+
+    def set_data(self, pos, **kwargs):
+        """Internal event handler to draw the cursor when the mouse moves."""
+        return
+
+
+class ShapeCollectionLegacy:
+    """
+    This will create the axes for each collection of shapes and will also
+    hold the collection of shapes into a dict self._shapes.
+    This handles the shapes redraw on canvas.
+    """
+    def __init__(self, obj, app, name=None, annotation_job=None, linewidth=1):
+        """
+
+        :param obj:             This is the object to which the shapes collection is attached and for
+                                which it will have to draw shapes
+        :param app:             This is the FLatCAM.App usually, needed because we have to access attributes there
+        :param name:            This is the name given to the Matplotlib axes; it needs to be unique due of
+                                Matplotlib requurements
+        :param annotation_job:  Make this True if the job needed is just for annotation
+        :param linewidth:       THe width of the line (outline where is the case)
+        """
+        self.obj = obj
+        self.app = app
+        self.annotation_job = annotation_job
+
+        self._shapes = {}
+        self.shape_dict = {}
+        self.shape_id = 0
+
+        self._color = None
+        self._face_color = None
+        self._visible = True
+        self._update = False
+        self._alpha = None
+        self._tool_tolerance = None
+        self._tooldia = None
+
+        self._obj = None
+        self._gcode_parsed = None
+
+        self._linewidth = linewidth
+
+        if name is None:
+            axes_name = self.obj.options['name']
+        else:
+            axes_name = name
+
+        # Axes must exist and be attached to canvas.
+        if axes_name not in self.app.plotcanvas.figure.axes:
+            self.axes = self.app.plotcanvas.new_axes(axes_name)
+
+    def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
+            update=False, layer=1, tolerance=0.01, obj=None, gcode_parsed=None, tool_tolerance=None, tooldia=None,
+            linewidth=None):
+        """
+        This function will add shapes to the shape collection
+
+        :param shape: the Shapely shape to be added to the shape collection
+        :param color: edge color of the shape, hex value
+        :param face_color: the body color of the shape, hex value
+        :param alpha: level of transparency of the shape [0.0 ... 1.0]; Float
+        :param visible: if True will allow the shapes to be added
+        :param update: not used; just for compatibility with VIsPy canvas
+        :param layer: just for compatibility with VIsPy canvas
+        :param tolerance: just for compatibility with VIsPy canvas
+        :param obj: not used
+        :param gcode_parsed: not used; just for compatibility with VIsPy canvas
+        :param tool_tolerance: just for compatibility with VIsPy canvas
+        :param tooldia:
+        :param linewidth: the width of the line
+        :return:
+        """
+        self._color = color if color is not None else "#006E20"
+        # self._face_color = face_color if face_color is not None else "#BBF268"
+        self._face_color = face_color
+
+        if linewidth is None:
+            line_width = self._linewidth
+        else:
+            line_width = linewidth
+
+        if len(self._color) > 7:
+            self._color = self._color[:7]
+
+        if self._face_color is not None:
+            if len(self._face_color) > 7:
+                self._face_color = self._face_color[:7]
+                # self._alpha = int(self._face_color[-2:], 16) / 255
+
+        self._alpha = 0.75
+
+        if alpha is not None:
+            self._alpha = alpha
+
+        self._visible = visible
+        self._update = update
+
+        # CNCJob object related arguments
+        self._obj = obj
+        self._gcode_parsed = gcode_parsed
+        self._tool_tolerance = tool_tolerance
+        self._tooldia = tooldia
+
+        # if self._update:
+        #     self.clear()
+
+        try:
+            for sh in shape:
+                self.shape_id += 1
+                self.shape_dict.update({
+                    'color': self._color,
+                    'face_color': self._face_color,
+                    'linewidth': line_width,
+                    'alpha': self._alpha,
+                    'visible': self._visible,
+                    'shape': sh
+                })
+
+                self._shapes.update({
+                    self.shape_id: deepcopy(self.shape_dict)
+                })
+        except TypeError:
+            self.shape_id += 1
+            self.shape_dict.update({
+                'color': self._color,
+                'face_color': self._face_color,
+                'linewidth': line_width,
+                'alpha': self._alpha,
+                'visible': self._visible,
+                'shape': shape
+            })
+
+            self._shapes.update({
+                self.shape_id: deepcopy(self.shape_dict)
+            })
+
+        return self.shape_id
+
+    def remove(self, shape_id, update=None):
+        for k in list(self._shapes.keys()):
+            if shape_id == k:
+                self._shapes.pop(k, None)
+
+        if update is True:
+            self.redraw()
+
+    def clear(self, update=None):
+        """
+        Clear the canvas of the shapes.
+
+        :param update:
+        :return: None
+        """
+        self._shapes.clear()
+        self.shape_id = 0
+
+        self.axes.cla()
+        try:
+            self.app.plotcanvas.auto_adjust_axes()
+        except Exception as e:
+            log.debug("ShapeCollectionLegacy.clear() --> %s" % str(e))
+
+        if update is True:
+            self.redraw()
+
+    def redraw(self, update_colors=None):
+        """
+        This draw the shapes in the shapes collection, on canvas
+
+        :return: None
+        """
+
+        path_num = 0
+        local_shapes = deepcopy(self._shapes)
+
+        try:
+            obj_type = self.obj.kind
+        except AttributeError:
+            obj_type = 'utility'
+
+        # if we don't use this then when adding each new shape, the old ones will be added again, too
+        # if obj_type == 'utility':
+        #     self.axes.patches.clear()
+        self.axes.patches.clear()
+
+        for element in local_shapes:
+            if local_shapes[element]['visible'] is True:
+                if obj_type == 'excellon':
+                    # Plot excellon (All polygons?)
+                    if self.obj.options["solid"] and isinstance(local_shapes[element]['shape'], Polygon):
+                        try:
+                            patch = PolygonPatch(local_shapes[element]['shape'],
+                                                 facecolor=local_shapes[element]['face_color'],
+                                                 edgecolor=local_shapes[element]['color'],
+                                                 alpha=local_shapes[element]['alpha'],
+                                                 zorder=3,
+                                                 linewidth=local_shapes[element]['linewidth']
+                                                 )
+                            self.axes.add_patch(patch)
+                        except Exception as e:
+                            log.debug("ShapeCollectionLegacy.redraw() excellon poly --> %s" % str(e))
+                    else:
+                        try:
+                            if isinstance(local_shapes[element]['shape'], Polygon):
+                                x, y = local_shapes[element]['shape'].exterior.coords.xy
+                                self.axes.plot(x, y, 'r-', linewidth=local_shapes[element]['linewidth'])
+                                for ints in local_shapes[element]['shape'].interiors:
+                                    x, y = ints.coords.xy
+                                    self.axes.plot(x, y, 'o-', linewidth=local_shapes[element]['linewidth'])
+                            elif isinstance(local_shapes[element]['shape'], LinearRing):
+                                x, y = local_shapes[element]['shape'].coords.xy
+                                self.axes.plot(x, y, 'r-', linewidth=local_shapes[element]['linewidth'])
+                        except Exception as e:
+                            log.debug("ShapeCollectionLegacy.redraw() excellon no poly --> %s" % str(e))
+                elif obj_type == 'geometry':
+                    if type(local_shapes[element]['shape']) == Polygon:
+                        try:
+                            x, y = local_shapes[element]['shape'].exterior.coords.xy
+                            self.axes.plot(x, y, local_shapes[element]['color'],
+                                           linestyle='-',
+                                           linewidth=local_shapes[element]['linewidth'])
+                            for ints in local_shapes[element]['shape'].interiors:
+                                x, y = ints.coords.xy
+                                self.axes.plot(x, y, local_shapes[element]['color'],
+                                               linestyle='-',
+                                               linewidth=local_shapes[element]['linewidth'])
+                        except Exception as e:
+                            log.debug("ShapeCollectionLegacy.redraw() geometry poly --> %s" % str(e))
+                    elif type(local_shapes[element]['shape']) == LineString or \
+                            type(local_shapes[element]['shape']) == LinearRing:
+
+                        try:
+                            x, y = local_shapes[element]['shape'].coords.xy
+                            self.axes.plot(x, y, local_shapes[element]['color'],
+                                           linestyle='-',
+                                           linewidth=local_shapes[element]['linewidth'])
+                        except Exception as e:
+                            log.debug("ShapeCollectionLegacy.redraw() geometry no poly --> %s" % str(e))
+                elif obj_type == 'gerber':
+                    if self.obj.options["multicolored"]:
+                        linespec = '-'
+                    else:
+                        linespec = 'k-'
+
+                    if self.obj.options["solid"]:
+                        if update_colors:
+                            gerber_fill_color = update_colors[0]
+                            gerber_outline_color = update_colors[1]
+                        else:
+                            gerber_fill_color = local_shapes[element]['face_color']
+                            gerber_outline_color = local_shapes[element]['color']
+
+                        try:
+                            patch = PolygonPatch(local_shapes[element]['shape'],
+                                                 facecolor=gerber_fill_color,
+                                                 edgecolor=gerber_outline_color,
+                                                 alpha=local_shapes[element]['alpha'],
+                                                 zorder=2,
+                                                 linewidth=local_shapes[element]['linewidth'])
+                            self.axes.add_patch(patch)
+                        except AssertionError:
+                            log.warning("A geometry component was not a polygon:")
+                            log.warning(str(element))
+                        except Exception as e:
+                            log.debug(
+                                "PlotCanvasLegacy.ShepeCollectionLegacy.redraw() gerber 'solid' --> %s" % str(e))
+                    else:
+                        try:
+                            x, y = local_shapes[element]['shape'].exterior.xy
+                            self.axes.plot(x, y, linespec, linewidth=local_shapes[element]['linewidth'])
+                            for ints in local_shapes[element]['shape'].interiors:
+                                x, y = ints.coords.xy
+                                self.axes.plot(x, y, linespec, linewidth=local_shapes[element]['linewidth'])
+                        except Exception as e:
+                            log.debug("ShapeCollectionLegacy.redraw() gerber no 'solid' --> %s" % str(e))
+                elif obj_type == 'cncjob':
+
+                    if local_shapes[element]['face_color'] is None:
+                        try:
+                            linespec = '--'
+                            linecolor = local_shapes[element]['color']
+                            # if geo['kind'][0] == 'C':
+                            #     linespec = 'k-'
+                            x, y = local_shapes[element]['shape'].coords.xy
+                            self.axes.plot(x, y, linespec, color=linecolor,
+                                           linewidth=local_shapes[element]['linewidth'])
+                        except Exception as e:
+                            log.debug("ShapeCollectionLegacy.redraw() cncjob with face_color --> %s" % str(e))
+                    else:
+                        try:
+                            path_num += 1
+                            if self.obj.ui.annotation_cb.get_value():
+                                if isinstance(local_shapes[element]['shape'], Polygon):
+                                    self.axes.annotate(
+                                        str(path_num),
+                                        xy=local_shapes[element]['shape'].exterior.coords[0],
+                                        xycoords='data', fontsize=20)
+                                else:
+                                    self.axes.annotate(
+                                        str(path_num),
+                                        xy=local_shapes[element]['shape'].coords[0],
+                                        xycoords='data', fontsize=20)
+
+                            patch = PolygonPatch(local_shapes[element]['shape'],
+                                                 facecolor=local_shapes[element]['face_color'],
+                                                 edgecolor=local_shapes[element]['color'],
+                                                 alpha=local_shapes[element]['alpha'], zorder=2,
+                                                 linewidth=local_shapes[element]['linewidth'])
+                            self.axes.add_patch(patch)
+                        except Exception as e:
+                            log.debug("ShapeCollectionLegacy.redraw() cncjob no face_color --> %s" % str(e))
+                elif obj_type == 'utility':
+                    # not a FlatCAM object, must be utility
+                    if local_shapes[element]['face_color']:
+                        try:
+                            patch = PolygonPatch(local_shapes[element]['shape'],
+                                                 facecolor=local_shapes[element]['face_color'],
+                                                 edgecolor=local_shapes[element]['color'],
+                                                 alpha=local_shapes[element]['alpha'],
+                                                 zorder=2,
+                                                 linewidth=local_shapes[element]['linewidth'])
+
+                            self.axes.add_patch(patch)
+                        except Exception as e:
+                            log.debug("ShapeCollectionLegacy.redraw() utility poly with face_color --> %s" % str(e))
+                    else:
+                        if isinstance(local_shapes[element]['shape'], Polygon):
+                            try:
+                                ext_shape = local_shapes[element]['shape'].exterior
+                                if ext_shape is not None:
+                                    x, y = ext_shape.xy
+                                    self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-',
+                                                   linewidth=local_shapes[element]['linewidth'])
+                                for ints in local_shapes[element]['shape'].interiors:
+                                    if ints is not None:
+                                        x, y = ints.coords.xy
+                                        self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-',
+                                                       linewidth=local_shapes[element]['linewidth'])
+                            except Exception as e:
+                                log.debug("ShapeCollectionLegacy.redraw() utility poly no face_color --> %s" % str(e))
+                        else:
+                            try:
+                                if local_shapes[element]['shape'] is not None:
+                                    x, y = local_shapes[element]['shape'].coords.xy
+                                    self.axes.plot(x, y, local_shapes[element]['color'], linestyle='-',
+                                                   linewidth=local_shapes[element]['linewidth'])
+                            except Exception as e:
+                                log.debug("ShapeCollectionLegacy.redraw() utility lines no face_color --> %s" % str(e))
+        self.app.plotcanvas.auto_adjust_axes()
+
+    def set(self, text, pos, visible=True, font_size=16, color=None):
+        """
+        This will set annotations on the canvas.
+
+        :param text: a list of text elements to be used as annotations
+        :param pos: a list of positions for showing the text elements above
+        :param visible: if True will display annotations, if False will clear them on canvas
+        :param font_size: the font size or the annotations
+        :param color: color of the annotations
+        :return: None
+        """
+        if color is None:
+            color = "#000000FF"
+
+        if visible is not True:
+            self.clear()
+            return
+
+        if len(text) != len(pos):
+            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Could not annotate due of a difference between the number "
+                                                        "of text elements and the number of text positions."))
+            return
+
+        for idx in range(len(text)):
+            try:
+                self.axes.annotate(text[idx], xy=pos[idx], xycoords='data', fontsize=font_size, color=color)
+            except Exception as e:
+                log.debug("ShapeCollectionLegacy.set() --> %s" % str(e))
+
+        self.app.plotcanvas.auto_adjust_axes()
+
+    @property
+    def visible(self):
+        return self._visible
+
+    @visible.setter
+    def visible(self, value):
+        if value is False:
+            self.axes.cla()
+            self.app.plotcanvas.auto_adjust_axes()
+        else:
+            if self._visible is False:
+                self.redraw()
+        self._visible = value
+
+    def update_visibility(self, state, indexes=None):
+        if indexes:
+            for i in indexes:
+                if i in self._shapes:
+                    self._shapes[i]['visible'] = state
+        else:
+            for i in self._shapes:
+                self._shapes[i]['visible'] = state
+
+        self.redraw()
+
+    @property
+    def enabled(self):
+        return self._visible
+
+    @enabled.setter
+    def enabled(self, value):
+        if value is False:
+            self.axes.cla()
+            self.app.plotcanvas.auto_adjust_axes()
+        else:
+            if self._visible is False:
+                self.redraw()
+        self._visible = value
+
+# class MplCursor(Cursor):
+#     """
+#     Unfortunately this gets attached to the current axes and if a new axes is added
+#     it will not be showed until that axes is deleted.
+#     Not the kind of behavior needed here so I don't use it anymore.
+#     """
+#     def __init__(self, axes, color='red', linewidth=1):
+#
+#         super().__init__(ax=axes, useblit=True, color=color, linewidth=linewidth)
+#         self._enabled = True
+#
+#         self.axes = axes
+#         self.color = color
+#         self.linewidth = linewidth
+#
+#         self.x = None
+#         self.y = None
+#
+#     @property
+#     def enabled(self):
+#         return True if self._enabled else False
+#
+#     @enabled.setter
+#     def enabled(self, value):
+#         self._enabled = value
+#         self.visible = self._enabled
+#         self.canvas.draw()
+#
+#     def onmove(self, event):
+#         pass
+#
+#     def set_data(self, event, pos):
+#         """Internal event handler to draw the cursor when the mouse moves."""
+#         self.x = pos[0]
+#         self.y = pos[1]
+#
+#         if self.ignore(event):
+#             return
+#         if not self.canvas.widgetlock.available(self):
+#             return
+#         if event.inaxes != self.ax:
+#             self.linev.set_visible(False)
+#             self.lineh.set_visible(False)
+#
+#             if self.needclear:
+#                 self.canvas.draw()
+#                 self.needclear = False
+#             return
+#         self.needclear = True
+#         if not self.visible:
+#             return
+#         self.linev.set_xdata((self.x, self.x))
+#
+#         self.lineh.set_ydata((self.y, self.y))
+#         self.linev.set_visible(self.visible and self.vertOn)
+#         self.lineh.set_visible(self.visible and self.horizOn)
+#
+#         self._update()

+ 72 - 20
flatcamGUI/VisPyCanvas.py → appGUI/VisPyCanvas.py

@@ -1,30 +1,56 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Dennis Hayrullin                            #
 # Date: 2/5/2016                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
-import numpy as np
 from PyQt5.QtGui import QPalette
+from PyQt5.QtCore import QSettings
+
+import numpy as np
+
 import vispy.scene as scene
 from vispy.scene.cameras.base_camera import BaseCamera
+# from vispy.scene.widgets import Widget as VisPyWidget
 from vispy.color import Color
+
 import time
 
-white = Color("#ffffff" )
+white = Color("#ffffff")
 black = Color("#000000")
 
 
 class VisPyCanvas(scene.SceneCanvas):
 
     def __init__(self, config=None):
-        scene.SceneCanvas.__init__(self, keys=None, config=config)
+        # scene.SceneCanvas.__init__(self, keys=None, config=config)
+        super().__init__(config=config, keys=None)
 
         self.unfreeze()
 
-        back_color = str(QPalette().color(QPalette.Window).name())
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("axis_font_size"):
+            a_fsize = settings.value('axis_font_size', type=int)
+        else:
+            a_fsize = 8
+
+        if settings.contains("theme"):
+            theme = settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            theme_color = Color('#FFFFFF')
+            tick_color = Color('#000000')
+            back_color = str(QPalette().color(QPalette.Window).name())
+        else:
+            theme_color = Color('#000000')
+            tick_color = Color('gray')
+            back_color = Color('#000000')
+            # back_color = Color('#272822') # darker
+            # back_color = Color('#3c3f41') # lighter
 
         self.central_widget.bgcolor = back_color
         self.central_widget.border_color = back_color
@@ -35,20 +61,25 @@ class VisPyCanvas(scene.SceneCanvas):
         top_padding = self.grid_widget.add_widget(row=0, col=0, col_span=2)
         top_padding.height_max = 0
 
-        self.yaxis = scene.AxisWidget(orientation='left', axis_color='black', text_color='black', font_size=8)
+        self.yaxis = scene.AxisWidget(
+            orientation='left', axis_color=tick_color, text_color=tick_color, font_size=a_fsize, axis_width=1
+        )
         self.yaxis.width_max = 55
         self.grid_widget.add_widget(self.yaxis, row=1, col=0)
 
-        self.xaxis = scene.AxisWidget(orientation='bottom', axis_color='black', text_color='black', font_size=8)
-        self.xaxis.height_max = 25
+        self.xaxis = scene.AxisWidget(
+            orientation='bottom', axis_color=tick_color, text_color=tick_color, font_size=a_fsize, axis_width=1,
+            anchors=['center', 'bottom']
+        )
+        self.xaxis.height_max = 30
         self.grid_widget.add_widget(self.xaxis, row=2, col=1)
 
         right_padding = self.grid_widget.add_widget(row=0, col=2, row_span=2)
         # right_padding.width_max = 24
         right_padding.width_max = 0
 
-        view = self.grid_widget.add_view(row=1, col=1, border_color='black', bgcolor='white')
-        view.camera = Camera(aspect=1, rect=(-25,-25,150,150))
+        view = self.grid_widget.add_view(row=1, col=1, border_color=tick_color, bgcolor=theme_color)
+        view.camera = Camera(aspect=1, rect=(-25, -25, 150, 150))
 
         # Following function was removed from 'prepare_draw()' of 'Grid' class by patch,
         # it is necessary to call manually
@@ -57,21 +88,39 @@ class VisPyCanvas(scene.SceneCanvas):
         self.xaxis.link_view(view)
         self.yaxis.link_view(view)
 
-        grid1 = scene.GridLines(parent=view.scene, color='dimgray')
-        grid1.set_gl_state(depth_test=False)
+        # grid1 = scene.GridLines(parent=view.scene, color='dimgray')
+        # grid1.set_gl_state(depth_test=False)
+
+        settings = QSettings("Open Source", "FlatCAM")
+        if settings.contains("theme"):
+            theme = settings.value('theme', type=str)
+        else:
+            theme = 'white'
 
         self.view = view
-        self.grid = grid1
+        if theme == 'white':
+            self.grid = scene.GridLines(parent=self.view.scene, color='dimgray')
+        else:
+            self.grid = scene.GridLines(parent=self.view.scene, color='#dededeff')
+
+        self.grid.set_gl_state(depth_test=False)
 
         self.freeze()
 
         # self.measure_fps()
 
     def translate_coords(self, pos):
+        """
+        Translate pixels to FlatCAM units.
+
+        """
         tr = self.grid.get_transform('canvas', 'visual')
         return tr.map(pos)
 
     def translate_coords_2(self, pos):
+        """
+        Translate FlatCAM units to pixels.
+        """
         tr = self.grid.get_transform('visual', 'document')
         return tr.map(pos)
 
@@ -107,6 +156,9 @@ class Camera(scene.PanZoomCamera):
         if event.handled or not self.interactive:
             return
 
+        # key modifiers
+        modifiers = event.mouse_event.modifiers
+
         # Limit mouse move events
         last_event = event.last_event
         t = time.time()
@@ -121,21 +173,21 @@ class Camera(scene.PanZoomCamera):
             event.handled = True
             return
 
-        # Scrolling
+        # ################### Scrolling ##########################
         BaseCamera.viewbox_mouse_event(self, event)
 
         if event.type == 'mouse_wheel':
-            center = self._scene_transform.imap(event.pos)
-            scale = (1 + self.zoom_factor) ** (-event.delta[1] * 30)
-            self.limited_zoom(scale, center)
+            if not modifiers:
+                center = self._scene_transform.imap(event.pos)
+                scale = (1 + self.zoom_factor) ** (-event.delta[1] * 30)
+                self.limited_zoom(scale, center)
             event.handled = True
 
         elif event.type == 'mouse_move':
             if event.press_event is None:
                 return
 
-            modifiers = event.mouse_event.modifiers
-
+            # ################ Panning ############################
             # self.pan_button_setting is actually self.FlatCAM.APP.defaults['global_pan_button']
             if event.button == int(self.pan_button_setting) and not modifiers:
                 # Translate

BIN
appGUI/VisPyData/data/fonts/opensans-regular.ttf


BIN
appGUI/VisPyData/data/freetype/freetype253.dll


BIN
appGUI/VisPyData/data/freetype/freetype253_x64.dll


+ 12 - 9
flatcamGUI/VisPyPatches.py → appGUI/VisPyPatches.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Dennis Hayrullin                            #
 # Date: 2/5/2016                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from vispy.visuals import markers, LineVisual, InfiniteLineVisual
 from vispy.visuals.axis import Ticker, _get_ticks_talbot
@@ -50,7 +50,7 @@ def apply_patches():
         try:
             self._update_child_widget_dim()
         except Exception as e:
-            print(e)
+            print("VisPyPatches.apply_patches._update_clipper() -> %s" % str(e))
 
     Grid._prepare_draw = _prepare_draw
     Grid._update_clipper = _update_clipper
@@ -72,7 +72,7 @@ def apply_patches():
 
         if GL:
             GL.glDisable(GL.GL_LINE_SMOOTH)
-            GL.glLineWidth(1.0)
+            GL.glLineWidth(2.0)
 
         if self._changed['pos']:
             self.pos_buf.set_data(self._pos)
@@ -88,7 +88,7 @@ def apply_patches():
     def _get_tick_frac_labels(self):
         """Get the major ticks, minor ticks, and major labels"""
         minor_num = 4  # number of minor ticks per major division
-        if (self.axis.scale_type == 'linear'):
+        if self.axis.scale_type == 'linear':
             domain = self.axis.domain
             if domain[1] < domain[0]:
                 flip = True
@@ -117,18 +117,21 @@ def apply_patches():
                 minor.extend(np.linspace(maj + minstep,
                                          maj + majstep - minstep,
                                          minor_num))
+
             major_frac = (major - offset) / scale
-            minor_frac = (np.array(minor) - offset) / scale
             major_frac = major_frac[::-1] if flip else major_frac
             use_mask = (major_frac > -0.0001) & (major_frac < 1.0001)
             major_frac = major_frac[use_mask]
             labels = [l for li, l in enumerate(labels) if use_mask[li]]
-            minor_frac = minor_frac[(minor_frac > -0.0001) &
-                                    (minor_frac < 1.0001)]
+
+            minor_frac = (np.array(minor) - offset) / scale
+            use_minor_mask = (minor_frac > -0.0001) & (minor_frac < 1.0001)
+            minor_frac = minor_frac[use_minor_mask]
+
+            return major_frac, minor_frac, labels
         elif self.axis.scale_type == 'logarithmic':
             return NotImplementedError
         elif self.axis.scale_type == 'power':
             return NotImplementedError
-        return major_frac, minor_frac, labels
 
     Ticker._get_tick_frac_labels = _get_tick_frac_labels

+ 5 - 4
flatcamGUI/VisPyTesselators.py → appGUI/VisPyTesselators.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Dennis Hayrullin                            #
 # Date: 2/5/2016                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from OpenGL import GLU
 
@@ -29,9 +29,10 @@ class GLUTess:
         pass
 
     def _on_combine(self, coords, data, weight):
-        return (coords[0], coords[1], coords[2])
+        return coords[0], coords[1], coords[2]
 
-    def _on_error(self, errno):
+    @staticmethod
+    def _on_error(errno):
         print("GLUTess error:", errno)
 
     def _on_end_primitive(self):

+ 236 - 45
flatcamGUI/VisPyVisuals.py → appGUI/VisPyVisuals.py

@@ -1,10 +1,10 @@
-# ########################################################## ##
+# ##########################################################
 # FlatCAM: 2D Post-processing for Manufacturing            #
 # http://flatcam.org                                       #
 # File Author: Dennis Hayrullin                            #
 # Date: 2/5/2016                                           #
 # MIT Licence                                              #
-# ########################################################## ##
+# ##########################################################
 
 from vispy.visuals import CompoundVisual, LineVisual, MeshVisual, TextVisual, MarkersVisual
 from vispy.scene.visuals import VisualNode, generate_docstring, visuals
@@ -13,14 +13,13 @@ from vispy.color import Color
 from shapely.geometry import Polygon, LineString, LinearRing
 import threading
 import numpy as np
-from flatcamGUI.VisPyTesselators import GLUTess
+from appGUI.VisPyTesselators import GLUTess
 
 
 class FlatCAMLineVisual(LineVisual):
-    def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
-                            method='gl', antialias=False):
-        LineVisual.__init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip',
-                            method='gl', antialias=True)
+    def __init__(self, pos=None, color=(0.5, 0.5, 0.5, 1), width=1, connect='strip', method='gl', antialias=False):
+        LineVisual.__init__(self, pos=pos, color=color, width=width, connect=connect,
+                            method=method, antialias=True)
 
     def clear_data(self):
         self._bounds = None
@@ -46,44 +45,48 @@ def _update_shape_buffers(data, triangulation='glu'):
     geo, color, face_color, tolerance = data['geometry'], data['color'], data['face_color'], data['tolerance']
 
     if geo is not None and not geo.is_empty:
-        simple = geo.simplify(tolerance) if tolerance else geo      # Simplified shape
-        pts = []                                                    # Shape line points
-        tri_pts = []                                                # Mesh vertices
-        tri_tris = []                                               # Mesh faces
+        simplified_geo = geo.simplify(tolerance) if tolerance else geo      # Simplified shape
+        pts = []                                                            # Shape line points
+        tri_pts = []                                                        # Mesh vertices
+        tri_tris = []                                                       # Mesh faces
 
         if type(geo) == LineString:
             # Prepare lines
-            pts = _linestring_to_segments(list(simple.coords))
+            pts = _linestring_to_segments(list(simplified_geo.coords))
 
         elif type(geo) == LinearRing:
             # Prepare lines
-            pts = _linearring_to_segments(list(simple.coords))
+            pts = _linearring_to_segments(list(simplified_geo.coords))
 
         elif type(geo) == Polygon:
             # Prepare polygon faces
             if face_color is not None:
                 if triangulation == 'glu':
                     gt = GLUTess()
-                    tri_tris, tri_pts = gt.triangulate(simple)
+                    tri_tris, tri_pts = gt.triangulate(simplified_geo)
                 else:
                     print("Triangulation type '%s' isn't implemented. Drawing only edges." % triangulation)
 
             # Prepare polygon edges
             if color is not None:
-                pts = _linearring_to_segments(list(simple.exterior.coords))
-                for ints in simple.interiors:
+                pts = _linearring_to_segments(list(simplified_geo.exterior.coords))
+                for ints in simplified_geo.interiors:
                     pts += _linearring_to_segments(list(ints.coords))
 
         # Appending data for mesh
         if len(tri_pts) > 0 and len(tri_tris) > 0:
             mesh_tris += tri_tris
             mesh_vertices += tri_pts
-            mesh_colors += [Color(face_color).rgba] * (len(tri_tris) // 3)
+            face_color_rgba = Color(face_color).rgba
+            # mesh_colors += [face_color_rgba] * (len(tri_tris) // 3)
+            mesh_colors += [face_color_rgba for __ in range(len(tri_tris) // 3)]
 
         # Appending data for line
         if len(pts) > 0:
             line_pts += pts
-            line_colors += [Color(color).rgba] * len(pts)
+            colo_rgba = Color(color).rgba
+            # line_colors += [colo_rgba] * len(pts)
+            line_colors += [colo_rgba for __ in range(len(pts))]
 
     # Store buffers
     data['line_pts'] = line_pts
@@ -142,7 +145,15 @@ class ShapeGroup(object):
         :param kwargs: keyword arguments
             Arguments for ShapeCollection.add function
         """
-        self._indexes.append(self._collection.add(**kwargs))
+        key = self._collection.add(**kwargs)
+        self._indexes.append(key)
+        return key
+
+    def remove(self, idx, update=False):
+        self._indexes.remove(idx)
+        self._collection.remove(idx, False)
+        if update:
+            self._collection.redraw([])             # Skip waiting results
 
     def clear(self, update=False):
         """
@@ -158,11 +169,14 @@ class ShapeGroup(object):
         if update:
             self._collection.redraw([])             # Skip waiting results
 
-    def redraw(self):
+    def redraw(self, update_colors=None):
         """
         Redraws shape collection
         """
-        self._collection.redraw(self._indexes)
+        if update_colors:
+            self._collection.redraw(self._indexes, update_colors=update_colors)
+        else:
+            self._collection.redraw(self._indexes)
 
     @property
     def visible(self):
@@ -184,13 +198,24 @@ class ShapeGroup(object):
 
         self._collection.redraw([])
 
+    def update_visibility(self, state, indexes=None):
+        if indexes:
+            for i in indexes:
+                if i in self._indexes:
+                    self._collection.data[i]['visible'] = state
+        else:
+            for i in self._indexes:
+                self._collection.data[i]['visible'] = state
+
+        self._collection.redraw([])
+
 
 class ShapeCollectionVisual(CompoundVisual):
 
-    def __init__(self, line_width=1, triangulation='gpc', layers=3, pool=None, **kwargs):
+    def __init__(self, linewidth=1, triangulation='vispy', layers=3, pool=None, **kwargs):
         """
         Represents collection of shapes to draw on VisPy scene
-        :param line_width: float
+        :param linewidth: float
             Width of lines/edges
         :param triangulation: str
             Triangulation method used for polygons translation
@@ -217,7 +242,7 @@ class ShapeCollectionVisual(CompoundVisual):
         # self._lines = [LineVisual(antialias=True) for _ in range(0, layers)]
         self._lines = [FlatCAMLineVisual(antialias=True) for _ in range(0, layers)]
 
-        self._line_width = line_width
+        self._line_width = linewidth
         self._triangulation = triangulation
 
         visuals_ = [self._lines[i // 2] if i % 2 else self._meshes[i // 2] for i in range(0, layers * 2)]
@@ -228,14 +253,14 @@ class ShapeCollectionVisual(CompoundVisual):
             pass
             m.set_gl_state(polygon_offset_fill=True, polygon_offset=(1, 1), cull_face=False)
 
-        for l in self._lines:
+        for lne in self._lines:
             pass
-            l.set_gl_state(blend=True)
+            lne.set_gl_state(blend=True)
 
         self.freeze()
 
     def add(self, shape=None, color=None, face_color=None, alpha=None, visible=True,
-            update=False, layer=1, tolerance=0.01):
+            update=False, layer=1, tolerance=0.01, linewidth=None):
         """
         Adds shape to collection
         :return:
@@ -245,6 +270,8 @@ class ShapeCollectionVisual(CompoundVisual):
             Line/edge color
         :param face_color: str, tuple
             Polygon face color
+        :param alpha: str
+            Polygon transparency
         :param visible: bool
             Shape visibility
         :param update: bool
@@ -253,6 +280,8 @@ class ShapeCollectionVisual(CompoundVisual):
             Layer number. 0 - lowest.
         :param tolerance: float
             Geometry simplifying tolerance
+        :param linewidth: int
+            Width of the line
         :return: int
             Index of shape
         """
@@ -266,14 +295,17 @@ class ShapeCollectionVisual(CompoundVisual):
         self.data[key] = {'geometry': shape, 'color': color, 'alpha': alpha, 'face_color': face_color,
                           'visible': visible, 'layer': layer, 'tolerance': tolerance}
 
+        if linewidth:
+            self._line_width = linewidth
+
         # Add data to process pool if pool exists
         try:
             self.results[key] = self.pool.map_async(_update_shape_buffers, [self.data[key]])
-        except Exception as e:
+        except Exception:
             self.data[key] = _update_shape_buffers(self.data[key])
 
         if update:
-            self.redraw()                       # redraw() waits for pool process end
+            self.redraw()   # redraw() waits for pool process end
 
         return key
 
@@ -307,6 +339,147 @@ class ShapeCollectionVisual(CompoundVisual):
         if update:
             self.__update()
 
+    def update_visibility(self, state: bool, indexes=None) -> None:
+        # Lock sub-visuals updates
+        self.update_lock.acquire(True)
+        if indexes is None:
+            for k, data in list(self.data.items()):
+                self.data[k]['visible'] = state
+        else:
+            for k, data in list(self.data.items()):
+                if k in indexes:
+                    self.data[k]['visible'] = state
+
+        self.update_lock.release()
+
+    def update_color(self, new_mesh_color=None, new_line_color=None, indexes=None):
+        if new_mesh_color is None and new_line_color is None:
+            return
+
+        if not self.data:
+            return
+
+        # if a new color is empty string then make it None so it will not be updated
+        # if a new color is valid then transform it here in a format palatable
+        mesh_color_rgba = None
+        line_color_rgba = None
+        if new_mesh_color:
+            if new_mesh_color != '':
+                mesh_color_rgba = Color(new_mesh_color).rgba
+            else:
+                new_mesh_color = None
+        if new_line_color:
+            if new_line_color != '':
+                line_color_rgba = Color(new_line_color).rgba
+            else:
+                new_line_color = None
+
+        mesh_colors = [[] for _ in range(0, len(self._meshes))]     # Face colors
+        line_colors = [[] for _ in range(0, len(self._meshes))]     # Line colors
+        line_pts = [[] for _ in range(0, len(self._lines))]         # Vertices for line
+
+        # Lock sub-visuals updates
+        self.update_lock.acquire(True)
+        # Merge shapes buffers
+
+        if indexes is None:
+            for k, data in list(self.data.items()):
+                if data['visible'] and 'line_pts' in data:
+                    if new_mesh_color and new_mesh_color != '':
+                        dim_mesh_tris = (len(data['mesh_tris']) // 3)
+                        if dim_mesh_tris != 0:
+                            try:
+                                mesh_colors[data['layer']] += [mesh_color_rgba] * dim_mesh_tris
+                                self.data[k]['face_color'] = new_mesh_color
+
+                                data['mesh_colors'] = [mesh_color_rgba for __ in range(len(data['mesh_colors']))]
+                            except Exception as e:
+                                print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                      "Create mesh colors --> Data error. %s" % str(e))
+
+                    if new_line_color and new_line_color != '':
+                        dim_line_pts = (len(data['line_pts']))
+                        if dim_line_pts != 0:
+                            try:
+                                line_pts[data['layer']] += data['line_pts']
+                                line_colors[data['layer']] += [line_color_rgba] * dim_line_pts
+                                self.data[k]['color'] = new_line_color
+
+                                data['line_colors'] = [mesh_color_rgba for __ in range(len(data['line_colors']))]
+                            except Exception as e:
+                                print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                      "Create line colors --> Data error. %s" % str(e))
+        else:
+            for k, data in list(self.data.items()):
+                if data['visible'] and 'line_pts' in data:
+                    dim_mesh_tris = (len(data['mesh_tris']) // 3)
+                    dim_line_pts = (len(data['line_pts']))
+
+                    if k in indexes:
+                        if new_mesh_color and new_mesh_color != '':
+                            if dim_mesh_tris != 0:
+                                try:
+                                    mesh_colors[data['layer']] += [mesh_color_rgba] * dim_mesh_tris
+                                    self.data[k]['face_color'] = new_mesh_color
+
+                                    data['mesh_colors'] = [mesh_color_rgba for __ in range(len(data['mesh_colors']))]
+                                except Exception as e:
+                                    print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                          "Create mesh colors --> Data error. %s" % str(e))
+                        if new_line_color and new_line_color != '':
+                            if dim_line_pts != 0:
+                                try:
+                                    line_pts[data['layer']] += data['line_pts']
+                                    line_colors[data['layer']] += [line_color_rgba] * dim_line_pts
+                                    self.data[k]['color'] = new_line_color
+
+                                    data['line_colors'] = [mesh_color_rgba for __ in range(len(data['line_colors']))]
+                                except Exception as e:
+                                    print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                          "Create line colors --> Data error. %s" % str(e))
+                    else:
+                        if dim_mesh_tris != 0:
+                            try:
+                                mesh_colors[data['layer']] += [Color(data['face_color']).rgba] * dim_mesh_tris
+                            except Exception as e:
+                                print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                      "Create mesh colors --> Data error. %s" % str(e))
+
+                        if dim_line_pts != 0:
+                            try:
+                                line_pts[data['layer']] += data['line_pts']
+                                line_colors[data['layer']] += [Color(data['color']).rgba] * dim_line_pts
+                            except Exception as e:
+                                print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                                      "Create line colors --> Data error. %s" % str(e))
+
+        # Updating meshes
+        if new_mesh_color and new_mesh_color != '':
+            for i, mesh in enumerate(self._meshes):
+                if mesh_colors[i]:
+                    try:
+                        mesh._meshdata.set_face_colors(colors=np.asarray(mesh_colors[i]))
+                        mesh.mesh_data_changed()
+                    except Exception as e:
+                        print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                              "Apply mesh colors --> Data error. %s" % str(e))
+
+        # Updating lines
+        if new_line_color and new_line_color != '':
+            for i, line in enumerate(self._lines):
+                if len(line_pts[i]) > 0:
+                    try:
+                        line._color = np.asarray(line_colors[i])
+                        line._changed['color'] = True
+                        line.update()
+                    except Exception as e:
+                        print("VisPyVisuals.ShapeCollectionVisual.update_color(). "
+                              "Apply line colors --> Data error. %s" % str(e))
+                else:
+                    line.clear_data()
+
+        self.update_lock.release()
+
     def __update(self):
         """
         Merges internal buffers, sets data to visuals, redraws collection on scene
@@ -326,20 +499,23 @@ class ShapeCollectionVisual(CompoundVisual):
                 try:
                     line_pts[data['layer']] += data['line_pts']
                     line_colors[data['layer']] += data['line_colors']
-                    mesh_tris[data['layer']] += [x + len(mesh_vertices[data['layer']])
-                                                 for x in data['mesh_tris']]
 
+                    mesh_tris[data['layer']] += [x + len(mesh_vertices[data['layer']]) for x in data['mesh_tris']]
                     mesh_vertices[data['layer']] += data['mesh_vertices']
                     mesh_colors[data['layer']] += data['mesh_colors']
                 except Exception as e:
-                    print("Data error", e)
+                    print("VisPyVisuals.ShapeCollectionVisual._update() --> Data error. %s" % str(e))
 
         # Updating meshes
         for i, mesh in enumerate(self._meshes):
             if len(mesh_vertices[i]) > 0:
                 set_state(polygon_offset_fill=False)
-                mesh.set_data(np.asarray(mesh_vertices[i]), np.asarray(mesh_tris[i], dtype=np.uint32)
-                              .reshape((-1, 3)), face_colors=np.asarray(mesh_colors[i]))
+                faces_array = np.asarray(mesh_tris[i], dtype=np.uint32)
+                mesh.set_data(
+                    vertices=np.asarray(mesh_vertices[i]),
+                    faces=faces_array.reshape((-1, 3)),
+                    face_colors=np.asarray(mesh_colors[i])
+                )
             else:
                 mesh.set_data()
 
@@ -348,38 +524,53 @@ class ShapeCollectionVisual(CompoundVisual):
         # Updating lines
         for i, line in enumerate(self._lines):
             if len(line_pts[i]) > 0:
-                line.set_data(np.asarray(line_pts[i]), np.asarray(line_colors[i]), self._line_width, 'segments')
+                line.set_data(
+                    pos=np.asarray(line_pts[i]),
+                    color=np.asarray(line_colors[i]),
+                    width=self._line_width,
+                    connect='segments')
             else:
                 line.clear_data()
 
             line._bounds_changed()
 
         self._bounds_changed()
-
         self.update_lock.release()
 
-    def redraw(self, indexes=None):
+    def redraw(self, indexes=None, update_colors=None):
         """
         Redraws collection
-        :param indexes: list
+        :param indexes:     list
             Shape indexes to get from process pool
+        :param update_colors:
         """
         # Only one thread can update data
         self.results_lock.acquire(True)
 
-        for i in list(self.data.copy().keys()) if not indexes else indexes:
-            if i in list(self.results.copy().keys()):
+        for i in list(self.data.keys()) if not indexes else indexes:
+            if i in list(self.results.keys()):
                 try:
                     self.results[i].wait()                                  # Wait for process results
                     if i in self.data:
                         self.data[i] = self.results[i].get()[0]             # Store translated data
                         del self.results[i]
                 except Exception as e:
-                    print(e, indexes)
+                    print("VisPyVisuals.ShapeCollectionVisual.redraw() --> Data error = %s. Indexes = %s" %
+                          (str(e), str(indexes)))
 
         self.results_lock.release()
 
-        self.__update()
+        if update_colors is None or update_colors is False:
+            self.__update()
+        else:
+            try:
+                self.update_color(
+                    new_mesh_color=update_colors[0],
+                    new_line_color=update_colors[1],
+                    indexes=indexes
+                )
+            except Exception as e:
+                print("VisPyVisuals.ShapeCollectionVisual.redraw() --> Update colors error = %s." % str(e))
 
     def lock_updates(self):
         self.update_lock.acquire(True)
@@ -487,7 +678,7 @@ class TextCollectionVisual(TextVisual):
         self.lock.release()
 
         # Prepare data for translation
-        self.data[key] = {'text': text, 'pos': pos, 'visible': visible,'font_size': font_size, 'color': color}
+        self.data[key] = {'text': text, 'pos': pos, 'visible': visible, 'font_size': font_size, 'color': color}
 
         if update:
             self.redraw()
@@ -509,7 +700,7 @@ class TextCollectionVisual(TextVisual):
 
     def clear(self, update=False):
         """
-        Removes all shapes from colleciton
+        Removes all shapes from collection
         :param update: bool
             Set True to redraw collection
         """
@@ -535,7 +726,7 @@ class TextCollectionVisual(TextVisual):
                     font_s = data['font_size']
                     color = data['color']
                 except Exception as e:
-                    print("Data error", e)
+                    print("VisPyVisuals.TextCollectionVisual._update() --> Data error. %s" % str(e))
 
         # Updating text
         if len(labels) > 0:

+ 0 - 0
flatcamGUI/__init__.py → appGUI/__init__.py


+ 327 - 0
appGUI/preferences/OptionUI.py

@@ -0,0 +1,327 @@
+from typing import Union, Sequence, List
+
+from PyQt5 import QtWidgets, QtGui
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCCheckBox, FCButton, FCComboBox, FCEntry, FCSpinner, FCColorEntry, \
+    FCSliderWithSpinner, FCDoubleSpinner, FloatEntry, FCTextArea
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class OptionUI:
+
+    def __init__(self, option: str):
+        self.option = option
+
+    def add_to_grid(self, grid: QtWidgets.QGridLayout, row: int) -> int:
+        """
+        Adds the necessary widget to the grid, starting at the supplied row.
+        Returns the number of rows used (normally 1)
+        """
+        raise NotImplementedError()
+
+    def get_field(self):
+        raise NotImplementedError()
+
+
+class BasicOptionUI(OptionUI):
+    """Abstract OptionUI that has a label on the left then some other widget on the right"""
+    def __init__(self, option: str, label_text: str, label_tooltip: Union[str, None] = None,
+                 label_bold: bool = False, label_color: Union[str, None] = None):
+        super().__init__(option=option)
+        self.label_text = label_text
+        self.label_tooltip = label_tooltip
+        self.label_bold = label_bold
+        self.label_color = label_color
+        self.label_widget = self.build_label_widget()
+        self.entry_widget = self.build_entry_widget()
+
+    def build_label_widget(self) -> QtWidgets.QLabel:
+        fmt = "%s:"
+        if self.label_bold:
+            fmt = "<b>%s</b>" % fmt
+        if self.label_color:
+            fmt = "<span style=\"color:%s;\">%s</span>" % (self.label_color, fmt)
+        label_widget = QtWidgets.QLabel(fmt % _(self.label_text))
+        if self.label_tooltip is not None:
+            label_widget.setToolTip(_(self.label_tooltip))
+        return label_widget
+
+    def build_entry_widget(self) -> QtWidgets.QWidget:
+        raise NotImplementedError()
+
+    def add_to_grid(self, grid: QtWidgets.QGridLayout, row: int) -> int:
+        grid.addWidget(self.label_widget, row, 0)
+        grid.addWidget(self.entry_widget, row, 1)
+        return 1
+
+    def get_field(self):
+        return self.entry_widget
+
+
+class LineEntryOptionUI(BasicOptionUI):
+    def build_entry_widget(self) -> QtWidgets.QWidget:
+        return FCEntry()
+
+
+# Not sure why this is needed over DoubleSpinnerOptionUI
+class FloatEntryOptionUI(BasicOptionUI):
+    def build_entry_widget(self) -> QtWidgets.QWidget:
+        return FloatEntry()
+
+
+class RadioSetOptionUI(BasicOptionUI):
+
+    def __init__(self, option: str, label_text: str, choices: list, orientation='horizontal', **kwargs):
+        self.choices = choices
+        self.orientation = orientation
+        super().__init__(option=option, label_text=label_text, **kwargs)
+
+    def build_entry_widget(self) -> QtWidgets.QWidget:
+        return RadioSet(choices=self.choices, orientation=self.orientation)
+
+
+class TextAreaOptionUI(OptionUI):
+
+    def __init__(self, option: str, label_text: str, label_tooltip: str):
+        super().__init__(option=option)
+        self.label_text = label_text
+        self.label_tooltip = label_tooltip
+        self.label_widget = self.build_label_widget()
+        self.textarea_widget = self.build_textarea_widget()
+
+    def build_label_widget(self):
+        label = QtWidgets.QLabel("%s:" % _(self.label_text))
+        label.setToolTip(_(self.label_tooltip))
+        return label
+
+    def build_textarea_widget(self):
+        textarea = FCTextArea()
+        textarea.setPlaceholderText(_(self.label_tooltip))
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("textbox_font_size"):
+            tb_fsize = qsettings.value('textbox_font_size', type=int)
+        else:
+            tb_fsize = 10
+        font = QtGui.QFont()
+        font.setPointSize(tb_fsize)
+        textarea.setFont(font)
+
+        return textarea
+
+    def get_field(self):
+        return self.textarea_widget
+
+    def add_to_grid(self, grid: QtWidgets.QGridLayout, row: int) -> int:
+        grid.addWidget(self.label_widget, row, 0, 1, 3)
+        grid.addWidget(self.textarea_widget, row+1, 0, 1, 3)
+        return 2
+
+
+class CheckboxOptionUI(OptionUI):
+
+    def __init__(self, option: str, label_text: str, label_tooltip: str):
+        super().__init__(option=option)
+        self.label_text = label_text
+        self.label_tooltip = label_tooltip
+        self.checkbox_widget = self.build_checkbox_widget()
+
+    def build_checkbox_widget(self):
+        checkbox = FCCheckBox('%s' % _(self.label_text))
+        checkbox.setToolTip(_(self.label_tooltip))
+        return checkbox
+
+    def add_to_grid(self, grid: QtWidgets.QGridLayout, row: int) -> int:
+        grid.addWidget(self.checkbox_widget, row, 0, 1, 3)
+        return 1
+
+    def get_field(self):
+        return self.checkbox_widget
+
+
+class ComboboxOptionUI(BasicOptionUI):
+
+    def __init__(self, option: str, label_text: str, choices: Sequence, **kwargs):
+        self.choices = choices
+        super().__init__(option=option, label_text=label_text, **kwargs)
+
+    def build_entry_widget(self):
+        combo = FCComboBox()
+        for choice in self.choices:
+            # don't translate the QCombo items as they are used in QSettings and identified by name
+            combo.addItem(choice)
+        return combo
+
+
+class ColorOptionUI(BasicOptionUI):
+    def build_entry_widget(self) -> QtWidgets.QWidget:
+        entry = FCColorEntry()
+        return entry
+
+
+class SliderWithSpinnerOptionUI(BasicOptionUI):
+    def __init__(self, option: str, label_text: str, min_value=0, max_value=100, step=1, **kwargs):
+        self.min_value = min_value
+        self.max_value = max_value
+        self.step = step
+        super().__init__(option=option, label_text=label_text, **kwargs)
+
+    def build_entry_widget(self) -> QtWidgets.QWidget:
+        entry = FCSliderWithSpinner(min=self.min_value, max=self.max_value, step=self.step)
+        return entry
+
+
+class ColorAlphaSliderOptionUI(SliderWithSpinnerOptionUI):
+    def __init__(self, applies_to: List[str], group, label_text: str, **kwargs):
+        self.applies_to = applies_to
+        self.group = group
+        super().__init__(option="__color_alpha_slider", label_text=label_text, min_value=0, max_value=255, step=1,
+                         **kwargs)
+        self.get_field().valueChanged.connect(self._on_alpha_change)
+
+    def add_to_grid(self, grid: QtWidgets.QGridLayout, row: int) -> int:
+        for index, field in enumerate(self._get_target_fields()):
+            field.entry.textChanged.connect(lambda value, i=index: self._on_target_change(target_index=i))
+        return super().add_to_grid(grid, row)
+
+    def _get_target_fields(self):
+        return list(map(lambda n: self.group.option_dict()[n].get_field(), self.applies_to))
+
+    def _on_target_change(self, target_index: int):
+        field = self._get_target_fields()[target_index]
+        color = field.get_value()
+        alpha_part = color[7:]
+        if len(alpha_part) != 2:
+            return
+        alpha = int(alpha_part, 16)
+        if alpha < 0 or alpha > 255 or self.get_field().get_value() == alpha:
+            return
+        self.get_field().set_value(alpha)
+
+    def _on_alpha_change(self):
+        alpha = self.get_field().get_value()
+        for field in self._get_target_fields():
+            old_value = field.get_value()
+            new_value = self._modify_color_alpha(old_value, alpha=alpha)
+            field.set_value(new_value)
+
+    @staticmethod
+    def _modify_color_alpha(color: str, alpha: int):
+        color_without_alpha = color[:7]
+        if alpha > 255:
+            return color_without_alpha + "FF"
+        elif alpha < 0:
+            return color_without_alpha + "00"
+        else:
+            hexalpha = hex(alpha)[2:]
+            if len(hexalpha) == 1:
+                hexalpha = "0" + hexalpha
+            return color_without_alpha + hexalpha
+
+
+class SpinnerOptionUI(BasicOptionUI):
+    def __init__(self, option: str, label_text: str, min_value: int, max_value: int, step: int = 1, **kwargs):
+        self.min_value = min_value
+        self.max_value = max_value
+        self.step = step
+        super().__init__(option=option, label_text=label_text, **kwargs)
+
+    def build_entry_widget(self) -> QtWidgets.QWidget:
+        entry = FCSpinner()
+        entry.set_range(self.min_value, self.max_value)
+        entry.set_step(self.step)
+        entry.setWrapping(True)
+        return entry
+
+
+class DoubleSpinnerOptionUI(BasicOptionUI):
+    def __init__(self, option: str, label_text: str, step: float, decimals: int, min_value=None, max_value=None,
+                 suffix=None, **kwargs):
+        self.min_value = min_value
+        self.max_value = max_value
+        self.step = step
+        self.suffix = suffix
+        self.decimals = decimals
+        super().__init__(option=option, label_text=label_text, **kwargs)
+
+    def build_entry_widget(self) -> QtWidgets.QWidget:
+        entry = FCDoubleSpinner(suffix=self.suffix)
+        entry.set_precision(self.decimals)
+        entry.setSingleStep(self.step)
+        if self.min_value is None:
+            self.min_value = entry.minimum()
+        else:
+            entry.setMinimum(self.min_value)
+        if self.max_value is None:
+            self.max_value = entry.maximum()
+        else:
+            entry.setMaximum(self.max_value)
+        return entry
+
+
+class HeadingOptionUI(OptionUI):
+    def __init__(self, label_text: str, label_tooltip: Union[str, None] = None):
+        super().__init__(option="__heading")
+        self.label_text = label_text
+        self.label_tooltip = label_tooltip
+
+    def build_heading_widget(self):
+        heading = QtWidgets.QLabel('<b>%s</b>' % _(self.label_text))
+        heading.setToolTip(_(self.label_tooltip))
+        return heading
+
+    def add_to_grid(self, grid: QtWidgets.QGridLayout, row: int) -> int:
+        grid.addWidget(self.build_heading_widget(), row, 0, 1, 2)
+        return 1
+
+    def get_field(self):
+        return None
+
+
+class SeparatorOptionUI(OptionUI):
+
+    def __init__(self):
+        super().__init__(option="__separator")
+
+    @staticmethod
+    def build_separator_widget():
+        separator = QtWidgets.QFrame()
+        separator.setFrameShape(QtWidgets.QFrame.HLine)
+        separator.setFrameShadow(QtWidgets.QFrame.Sunken)
+        return separator
+
+    def add_to_grid(self, grid: QtWidgets.QGridLayout, row: int) -> int:
+        grid.addWidget(self.build_separator_widget(), row, 0, 1, 2)
+        return 1
+
+    def get_field(self):
+        return None
+
+
+class FullWidthButtonOptionUI(OptionUI):
+    def __init__(self, option: str, label_text: str, label_tooltip: Union[str, None]):
+        super().__init__(option=option)
+        self.label_text = label_text
+        self.label_tooltip = label_tooltip
+        self.button_widget = self.build_button_widget()
+
+    def build_button_widget(self):
+        button = FCButton(_(self.label_text))
+        if self.label_tooltip is not None:
+            button.setToolTip(_(self.label_tooltip))
+        return button
+
+    def add_to_grid(self, grid: QtWidgets.QGridLayout, row: int) -> int:
+        grid.addWidget(self.button_widget, row, 0, 1, 3)
+        return 1
+
+    def get_field(self):
+        return self.button_widget

+ 77 - 0
appGUI/preferences/OptionsGroupUI.py

@@ -0,0 +1,77 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File by:  David Robertson (c)                            #
+# Date:     5/2020                                         #
+# License:  MIT Licence                                    #
+# ##########################################################
+
+from typing import Dict
+
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+from appGUI.preferences.OptionUI import OptionUI
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class OptionsGroupUI(QtWidgets.QGroupBox):
+    app = None
+
+    def __init__(self, title, parent=None):
+        # QtGui.QGroupBox.__init__(self, title, parent=parent)
+        super(OptionsGroupUI, self).__init__()
+        self.setStyleSheet("""
+        QGroupBox
+        {
+            font-size: 16px;
+            font-weight: bold;
+        }
+        """)
+
+        self.layout = QtWidgets.QVBoxLayout()
+        self.setLayout(self.layout)
+
+    def option_dict(self) -> Dict[str, OptionUI]:
+        # FIXME!
+        return {}
+
+
+class OptionsGroupUI2(OptionsGroupUI):
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+
+        self.grid = QtWidgets.QGridLayout()
+        self.layout.addLayout(self.grid)
+        self.grid.setColumnStretch(0, 0)
+        self.grid.setColumnStretch(1, 1)
+
+        self.options = self.build_options()
+
+        row = 0
+        for option in self.options:
+            row += option.add_to_grid(grid=self.grid, row=row)
+
+        self.layout.addStretch()
+
+    def build_options(self) -> [OptionUI]:
+        return []
+
+    def option_dict(self) -> Dict[str, OptionUI]:
+        result = {}
+        for optionui in self.options:
+            result[optionui.option] = optionui
+        return result

+ 41 - 0
appGUI/preferences/PreferencesSectionUI.py

@@ -0,0 +1,41 @@
+from typing import Dict
+from PyQt5 import QtWidgets, QtCore
+
+from appGUI.ColumnarFlowLayout import ColumnarFlowLayout
+from appGUI.preferences.OptionUI import OptionUI
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+
+class PreferencesSectionUI(QtWidgets.QWidget):
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+        self.layout = ColumnarFlowLayout()  # QtWidgets.QHBoxLayout()
+        self.setLayout(self.layout)
+
+        self.groups = self.build_groups()
+        for group in self.groups:
+            group.setMinimumWidth(250)
+            self.layout.addWidget(group)
+
+    def build_groups(self) -> [OptionsGroupUI]:
+        return []
+
+    def option_dict(self) -> Dict[str, OptionUI]:
+        result = {}
+        for group in self.groups:
+            groupoptions = group.option_dict()
+            result.update(groupoptions)
+        return result
+
+    def build_tab(self):
+        scroll_area = QtWidgets.QScrollArea()
+        scroll_area.setWidget(self)
+        scroll_area.setWidgetResizable(True)
+        return scroll_area
+
+    def get_tab_id(self) -> str:
+        raise NotImplementedError
+
+    def get_tab_label(self) -> str:
+        raise NotImplementedError

+ 1208 - 0
appGUI/preferences/PreferencesUIManager.py

@@ -0,0 +1,1208 @@
+import os
+from PyQt5 import QtGui, QtCore, QtWidgets
+from PyQt5.QtCore import QSettings
+from defaults import FlatCAMDefaults
+import logging
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+log = logging.getLogger('base2')
+
+
+class PreferencesUIManager:
+
+    def __init__(self, defaults: FlatCAMDefaults, data_path: str, ui, inform):
+        """
+        Class that control the Preferences Tab
+
+        :param defaults:    a dictionary storage where all the application settings are stored
+        :param data_path:   a path to the file where all the preferences are stored for persistence
+        :param ui:          reference to the MainGUI class which constructs the UI
+        :param inform:      a pyqtSignal used to display information's in the StatusBar of the GUI
+        """
+
+        self.defaults = defaults
+        self.data_path = data_path
+        self.ui = ui
+        self.inform = inform
+        self.ignore_tab_close_event = False
+
+        # if Preferences are changed in the Edit -> Preferences tab the value will be set to True
+        self.preferences_changed_flag = False
+
+        self.old_color = QtGui.QColor('black')
+
+        # when adding entries here read the comments in the  method found below named:
+        # def app_obj.new_object(self, kind, name, initialize, active=True, fit=True, plot=True)
+        self.defaults_form_fields = {
+            # General App
+            "decimals_inch": self.ui.general_defaults_form.general_app_group.precision_inch_entry,
+            "decimals_metric": self.ui.general_defaults_form.general_app_group.precision_metric_entry,
+            "units": self.ui.general_defaults_form.general_app_group.units_radio,
+            "global_graphic_engine": self.ui.general_defaults_form.general_app_group.ge_radio,
+            "global_app_level": self.ui.general_defaults_form.general_app_group.app_level_radio,
+            "global_portable": self.ui.general_defaults_form.general_app_group.portability_cb,
+            "global_language": self.ui.general_defaults_form.general_app_group.language_cb,
+
+            "global_systray_icon": self.ui.general_defaults_form.general_app_group.systray_cb,
+            "global_shell_at_startup": self.ui.general_defaults_form.general_app_group.shell_startup_cb,
+            "global_project_at_startup": self.ui.general_defaults_form.general_app_group.project_startup_cb,
+            "global_version_check": self.ui.general_defaults_form.general_app_group.version_check_cb,
+            "global_send_stats": self.ui.general_defaults_form.general_app_group.send_stats_cb,
+
+            "global_worker_number": self.ui.general_defaults_form.general_app_group.worker_number_sb,
+            "global_tolerance": self.ui.general_defaults_form.general_app_group.tol_entry,
+
+            "global_compression_level": self.ui.general_defaults_form.general_app_group.compress_spinner,
+            "global_save_compressed": self.ui.general_defaults_form.general_app_group.save_type_cb,
+            "global_autosave": self.ui.general_defaults_form.general_app_group.autosave_cb,
+            "global_autosave_timeout": self.ui.general_defaults_form.general_app_group.autosave_entry,
+
+            "global_tpdf_tmargin": self.ui.general_defaults_form.general_app_group.tmargin_entry,
+            "global_tpdf_bmargin": self.ui.general_defaults_form.general_app_group.bmargin_entry,
+            "global_tpdf_lmargin": self.ui.general_defaults_form.general_app_group.lmargin_entry,
+            "global_tpdf_rmargin": self.ui.general_defaults_form.general_app_group.rmargin_entry,
+
+            # General GUI Preferences
+            "global_theme": self.ui.general_defaults_form.general_gui_group.theme_radio,
+            "global_gray_icons": self.ui.general_defaults_form.general_gui_group.gray_icons_cb,
+            "global_layout": self.ui.general_defaults_form.general_gui_group.layout_combo,
+            "global_hover": self.ui.general_defaults_form.general_gui_group.hover_cb,
+            "global_selection_shape": self.ui.general_defaults_form.general_gui_group.selection_cb,
+
+            "global_sel_fill": self.ui.general_defaults_form.general_gui_group.sf_color_entry,
+            "global_sel_line": self.ui.general_defaults_form.general_gui_group.sl_color_entry,
+            "global_alt_sel_fill": self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry,
+            "global_alt_sel_line": self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry,
+            "global_draw_color": self.ui.general_defaults_form.general_gui_group.draw_color_entry,
+            "global_sel_draw_color": self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry,
+
+            "global_proj_item_color": self.ui.general_defaults_form.general_gui_group.proj_color_entry,
+            "global_proj_item_dis_color": self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry,
+            "global_project_autohide": self.ui.general_defaults_form.general_gui_group.project_autohide_cb,
+
+            # General APP Settings
+            "global_gridx": self.ui.general_defaults_form.general_app_set_group.gridx_entry,
+            "global_gridy": self.ui.general_defaults_form.general_app_set_group.gridy_entry,
+            "global_snap_max": self.ui.general_defaults_form.general_app_set_group.snap_max_dist_entry,
+            "global_workspace": self.ui.general_defaults_form.general_app_set_group.workspace_cb,
+            "global_workspaceT": self.ui.general_defaults_form.general_app_set_group.wk_cb,
+            "global_workspace_orientation": self.ui.general_defaults_form.general_app_set_group.wk_orientation_radio,
+
+            "global_cursor_type": self.ui.general_defaults_form.general_app_set_group.cursor_radio,
+            "global_cursor_size": self.ui.general_defaults_form.general_app_set_group.cursor_size_entry,
+            "global_cursor_width": self.ui.general_defaults_form.general_app_set_group.cursor_width_entry,
+            "global_cursor_color_enabled": self.ui.general_defaults_form.general_app_set_group.mouse_cursor_color_cb,
+            "global_cursor_color": self.ui.general_defaults_form.general_app_set_group.mouse_cursor_entry,
+            "global_pan_button": self.ui.general_defaults_form.general_app_set_group.pan_button_radio,
+            "global_mselect_key": self.ui.general_defaults_form.general_app_set_group.mselect_radio,
+            "global_delete_confirmation": self.ui.general_defaults_form.general_app_set_group.delete_conf_cb,
+            "global_allow_edit_in_project_tab": self.ui.general_defaults_form.general_app_set_group.allow_edit_cb,
+            "global_open_style": self.ui.general_defaults_form.general_app_set_group.open_style_cb,
+            "global_toggle_tooltips": self.ui.general_defaults_form.general_app_set_group.toggle_tooltips_cb,
+            "global_machinist_setting": self.ui.general_defaults_form.general_app_set_group.machinist_cb,
+
+            "global_bookmarks_limit": self.ui.general_defaults_form.general_app_set_group.bm_limit_spinner,
+            "global_activity_icon": self.ui.general_defaults_form.general_app_set_group.activity_combo,
+
+            # Gerber General
+            "gerber_plot": self.ui.gerber_defaults_form.gerber_gen_group.plot_cb,
+            "gerber_solid": self.ui.gerber_defaults_form.gerber_gen_group.solid_cb,
+            "gerber_multicolored": self.ui.gerber_defaults_form.gerber_gen_group.multicolored_cb,
+            "gerber_store_color_list": self.ui.gerber_defaults_form.gerber_gen_group.store_colors_cb,
+            "gerber_circle_steps": self.ui.gerber_defaults_form.gerber_gen_group.circle_steps_entry,
+            "gerber_def_units": self.ui.gerber_defaults_form.gerber_gen_group.gerber_units_radio,
+            "gerber_def_zeros": self.ui.gerber_defaults_form.gerber_gen_group.gerber_zeros_radio,
+            "gerber_clean_apertures": self.ui.gerber_defaults_form.gerber_gen_group.gerber_clean_cb,
+            "gerber_extra_buffering": self.ui.gerber_defaults_form.gerber_gen_group.gerber_extra_buffering,
+            "gerber_plot_fill": self.ui.gerber_defaults_form.gerber_gen_group.fill_color_entry,
+            "gerber_plot_line": self.ui.gerber_defaults_form.gerber_gen_group.line_color_entry,
+
+            # Gerber Options
+            "gerber_noncoppermargin": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_margin_entry,
+            "gerber_noncopperrounded": self.ui.gerber_defaults_form.gerber_opt_group.noncopper_rounded_cb,
+            "gerber_bboxmargin": self.ui.gerber_defaults_form.gerber_opt_group.bbmargin_entry,
+            "gerber_bboxrounded": self.ui.gerber_defaults_form.gerber_opt_group.bbrounded_cb,
+
+            # Gerber Advanced Options
+            "gerber_aperture_display": self.ui.gerber_defaults_form.gerber_adv_opt_group.aperture_table_visibility_cb,
+            # "gerber_aperture_scale_factor": self.ui.gerber_defaults_form.gerber_adv_opt_group.scale_aperture_entry,
+            # "gerber_aperture_buffer_factor": self.ui.gerber_defaults_form.gerber_adv_opt_group.buffer_aperture_entry,
+            "gerber_follow": self.ui.gerber_defaults_form.gerber_adv_opt_group.follow_cb,
+            "gerber_buffering": self.ui.gerber_defaults_form.gerber_adv_opt_group.buffering_radio,
+            "gerber_delayed_buffering": self.ui.gerber_defaults_form.gerber_adv_opt_group.delayed_buffer_cb,
+            "gerber_simplification": self.ui.gerber_defaults_form.gerber_adv_opt_group.simplify_cb,
+            "gerber_simp_tolerance": self.ui.gerber_defaults_form.gerber_adv_opt_group.simplification_tol_spinner,
+
+            # Gerber Export
+            "gerber_exp_units": self.ui.gerber_defaults_form.gerber_exp_group.gerber_units_radio,
+            "gerber_exp_integer": self.ui.gerber_defaults_form.gerber_exp_group.format_whole_entry,
+            "gerber_exp_decimals": self.ui.gerber_defaults_form.gerber_exp_group.format_dec_entry,
+            "gerber_exp_zeros": self.ui.gerber_defaults_form.gerber_exp_group.zeros_radio,
+
+            # Gerber Editor
+            "gerber_editor_sel_limit": self.ui.gerber_defaults_form.gerber_editor_group.sel_limit_entry,
+            "gerber_editor_newcode": self.ui.gerber_defaults_form.gerber_editor_group.addcode_entry,
+            "gerber_editor_newsize": self.ui.gerber_defaults_form.gerber_editor_group.addsize_entry,
+            "gerber_editor_newtype": self.ui.gerber_defaults_form.gerber_editor_group.addtype_combo,
+            "gerber_editor_newdim": self.ui.gerber_defaults_form.gerber_editor_group.adddim_entry,
+            "gerber_editor_array_size": self.ui.gerber_defaults_form.gerber_editor_group.grb_array_size_entry,
+            "gerber_editor_lin_axis": self.ui.gerber_defaults_form.gerber_editor_group.grb_axis_radio,
+            "gerber_editor_lin_pitch": self.ui.gerber_defaults_form.gerber_editor_group.grb_pitch_entry,
+            "gerber_editor_lin_angle": self.ui.gerber_defaults_form.gerber_editor_group.grb_angle_entry,
+            "gerber_editor_circ_dir": self.ui.gerber_defaults_form.gerber_editor_group.grb_circular_dir_radio,
+            "gerber_editor_circ_angle":
+                self.ui.gerber_defaults_form.gerber_editor_group.grb_circular_angle_entry,
+            "gerber_editor_scale_f": self.ui.gerber_defaults_form.gerber_editor_group.grb_scale_entry,
+            "gerber_editor_buff_f": self.ui.gerber_defaults_form.gerber_editor_group.grb_buff_entry,
+            "gerber_editor_ma_low": self.ui.gerber_defaults_form.gerber_editor_group.grb_ma_low_entry,
+            "gerber_editor_ma_high": self.ui.gerber_defaults_form.gerber_editor_group.grb_ma_high_entry,
+
+            # Excellon General
+            "excellon_plot": self.ui.excellon_defaults_form.excellon_gen_group.plot_cb,
+            "excellon_solid": self.ui.excellon_defaults_form.excellon_gen_group.solid_cb,
+            "excellon_multicolored": self.ui.excellon_defaults_form.excellon_gen_group.multicolored_cb,
+            "excellon_merge_fuse_tools": self.ui.excellon_defaults_form.excellon_gen_group.fuse_tools_cb,
+            "excellon_format_upper_in":
+                self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_in_entry,
+            "excellon_format_lower_in":
+                self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_lower_in_entry,
+            "excellon_format_upper_mm":
+                self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_mm_entry,
+            "excellon_format_lower_mm":
+                self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_lower_mm_entry,
+            "excellon_zeros": self.ui.excellon_defaults_form.excellon_gen_group.excellon_zeros_radio,
+            "excellon_units": self.ui.excellon_defaults_form.excellon_gen_group.excellon_units_radio,
+            "excellon_update": self.ui.excellon_defaults_form.excellon_gen_group.update_excellon_cb,
+            "excellon_optimization_type": self.ui.excellon_defaults_form.excellon_gen_group.excellon_optimization_radio,
+            "excellon_search_time": self.ui.excellon_defaults_form.excellon_gen_group.optimization_time_entry,
+            "excellon_plot_fill": self.ui.excellon_defaults_form.excellon_gen_group.fill_color_entry,
+            "excellon_plot_line": self.ui.excellon_defaults_form.excellon_gen_group.line_color_entry,
+
+            # Excellon Options
+            "excellon_operation": self.ui.excellon_defaults_form.excellon_opt_group.operation_radio,
+            "excellon_milling_type": self.ui.excellon_defaults_form.excellon_opt_group.milling_type_radio,
+
+            "excellon_milling_dia": self.ui.excellon_defaults_form.excellon_opt_group.mill_dia_entry,
+
+            "excellon_tooldia": self.ui.excellon_defaults_form.excellon_opt_group.tooldia_entry,
+            "excellon_slot_tooldia": self.ui.excellon_defaults_form.excellon_opt_group.slot_tooldia_entry,
+
+            # Excellon Advanced Options
+            "excellon_tools_table_display": self.ui.excellon_defaults_form.excellon_adv_opt_group.table_visibility_cb,
+            "excellon_autoload_db":         self.ui.excellon_defaults_form.excellon_adv_opt_group.autoload_db_cb,
+
+            # Excellon Export
+            "excellon_exp_units":       self.ui.excellon_defaults_form.excellon_exp_group.excellon_units_radio,
+            "excellon_exp_format":      self.ui.excellon_defaults_form.excellon_exp_group.format_radio,
+            "excellon_exp_integer":     self.ui.excellon_defaults_form.excellon_exp_group.format_whole_entry,
+            "excellon_exp_decimals":    self.ui.excellon_defaults_form.excellon_exp_group.format_dec_entry,
+            "excellon_exp_zeros":       self.ui.excellon_defaults_form.excellon_exp_group.zeros_radio,
+            "excellon_exp_slot_type":   self.ui.excellon_defaults_form.excellon_exp_group.slot_type_radio,
+
+            # Excellon Editor
+            "excellon_editor_sel_limit":    self.ui.excellon_defaults_form.excellon_editor_group.sel_limit_entry,
+            "excellon_editor_newdia":       self.ui.excellon_defaults_form.excellon_editor_group.addtool_entry,
+            "excellon_editor_array_size":   self.ui.excellon_defaults_form.excellon_editor_group.drill_array_size_entry,
+            "excellon_editor_lin_dir":      self.ui.excellon_defaults_form.excellon_editor_group.drill_axis_radio,
+            "excellon_editor_lin_pitch":    self.ui.excellon_defaults_form.excellon_editor_group.drill_pitch_entry,
+            "excellon_editor_lin_angle":    self.ui.excellon_defaults_form.excellon_editor_group.drill_angle_entry,
+            "excellon_editor_circ_dir": self.ui.excellon_defaults_form.excellon_editor_group.drill_circular_dir_radio,
+            "excellon_editor_circ_angle":
+                self.ui.excellon_defaults_form.excellon_editor_group.drill_circular_angle_entry,
+            # Excellon Slots
+            "excellon_editor_slot_direction":
+                self.ui.excellon_defaults_form.excellon_editor_group.slot_axis_radio,
+            "excellon_editor_slot_angle":
+                self.ui.excellon_defaults_form.excellon_editor_group.slot_angle_spinner,
+            "excellon_editor_slot_length":
+                self.ui.excellon_defaults_form.excellon_editor_group.slot_length_entry,
+            # Excellon Slots
+            "excellon_editor_slot_array_size":
+                self.ui.excellon_defaults_form.excellon_editor_group.slot_array_size_entry,
+            "excellon_editor_slot_lin_dir": self.ui.excellon_defaults_form.excellon_editor_group.slot_array_axis_radio,
+            "excellon_editor_slot_lin_pitch":
+                self.ui.excellon_defaults_form.excellon_editor_group.slot_array_pitch_entry,
+            "excellon_editor_slot_lin_angle":
+                self.ui.excellon_defaults_form.excellon_editor_group.slot_array_angle_entry,
+            "excellon_editor_slot_circ_dir":
+                self.ui.excellon_defaults_form.excellon_editor_group.slot_array_circular_dir_radio,
+            "excellon_editor_slot_circ_angle":
+                self.ui.excellon_defaults_form.excellon_editor_group.slot_array_circular_angle_entry,
+
+            # Geometry General
+            "geometry_plot":                self.ui.geometry_defaults_form.geometry_gen_group.plot_cb,
+            "geometry_multicolored":        self.ui.geometry_defaults_form.geometry_gen_group.multicolored_cb,
+            "geometry_circle_steps":        self.ui.geometry_defaults_form.geometry_gen_group.circle_steps_entry,
+            "geometry_cnctooldia":          self.ui.geometry_defaults_form.geometry_gen_group.cnctooldia_entry,
+            "geometry_merge_fuse_tools":    self.ui.geometry_defaults_form.geometry_gen_group.fuse_tools_cb,
+            "geometry_plot_line":           self.ui.geometry_defaults_form.geometry_gen_group.line_color_entry,
+            "geometry_optimization_type":   self.ui.geometry_defaults_form.geometry_gen_group.opt_algorithm_radio,
+            "geometry_search_time":         self.ui.geometry_defaults_form.geometry_gen_group.optimization_time_entry,
+
+            # Geometry Options
+            "geometry_cutz":            self.ui.geometry_defaults_form.geometry_opt_group.cutz_entry,
+            "geometry_travelz":         self.ui.geometry_defaults_form.geometry_opt_group.travelz_entry,
+            "geometry_feedrate":        self.ui.geometry_defaults_form.geometry_opt_group.cncfeedrate_entry,
+            "geometry_feedrate_z":      self.ui.geometry_defaults_form.geometry_opt_group.feedrate_z_entry,
+            "geometry_spindlespeed":    self.ui.geometry_defaults_form.geometry_opt_group.cncspindlespeed_entry,
+            "geometry_dwell":           self.ui.geometry_defaults_form.geometry_opt_group.dwell_cb,
+            "geometry_dwelltime":       self.ui.geometry_defaults_form.geometry_opt_group.dwelltime_entry,
+            "geometry_ppname_g":        self.ui.geometry_defaults_form.geometry_opt_group.pp_geometry_name_cb,
+            "geometry_toolchange":      self.ui.geometry_defaults_form.geometry_opt_group.toolchange_cb,
+            "geometry_toolchangez":     self.ui.geometry_defaults_form.geometry_opt_group.toolchangez_entry,
+            "geometry_endz":            self.ui.geometry_defaults_form.geometry_opt_group.endz_entry,
+            "geometry_endxy":           self.ui.geometry_defaults_form.geometry_opt_group.endxy_entry,
+            "geometry_depthperpass":    self.ui.geometry_defaults_form.geometry_opt_group.depthperpass_entry,
+            "geometry_multidepth":      self.ui.geometry_defaults_form.geometry_opt_group.multidepth_cb,
+
+            # Geometry Advanced Options
+            "geometry_toolchangexy":    self.ui.geometry_defaults_form.geometry_adv_opt_group.toolchangexy_entry,
+            "geometry_startz":          self.ui.geometry_defaults_form.geometry_adv_opt_group.gstartz_entry,
+            "geometry_feedrate_rapid":  self.ui.geometry_defaults_form.geometry_adv_opt_group.feedrate_rapid_entry,
+            "geometry_extracut":        self.ui.geometry_defaults_form.geometry_adv_opt_group.extracut_cb,
+            "geometry_extracut_length": self.ui.geometry_defaults_form.geometry_adv_opt_group.e_cut_entry,
+            "geometry_z_pdepth":        self.ui.geometry_defaults_form.geometry_adv_opt_group.pdepth_entry,
+            "geometry_feedrate_probe":  self.ui.geometry_defaults_form.geometry_adv_opt_group.feedrate_probe_entry,
+            "geometry_spindledir":      self.ui.geometry_defaults_form.geometry_adv_opt_group.spindledir_radio,
+            "geometry_f_plunge":        self.ui.geometry_defaults_form.geometry_adv_opt_group.fplunge_cb,
+            "geometry_segx":            self.ui.geometry_defaults_form.geometry_adv_opt_group.segx_entry,
+            "geometry_segy":            self.ui.geometry_defaults_form.geometry_adv_opt_group.segy_entry,
+            "geometry_area_exclusion":  self.ui.geometry_defaults_form.geometry_adv_opt_group.exclusion_cb,
+            "geometry_area_shape":      self.ui.geometry_defaults_form.geometry_adv_opt_group.area_shape_radio,
+            "geometry_area_strategy":   self.ui.geometry_defaults_form.geometry_adv_opt_group.strategy_radio,
+            "geometry_area_overz":      self.ui.geometry_defaults_form.geometry_adv_opt_group.over_z_entry,
+            # Polish
+            "geometry_polish":          self.ui.geometry_defaults_form.geometry_adv_opt_group.polish_cb,
+            "geometry_polish_dia":      self.ui.geometry_defaults_form.geometry_adv_opt_group.polish_dia_entry,
+            "geometry_polish_pressure": self.ui.geometry_defaults_form.geometry_adv_opt_group.polish_pressure_entry,
+            "geometry_polish_travelz":  self.ui.geometry_defaults_form.geometry_adv_opt_group.polish_travelz_entry,
+            "geometry_polish_margin":   self.ui.geometry_defaults_form.geometry_adv_opt_group.polish_margin_entry,
+            "geometry_polish_overlap":  self.ui.geometry_defaults_form.geometry_adv_opt_group.polish_over_entry,
+            "geometry_polish_method":   self.ui.geometry_defaults_form.geometry_adv_opt_group.polish_method_combo,
+
+            # Geometry Editor
+            "geometry_editor_sel_limit":        self.ui.geometry_defaults_form.geometry_editor_group.sel_limit_entry,
+            "geometry_editor_milling_type":     self.ui.geometry_defaults_form.geometry_editor_group.milling_type_radio,
+
+            # CNCJob General
+            "cncjob_plot":              self.ui.cncjob_defaults_form.cncjob_gen_group.plot_cb,
+
+            "cncjob_tooldia":           self.ui.cncjob_defaults_form.cncjob_gen_group.tooldia_entry,
+            "cncjob_coords_type":       self.ui.cncjob_defaults_form.cncjob_gen_group.coords_type_radio,
+            "cncjob_coords_decimals":   self.ui.cncjob_defaults_form.cncjob_gen_group.coords_dec_entry,
+            "cncjob_fr_decimals":       self.ui.cncjob_defaults_form.cncjob_gen_group.fr_dec_entry,
+            "cncjob_steps_per_circle":  self.ui.cncjob_defaults_form.cncjob_gen_group.steps_per_circle_entry,
+            "cncjob_line_ending":       self.ui.cncjob_defaults_form.cncjob_gen_group.line_ending_cb,
+            "cncjob_plot_line":         self.ui.cncjob_defaults_form.cncjob_gen_group.line_color_entry,
+            "cncjob_plot_fill":         self.ui.cncjob_defaults_form.cncjob_gen_group.fill_color_entry,
+            "cncjob_travel_line":       self.ui.cncjob_defaults_form.cncjob_gen_group.tline_color_entry,
+            "cncjob_travel_fill":       self.ui.cncjob_defaults_form.cncjob_gen_group.tfill_color_entry,
+
+            # CNC Job Options
+            "cncjob_plot_kind":         self.ui.cncjob_defaults_form.cncjob_opt_group.cncplot_method_radio,
+            "cncjob_annotation":        self.ui.cncjob_defaults_form.cncjob_opt_group.annotation_cb,
+
+            # CNC Job Advanced Options
+            "cncjob_annotation_fontsize":   self.ui.cncjob_defaults_form.cncjob_adv_opt_group.annotation_fontsize_sp,
+            "cncjob_annotation_fontcolor": self.ui.cncjob_defaults_form.cncjob_adv_opt_group.annotation_fontcolor_entry,
+            # Autolevelling
+            "cncjob_al_mode":               self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_mode_radio,
+            "cncjob_al_method":             self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_method_radio,
+            "cncjob_al_rows":               self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_rows_entry,
+            "cncjob_al_columns":            self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_columns_entry,
+            "cncjob_al_travelz":            self.ui.cncjob_defaults_form.cncjob_adv_opt_group.ptravelz_entry,
+            "cncjob_al_probe_depth":        self.ui.cncjob_defaults_form.cncjob_adv_opt_group.pdepth_entry,
+            "cncjob_al_probe_fr":           self.ui.cncjob_defaults_form.cncjob_adv_opt_group.feedrate_probe_entry,
+            "cncjob_al_controller":         self.ui.cncjob_defaults_form.cncjob_adv_opt_group.al_controller_combo,
+            "cncjob_al_grbl_jog_step":      self.ui.cncjob_defaults_form.cncjob_adv_opt_group.jog_step_entry,
+            "cncjob_al_grbl_jog_fr":        self.ui.cncjob_defaults_form.cncjob_adv_opt_group.jog_fr_entry,
+            "cncjob_al_grbl_travelz":       self.ui.cncjob_defaults_form.cncjob_adv_opt_group.jog_travelz_entry,
+
+            # CNC Job (GCode) Editor
+            "cncjob_prepend":               self.ui.cncjob_defaults_form.cncjob_editor_group.prepend_text,
+            "cncjob_append":                self.ui.cncjob_defaults_form.cncjob_editor_group.append_text,
+
+            # Isolation Routing Tool
+            "tools_iso_tooldia":        self.ui.tools_defaults_form.tools_iso_group.tool_dia_entry,
+            "tools_iso_order":          self.ui.tools_defaults_form.tools_iso_group.order_radio,
+            "tools_iso_tool_type":      self.ui.tools_defaults_form.tools_iso_group.tool_type_radio,
+            "tools_iso_tool_vtipdia":   self.ui.tools_defaults_form.tools_iso_group.tipdia_entry,
+            "tools_iso_tool_vtipangle": self.ui.tools_defaults_form.tools_iso_group.tipangle_entry,
+            "tools_iso_tool_cutz":      self.ui.tools_defaults_form.tools_iso_group.cutz_entry,
+            "tools_iso_newdia":         self.ui.tools_defaults_form.tools_iso_group.newdia_entry,
+
+            "tools_iso_passes":         self.ui.tools_defaults_form.tools_iso_group.passes_entry,
+            "tools_iso_overlap":        self.ui.tools_defaults_form.tools_iso_group.overlap_entry,
+            "tools_iso_milling_type":   self.ui.tools_defaults_form.tools_iso_group.milling_type_radio,
+            "tools_iso_follow":         self.ui.tools_defaults_form.tools_iso_group.follow_cb,
+            "tools_iso_isotype":        self.ui.tools_defaults_form.tools_iso_group.iso_type_radio,
+
+            "tools_iso_rest":           self.ui.tools_defaults_form.tools_iso_group.rest_cb,
+            "tools_iso_combine_passes": self.ui.tools_defaults_form.tools_iso_group.combine_passes_cb,
+            "tools_iso_check_valid":    self.ui.tools_defaults_form.tools_iso_group.valid_cb,
+            "tools_iso_isoexcept":      self.ui.tools_defaults_form.tools_iso_group.except_cb,
+            "tools_iso_selection":      self.ui.tools_defaults_form.tools_iso_group.select_combo,
+            "tools_iso_poly_ints":      self.ui.tools_defaults_form.tools_iso_group.poly_int_cb,
+            "tools_iso_force":          self.ui.tools_defaults_form.tools_iso_group.force_iso_cb,
+            "tools_iso_area_shape":     self.ui.tools_defaults_form.tools_iso_group.area_shape_radio,
+            "tools_iso_plotting":       self.ui.tools_defaults_form.tools_iso_group.plotting_radio,
+
+            # Drilling Tool
+            "tools_drill_tool_order":   self.ui.tools_defaults_form.tools_drill_group.order_radio,
+            "tools_drill_cutz":         self.ui.tools_defaults_form.tools_drill_group.cutz_entry,
+            "tools_drill_multidepth":   self.ui.tools_defaults_form.tools_drill_group.mpass_cb,
+            "tools_drill_depthperpass": self.ui.tools_defaults_form.tools_drill_group.maxdepth_entry,
+            "tools_drill_travelz":      self.ui.tools_defaults_form.tools_drill_group.travelz_entry,
+            "tools_drill_endz":         self.ui.tools_defaults_form.tools_drill_group.endz_entry,
+            "tools_drill_endxy":        self.ui.tools_defaults_form.tools_drill_group.endxy_entry,
+
+            "tools_drill_feedrate_z":   self.ui.tools_defaults_form.tools_drill_group.feedrate_z_entry,
+            "tools_drill_spindlespeed": self.ui.tools_defaults_form.tools_drill_group.spindlespeed_entry,
+            "tools_drill_dwell":        self.ui.tools_defaults_form.tools_drill_group.dwell_cb,
+            "tools_drill_dwelltime":    self.ui.tools_defaults_form.tools_drill_group.dwelltime_entry,
+            "tools_drill_toolchange":   self.ui.tools_defaults_form.tools_drill_group.toolchange_cb,
+            "tools_drill_toolchangez":  self.ui.tools_defaults_form.tools_drill_group.toolchangez_entry,
+            "tools_drill_ppname_e":     self.ui.tools_defaults_form.tools_drill_group.pp_excellon_name_cb,
+
+            "tools_drill_drill_slots":      self.ui.tools_defaults_form.tools_drill_group.drill_slots_cb,
+            "tools_drill_drill_overlap":    self.ui.tools_defaults_form.tools_drill_group.drill_overlap_entry,
+            "tools_drill_last_drill":       self.ui.tools_defaults_form.tools_drill_group.last_drill_cb,
+
+            # Advanced Options
+            "tools_drill_offset":           self.ui.tools_defaults_form.tools_drill_group.offset_entry,
+            "tools_drill_toolchangexy":     self.ui.tools_defaults_form.tools_drill_group.toolchangexy_entry,
+            "tools_drill_startz":           self.ui.tools_defaults_form.tools_drill_group.estartz_entry,
+            "tools_drill_feedrate_rapid":   self.ui.tools_defaults_form.tools_drill_group.feedrate_rapid_entry,
+            "tools_drill_z_pdepth":         self.ui.tools_defaults_form.tools_drill_group.pdepth_entry,
+            "tools_drill_feedrate_probe":   self.ui.tools_defaults_form.tools_drill_group.feedrate_probe_entry,
+            "tools_drill_spindledir":       self.ui.tools_defaults_form.tools_drill_group.spindledir_radio,
+            "tools_drill_f_plunge":         self.ui.tools_defaults_form.tools_drill_group.fplunge_cb,
+            "tools_drill_f_retract":        self.ui.tools_defaults_form.tools_drill_group.fretract_cb,
+
+            # Area Exclusion
+            "tools_drill_area_exclusion":   self.ui.tools_defaults_form.tools_drill_group.exclusion_cb,
+            "tools_drill_area_shape":       self.ui.tools_defaults_form.tools_drill_group.area_shape_radio,
+            "tools_drill_area_strategy":    self.ui.tools_defaults_form.tools_drill_group.strategy_radio,
+            "tools_drill_area_overz":       self.ui.tools_defaults_form.tools_drill_group.over_z_entry,
+
+            # NCC Tool
+            "tools_ncc_tools":           self.ui.tools_defaults_form.tools_ncc_group.ncc_tool_dia_entry,
+            "tools_ncc_order":           self.ui.tools_defaults_form.tools_ncc_group.ncc_order_radio,
+            "tools_ncc_overlap":         self.ui.tools_defaults_form.tools_ncc_group.ncc_overlap_entry,
+            "tools_ncc_margin":          self.ui.tools_defaults_form.tools_ncc_group.ncc_margin_entry,
+            "tools_ncc_method":          self.ui.tools_defaults_form.tools_ncc_group.ncc_method_combo,
+            "tools_ncc_connect":         self.ui.tools_defaults_form.tools_ncc_group.ncc_connect_cb,
+            "tools_ncc_contour":         self.ui.tools_defaults_form.tools_ncc_group.ncc_contour_cb,
+            "tools_ncc_rest":            self.ui.tools_defaults_form.tools_ncc_group.ncc_rest_cb,
+            "tools_ncc_offset_choice":  self.ui.tools_defaults_form.tools_ncc_group.ncc_choice_offset_cb,
+            "tools_ncc_offset_value":   self.ui.tools_defaults_form.tools_ncc_group.ncc_offset_spinner,
+            "tools_ncc_ref":             self.ui.tools_defaults_form.tools_ncc_group.select_combo,
+            "tools_ncc_area_shape":     self.ui.tools_defaults_form.tools_ncc_group.area_shape_radio,
+            "tools_ncc_milling_type":    self.ui.tools_defaults_form.tools_ncc_group.milling_type_radio,
+            "tools_ncc_tool_type":       self.ui.tools_defaults_form.tools_ncc_group.tool_type_radio,
+            "tools_ncc_cutz":            self.ui.tools_defaults_form.tools_ncc_group.cutz_entry,
+            "tools_ncc_tipdia":          self.ui.tools_defaults_form.tools_ncc_group.tipdia_entry,
+            "tools_ncc_tipangle":        self.ui.tools_defaults_form.tools_ncc_group.tipangle_entry,
+            "tools_ncc_newdia":          self.ui.tools_defaults_form.tools_ncc_group.newdia_entry,
+            "tools_ncc_plotting":       self.ui.tools_defaults_form.tools_ncc_group.plotting_radio,
+            "tools_ncc_check_valid":    self.ui.tools_defaults_form.tools_ncc_group.valid_cb,
+
+            # CutOut Tool
+            "tools_cutout_tooldia":          self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry,
+            "tools_cutout_kind":             self.ui.tools_defaults_form.tools_cutout_group.obj_kind_combo,
+            "tools_cutout_margin":          self.ui.tools_defaults_form.tools_cutout_group.cutout_margin_entry,
+            "tools_cutout_z":               self.ui.tools_defaults_form.tools_cutout_group.cutz_entry,
+            "tools_cutout_depthperpass":    self.ui.tools_defaults_form.tools_cutout_group.maxdepth_entry,
+            "tools_cutout_mdepth":          self.ui.tools_defaults_form.tools_cutout_group.mpass_cb,
+            "tools_cutout_gapsize":         self.ui.tools_defaults_form.tools_cutout_group.cutout_gap_entry,
+            "tools_cutout_gaps_ff":         self.ui.tools_defaults_form.tools_cutout_group.gaps_combo,
+            "tools_cutout_convexshape":     self.ui.tools_defaults_form.tools_cutout_group.convex_box,
+            "tools_cutout_big_cursor":      self.ui.tools_defaults_form.tools_cutout_group.big_cursor_cb,
+
+            "tools_cutout_gap_type":        self.ui.tools_defaults_form.tools_cutout_group.gaptype_radio,
+            "tools_cutout_gap_depth":       self.ui.tools_defaults_form.tools_cutout_group.thin_depth_entry,
+            "tools_cutout_mb_dia":          self.ui.tools_defaults_form.tools_cutout_group.mb_dia_entry,
+            "tools_cutout_mb_spacing":      self.ui.tools_defaults_form.tools_cutout_group.mb_spacing_entry,
+
+            # Paint Area Tool
+            "tools_paint_tooldia":       self.ui.tools_defaults_form.tools_paint_group.painttooldia_entry,
+            "tools_paint_order":         self.ui.tools_defaults_form.tools_paint_group.paint_order_radio,
+            "tools_paint_overlap":       self.ui.tools_defaults_form.tools_paint_group.paintoverlap_entry,
+            "tools_paint_offset":        self.ui.tools_defaults_form.tools_paint_group.paintmargin_entry,
+            "tools_paint_method":        self.ui.tools_defaults_form.tools_paint_group.paintmethod_combo,
+            "tools_paint_selectmethod":       self.ui.tools_defaults_form.tools_paint_group.selectmethod_combo,
+            "tools_paint_area_shape":   self.ui.tools_defaults_form.tools_paint_group.area_shape_radio,
+            "tools_paint_connect":        self.ui.tools_defaults_form.tools_paint_group.pathconnect_cb,
+            "tools_paint_contour":       self.ui.tools_defaults_form.tools_paint_group.contour_cb,
+            "tools_paint_plotting":     self.ui.tools_defaults_form.tools_paint_group.paint_plotting_radio,
+
+            "tools_paint_rest":          self.ui.tools_defaults_form.tools_paint_group.rest_cb,
+            "tools_paint_tool_type":     self.ui.tools_defaults_form.tools_paint_group.tool_type_radio,
+            "tools_paint_cutz":          self.ui.tools_defaults_form.tools_paint_group.cutz_entry,
+            "tools_paint_tipdia":        self.ui.tools_defaults_form.tools_paint_group.tipdia_entry,
+            "tools_paint_tipangle":      self.ui.tools_defaults_form.tools_paint_group.tipangle_entry,
+            "tools_paint_newdia":        self.ui.tools_defaults_form.tools_paint_group.newdia_entry,
+
+            # 2-sided Tool
+            "tools_2sided_mirror_axis": self.ui.tools_defaults_form.tools_2sided_group.mirror_axis_radio,
+            "tools_2sided_axis_loc":    self.ui.tools_defaults_form.tools_2sided_group.axis_location_radio,
+            "tools_2sided_drilldia":    self.ui.tools_defaults_form.tools_2sided_group.drill_dia_entry,
+            "tools_2sided_allign_axis": self.ui.tools_defaults_form.tools_2sided_group.align_axis_radio,
+
+            # Film Tool
+            "tools_film_type": self.ui.tools_defaults_form.tools_film_group.film_type_radio,
+            "tools_film_boundary": self.ui.tools_defaults_form.tools_film_group.film_boundary_entry,
+            "tools_film_scale_stroke": self.ui.tools_defaults_form.tools_film_group.film_scale_stroke_entry,
+            "tools_film_color": self.ui.tools_defaults_form.tools_film_group.film_color_entry,
+            "tools_film_scale_cb": self.ui.tools_defaults_form.tools_film_group.film_scale_cb,
+            "tools_film_scale_x_entry": self.ui.tools_defaults_form.tools_film_group.film_scalex_entry,
+            "tools_film_scale_y_entry": self.ui.tools_defaults_form.tools_film_group.film_scaley_entry,
+            "tools_film_skew_cb": self.ui.tools_defaults_form.tools_film_group.film_skew_cb,
+            "tools_film_skew_x_entry": self.ui.tools_defaults_form.tools_film_group.film_skewx_entry,
+            "tools_film_skew_y_entry": self.ui.tools_defaults_form.tools_film_group.film_skewy_entry,
+            "tools_film_skew_ref_radio": self.ui.tools_defaults_form.tools_film_group.film_skew_reference,
+            "tools_film_mirror_cb": self.ui.tools_defaults_form.tools_film_group.film_mirror_cb,
+            "tools_film_mirror_axis_radio": self.ui.tools_defaults_form.tools_film_group.film_mirror_axis,
+            "tools_film_file_type_radio": self.ui.tools_defaults_form.tools_film_group.file_type_radio,
+            "tools_film_orientation": self.ui.tools_defaults_form.tools_film_group.orientation_radio,
+            "tools_film_pagesize": self.ui.tools_defaults_form.tools_film_group.pagesize_combo,
+            "tools_film_png_dpi": self.ui.tools_defaults_form.tools_film_group.png_dpi_spinner,
+
+            # Panelize Tool
+            "tools_panelize_spacing_columns": self.ui.tools_defaults_form.tools_panelize_group.pspacing_columns,
+            "tools_panelize_spacing_rows": self.ui.tools_defaults_form.tools_panelize_group.pspacing_rows,
+            "tools_panelize_columns": self.ui.tools_defaults_form.tools_panelize_group.pcolumns,
+            "tools_panelize_rows": self.ui.tools_defaults_form.tools_panelize_group.prows,
+            "tools_panelize_optimization": self.ui.tools_defaults_form.tools_panelize_group.poptimization_cb,
+            "tools_panelize_constrain": self.ui.tools_defaults_form.tools_panelize_group.pconstrain_cb,
+            "tools_panelize_constrainx": self.ui.tools_defaults_form.tools_panelize_group.px_width_entry,
+            "tools_panelize_constrainy": self.ui.tools_defaults_form.tools_panelize_group.py_height_entry,
+            "tools_panelize_panel_type": self.ui.tools_defaults_form.tools_panelize_group.panel_type_radio,
+
+            # Calculators Tool
+            "tools_calc_vshape_tip_dia": self.ui.tools_defaults_form.tools_calculators_group.tip_dia_entry,
+            "tools_calc_vshape_tip_angle": self.ui.tools_defaults_form.tools_calculators_group.tip_angle_entry,
+            "tools_calc_vshape_cut_z": self.ui.tools_defaults_form.tools_calculators_group.cut_z_entry,
+            "tools_calc_electro_length": self.ui.tools_defaults_form.tools_calculators_group.pcblength_entry,
+            "tools_calc_electro_width": self.ui.tools_defaults_form.tools_calculators_group.pcbwidth_entry,
+            "tools_calc_electro_area": self.ui.tools_defaults_form.tools_calculators_group.area_entry,
+            "tools_calc_electro_cdensity": self.ui.tools_defaults_form.tools_calculators_group.cdensity_entry,
+            "tools_calc_electro_growth": self.ui.tools_defaults_form.tools_calculators_group.growth_entry,
+
+            # Transformations Tool
+            "tools_transform_reference": self.ui.tools_defaults_form.tools_transform_group.ref_combo,
+            "tools_transform_ref_object": self.ui.tools_defaults_form.tools_transform_group.type_obj_combo,
+            "tools_transform_ref_point": self.ui.tools_defaults_form.tools_transform_group.point_entry,
+
+            "tools_transform_rotate": self.ui.tools_defaults_form.tools_transform_group.rotate_entry,
+
+            "tools_transform_skew_x": self.ui.tools_defaults_form.tools_transform_group.skewx_entry,
+            "tools_transform_skew_y": self.ui.tools_defaults_form.tools_transform_group.skewy_entry,
+            "tools_transform_skew_link": self.ui.tools_defaults_form.tools_transform_group.skew_link_cb,
+
+            "tools_transform_scale_x": self.ui.tools_defaults_form.tools_transform_group.scalex_entry,
+            "tools_transform_scale_y": self.ui.tools_defaults_form.tools_transform_group.scaley_entry,
+            "tools_transform_scale_link": self.ui.tools_defaults_form.tools_transform_group.scale_link_cb,
+
+            "tools_transform_offset_x": self.ui.tools_defaults_form.tools_transform_group.offx_entry,
+            "tools_transform_offset_y": self.ui.tools_defaults_form.tools_transform_group.offy_entry,
+
+            "tools_transform_buffer_dis": self.ui.tools_defaults_form.tools_transform_group.buffer_entry,
+            "tools_transform_buffer_factor": self.ui.tools_defaults_form.tools_transform_group.buffer_factor_entry,
+            "tools_transform_buffer_corner": self.ui.tools_defaults_form.tools_transform_group.buffer_rounded_cb,
+
+            # SolderPaste Dispensing Tool
+            "tools_solderpaste_tools": self.ui.tools_defaults_form.tools_solderpaste_group.nozzle_tool_dia_entry,
+            "tools_solderpaste_new": self.ui.tools_defaults_form.tools_solderpaste_group.addtool_entry,
+            "tools_solderpaste_z_start": self.ui.tools_defaults_form.tools_solderpaste_group.z_start_entry,
+            "tools_solderpaste_z_dispense": self.ui.tools_defaults_form.tools_solderpaste_group.z_dispense_entry,
+            "tools_solderpaste_z_stop": self.ui.tools_defaults_form.tools_solderpaste_group.z_stop_entry,
+            "tools_solderpaste_z_travel": self.ui.tools_defaults_form.tools_solderpaste_group.z_travel_entry,
+            "tools_solderpaste_z_toolchange": self.ui.tools_defaults_form.tools_solderpaste_group.z_toolchange_entry,
+            "tools_solderpaste_xy_toolchange": self.ui.tools_defaults_form.tools_solderpaste_group.xy_toolchange_entry,
+            "tools_solderpaste_frxy": self.ui.tools_defaults_form.tools_solderpaste_group.frxy_entry,
+            "tools_solderpaste_frz": self.ui.tools_defaults_form.tools_solderpaste_group.frz_entry,
+            "tools_solderpaste_frz_dispense": self.ui.tools_defaults_form.tools_solderpaste_group.frz_dispense_entry,
+            "tools_solderpaste_speedfwd": self.ui.tools_defaults_form.tools_solderpaste_group.speedfwd_entry,
+            "tools_solderpaste_dwellfwd": self.ui.tools_defaults_form.tools_solderpaste_group.dwellfwd_entry,
+            "tools_solderpaste_speedrev": self.ui.tools_defaults_form.tools_solderpaste_group.speedrev_entry,
+            "tools_solderpaste_dwellrev": self.ui.tools_defaults_form.tools_solderpaste_group.dwellrev_entry,
+            "tools_solderpaste_pp": self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo,
+
+            # Subtractor Tool
+            "tools_sub_close_paths": self.ui.tools_defaults_form.tools_sub_group.close_paths_cb,
+            "tools_sub_delete_sources":  self.ui.tools_defaults_form.tools_sub_group.delete_sources_cb,
+
+            # Corner Markers Tool
+            "tools_corners_type": self.ui.tools_defaults_form.tools_corners_group.type_radio,
+            "tools_corners_thickness": self.ui.tools_defaults_form.tools_corners_group.thick_entry,
+            "tools_corners_length": self.ui.tools_defaults_form.tools_corners_group.l_entry,
+            "tools_corners_margin": self.ui.tools_defaults_form.tools_corners_group.margin_entry,
+            "tools_corners_drill_dia": self.ui.tools_defaults_form.tools_corners_group.drill_dia_entry,
+
+            # #######################################################################################################
+            # ########################################## TOOLS 2 ####################################################
+            # #######################################################################################################
+
+            # Optimal Tool
+            "tools_opt_precision": self.ui.tools2_defaults_form.tools2_optimal_group.precision_sp,
+
+            # Check Rules Tool
+            "tools_cr_trace_size": self.ui.tools2_defaults_form.tools2_checkrules_group.trace_size_cb,
+            "tools_cr_trace_size_val": self.ui.tools2_defaults_form.tools2_checkrules_group.trace_size_entry,
+            "tools_cr_c2c": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_copper2copper_cb,
+            "tools_cr_c2c_val": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_copper2copper_entry,
+            "tools_cr_c2o": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_copper2ol_cb,
+            "tools_cr_c2o_val": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_copper2ol_entry,
+            "tools_cr_s2s": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_silk2silk_cb,
+            "tools_cr_s2s_val": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_silk2silk_entry,
+            "tools_cr_s2sm": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_silk2sm_cb,
+            "tools_cr_s2sm_val": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_silk2sm_entry,
+            "tools_cr_s2o": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_silk2ol_cb,
+            "tools_cr_s2o_val": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_silk2ol_entry,
+            "tools_cr_sm2sm": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_sm2sm_cb,
+            "tools_cr_sm2sm_val": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_sm2sm_entry,
+            "tools_cr_ri": self.ui.tools2_defaults_form.tools2_checkrules_group.ring_integrity_cb,
+            "tools_cr_ri_val": self.ui.tools2_defaults_form.tools2_checkrules_group.ring_integrity_entry,
+            "tools_cr_h2h": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_d2d_cb,
+            "tools_cr_h2h_val": self.ui.tools2_defaults_form.tools2_checkrules_group.clearance_d2d_entry,
+            "tools_cr_dh": self.ui.tools2_defaults_form.tools2_checkrules_group.drill_size_cb,
+            "tools_cr_dh_val": self.ui.tools2_defaults_form.tools2_checkrules_group.drill_size_entry,
+
+            # QRCode Tool
+            "tools_qrcode_version": self.ui.tools2_defaults_form.tools2_qrcode_group.version_entry,
+            "tools_qrcode_error": self.ui.tools2_defaults_form.tools2_qrcode_group.error_radio,
+            "tools_qrcode_box_size": self.ui.tools2_defaults_form.tools2_qrcode_group.bsize_entry,
+            "tools_qrcode_border_size": self.ui.tools2_defaults_form.tools2_qrcode_group.border_size_entry,
+            "tools_qrcode_qrdata": self.ui.tools2_defaults_form.tools2_qrcode_group.text_data,
+            "tools_qrcode_polarity": self.ui.tools2_defaults_form.tools2_qrcode_group.pol_radio,
+            "tools_qrcode_rounded": self.ui.tools2_defaults_form.tools2_qrcode_group.bb_radio,
+            "tools_qrcode_fill_color": self.ui.tools2_defaults_form.tools2_qrcode_group.fill_color_entry,
+            "tools_qrcode_back_color": self.ui.tools2_defaults_form.tools2_qrcode_group.back_color_entry,
+            "tools_qrcode_sel_limit": self.ui.tools2_defaults_form.tools2_qrcode_group.sel_limit_entry,
+
+            # Copper Thieving Tool
+            "tools_copper_thieving_clearance": self.ui.tools2_defaults_form.tools2_cfill_group.clearance_entry,
+            "tools_copper_thieving_margin": self.ui.tools2_defaults_form.tools2_cfill_group.margin_entry,
+            "tools_copper_thieving_area": self.ui.tools2_defaults_form.tools2_cfill_group.area_entry,
+            "tools_copper_thieving_reference": self.ui.tools2_defaults_form.tools2_cfill_group.reference_radio,
+            "tools_copper_thieving_box_type": self.ui.tools2_defaults_form.tools2_cfill_group.bbox_type_radio,
+            "tools_copper_thieving_circle_steps": self.ui.tools2_defaults_form.tools2_cfill_group.circlesteps_entry,
+            "tools_copper_thieving_fill_type": self.ui.tools2_defaults_form.tools2_cfill_group.fill_type_radio,
+            "tools_copper_thieving_dots_dia": self.ui.tools2_defaults_form.tools2_cfill_group.dot_dia_entry,
+            "tools_copper_thieving_dots_spacing": self.ui.tools2_defaults_form.tools2_cfill_group.dot_spacing_entry,
+            "tools_copper_thieving_squares_size": self.ui.tools2_defaults_form.tools2_cfill_group.square_size_entry,
+            "tools_copper_thieving_squares_spacing":
+                self.ui.tools2_defaults_form.tools2_cfill_group.squares_spacing_entry,
+            "tools_copper_thieving_lines_size": self.ui.tools2_defaults_form.tools2_cfill_group.line_size_entry,
+            "tools_copper_thieving_lines_spacing": self.ui.tools2_defaults_form.tools2_cfill_group.lines_spacing_entry,
+            "tools_copper_thieving_rb_margin": self.ui.tools2_defaults_form.tools2_cfill_group.rb_margin_entry,
+            "tools_copper_thieving_rb_thickness": self.ui.tools2_defaults_form.tools2_cfill_group.rb_thickness_entry,
+            "tools_copper_thieving_mask_clearance": self.ui.tools2_defaults_form.tools2_cfill_group.clearance_ppm_entry,
+            "tools_copper_thieving_geo_choice": self.ui.tools2_defaults_form.tools2_cfill_group.ppm_choice_radio,
+
+            # Fiducials Tool
+            "tools_fiducials_dia": self.ui.tools2_defaults_form.tools2_fiducials_group.dia_entry,
+            "tools_fiducials_margin": self.ui.tools2_defaults_form.tools2_fiducials_group.margin_entry,
+            "tools_fiducials_mode": self.ui.tools2_defaults_form.tools2_fiducials_group.mode_radio,
+            "tools_fiducials_second_pos": self.ui.tools2_defaults_form.tools2_fiducials_group.pos_radio,
+            "tools_fiducials_type": self.ui.tools2_defaults_form.tools2_fiducials_group.fid_type_radio,
+            "tools_fiducials_line_thickness": self.ui.tools2_defaults_form.tools2_fiducials_group.line_thickness_entry,
+
+            # Calibration Tool
+            "tools_cal_calsource": self.ui.tools2_defaults_form.tools2_cal_group.cal_source_radio,
+            "tools_cal_travelz": self.ui.tools2_defaults_form.tools2_cal_group.travelz_entry,
+            "tools_cal_verz": self.ui.tools2_defaults_form.tools2_cal_group.verz_entry,
+            "tools_cal_zeroz": self.ui.tools2_defaults_form.tools2_cal_group.zeroz_cb,
+            "tools_cal_toolchangez": self.ui.tools2_defaults_form.tools2_cal_group.toolchangez_entry,
+            "tools_cal_toolchange_xy": self.ui.tools2_defaults_form.tools2_cal_group.toolchange_xy_entry,
+            "tools_cal_sec_point": self.ui.tools2_defaults_form.tools2_cal_group.second_point_radio,
+
+            # Extract Drills Tool
+            "tools_edrills_hole_type": self.ui.tools2_defaults_form.tools2_edrills_group.hole_size_radio,
+            "tools_edrills_hole_fixed_dia": self.ui.tools2_defaults_form.tools2_edrills_group.dia_entry,
+            "tools_edrills_hole_prop_factor": self.ui.tools2_defaults_form.tools2_edrills_group.factor_entry,
+            "tools_edrills_circular_ring": self.ui.tools2_defaults_form.tools2_edrills_group.circular_ring_entry,
+            "tools_edrills_oblong_ring": self.ui.tools2_defaults_form.tools2_edrills_group.oblong_ring_entry,
+            "tools_edrills_square_ring": self.ui.tools2_defaults_form.tools2_edrills_group.square_ring_entry,
+            "tools_edrills_rectangular_ring": self.ui.tools2_defaults_form.tools2_edrills_group.rectangular_ring_entry,
+            "tools_edrills_others_ring": self.ui.tools2_defaults_form.tools2_edrills_group.other_ring_entry,
+            "tools_edrills_circular": self.ui.tools2_defaults_form.tools2_edrills_group.circular_cb,
+            "tools_edrills_oblong": self.ui.tools2_defaults_form.tools2_edrills_group.oblong_cb,
+            "tools_edrills_square": self.ui.tools2_defaults_form.tools2_edrills_group.square_cb,
+            "tools_edrills_rectangular": self.ui.tools2_defaults_form.tools2_edrills_group.rectangular_cb,
+            "tools_edrills_others": self.ui.tools2_defaults_form.tools2_edrills_group.other_cb,
+
+            # Punch Gerber Tool
+            "tools_punch_hole_type": self.ui.tools2_defaults_form.tools2_punch_group.hole_size_radio,
+            "tools_punch_hole_fixed_dia": self.ui.tools2_defaults_form.tools2_punch_group.dia_entry,
+            "tools_punch_hole_prop_factor": self.ui.tools2_defaults_form.tools2_punch_group.factor_entry,
+            "tools_punch_circular_ring": self.ui.tools2_defaults_form.tools2_punch_group.circular_ring_entry,
+            "tools_punch_oblong_ring": self.ui.tools2_defaults_form.tools2_punch_group.oblong_ring_entry,
+            "tools_punch_square_ring": self.ui.tools2_defaults_form.tools2_punch_group.square_ring_entry,
+            "tools_punch_rectangular_ring": self.ui.tools2_defaults_form.tools2_punch_group.rectangular_ring_entry,
+            "tools_punch_others_ring": self.ui.tools2_defaults_form.tools2_punch_group.other_ring_entry,
+            "tools_punch_circular": self.ui.tools2_defaults_form.tools2_punch_group.circular_cb,
+            "tools_punch_oblong": self.ui.tools2_defaults_form.tools2_punch_group.oblong_cb,
+            "tools_punch_square": self.ui.tools2_defaults_form.tools2_punch_group.square_cb,
+            "tools_punch_rectangular": self.ui.tools2_defaults_form.tools2_punch_group.rectangular_cb,
+            "tools_punch_others": self.ui.tools2_defaults_form.tools2_punch_group.other_cb,
+
+            # Invert Gerber Tool
+            "tools_invert_margin": self.ui.tools2_defaults_form.tools2_invert_group.margin_entry,
+            "tools_invert_join_style": self.ui.tools2_defaults_form.tools2_invert_group.join_radio,
+
+            # Utilities
+            # File associations
+            "fa_excellon": self.ui.util_defaults_form.fa_excellon_group.exc_list_text,
+            "fa_gcode": self.ui.util_defaults_form.fa_gcode_group.gco_list_text,
+            # "fa_geometry": self.ui.util_defaults_form.fa_geometry_group.close_paths_cb,
+            "fa_gerber": self.ui.util_defaults_form.fa_gerber_group.grb_list_text,
+            "util_autocomplete_keywords": self.ui.util_defaults_form.kw_group.kw_list_text,
+
+        }
+
+    def defaults_read_form(self):
+        """
+        Will read all the values in the Preferences GUI and update the defaults dictionary.
+
+        :return: None
+        """
+        for option in self.defaults_form_fields:
+            try:
+                self.defaults[option] = self.defaults_form_fields[option].get_value()
+            except Exception as e:
+                log.debug("App.defaults_read_form() --> %s" % str(e))
+
+    def defaults_write_form(self, factor=None, fl_units=None, source_dict=None):
+        """
+        Will set the values for all the GUI elements in Preferences GUI based on the values found in the
+        self.defaults dictionary.
+
+        :param factor:          will apply a factor to the values that written in the GUI elements
+        :param fl_units:        current measuring units in FlatCAM: Metric or Inch
+        :param source_dict:     the repository of options, usually is the self.defaults
+        :return: None
+        """
+
+        options_storage = self.defaults if source_dict is None else source_dict
+
+        for option in options_storage:
+            if source_dict:
+                self.defaults_write_form_field(option, factor=factor, units=fl_units, defaults_dict=source_dict)
+            else:
+                self.defaults_write_form_field(option, factor=factor, units=fl_units)
+
+    def defaults_write_form_field(self, field, factor=None, units=None, defaults_dict=None):
+        """
+        Basically it is the worker in the self.defaults_write_form()
+
+        :param field:           the GUI element in Preferences GUI to be updated
+        :param factor:          factor to be applied to the field parameter
+        :param units:           current FlatCAM measuring units
+        :param defaults_dict:   the defaults storage
+        :return:                None, it updates GUI elements
+        """
+
+        def_dict = self.defaults if defaults_dict is None else defaults_dict
+
+        try:
+            value = def_dict[field]
+            # log.debug("value is " + str(value) + " and factor is "+str(factor))
+            if factor is not None:
+                value *= factor
+
+            form_field = self.defaults_form_fields[field]
+            if units is None:
+                form_field.set_value(value)
+            elif (units == 'IN' or units == 'MM') and (field == 'global_gridx' or field == 'global_gridy'):
+                form_field.set_value(value)
+
+        except KeyError:
+            pass
+        except AttributeError:
+            log.debug(field)
+
+    def show_preferences_gui(self):
+        """
+        Called to initialize and show the Preferences appGUI
+
+        :return: None
+        """
+
+        gen_form = self.ui.general_defaults_form
+        try:
+            self.ui.general_scroll_area.takeWidget()
+        except Exception:
+            log.debug("Nothing to remove")
+        self.ui.general_scroll_area.setWidget(gen_form)
+        gen_form.show()
+
+        ger_form = self.ui.gerber_defaults_form
+        try:
+            self.ui.gerber_scroll_area.takeWidget()
+        except Exception:
+            log.debug("Nothing to remove")
+        self.ui.gerber_scroll_area.setWidget(ger_form)
+        ger_form.show()
+
+        exc_form = self.ui.excellon_defaults_form
+        try:
+            self.ui.excellon_scroll_area.takeWidget()
+        except Exception:
+            log.debug("Nothing to remove")
+        self.ui.excellon_scroll_area.setWidget(exc_form)
+        exc_form.show()
+
+        geo_form = self.ui.geometry_defaults_form
+        try:
+            self.ui.geometry_scroll_area.takeWidget()
+        except Exception:
+            log.debug("Nothing to remove")
+        self.ui.geometry_scroll_area.setWidget(geo_form)
+        geo_form.show()
+
+        cnc_form = self.ui.cncjob_defaults_form
+        try:
+            self.ui.cncjob_scroll_area.takeWidget()
+        except Exception:
+            log.debug("Nothing to remove")
+        self.ui.cncjob_scroll_area.setWidget(cnc_form)
+        cnc_form.show()
+
+        tools_form = self.ui.tools_defaults_form
+        try:
+            self.ui.tools_scroll_area.takeWidget()
+        except Exception:
+            log.debug("Nothing to remove")
+        self.ui.tools_scroll_area.setWidget(tools_form)
+        tools_form.show()
+
+        tools2_form = self.ui.tools2_defaults_form
+        try:
+            self.ui.tools2_scroll_area.takeWidget()
+        except Exception:
+            log.debug("Nothing to remove")
+        self.ui.tools2_scroll_area.setWidget(tools2_form)
+        tools2_form.show()
+
+        fa_form = self.ui.util_defaults_form
+        try:
+            self.ui.fa_scroll_area.takeWidget()
+        except Exception:
+            log.debug("Nothing to remove")
+        self.ui.fa_scroll_area.setWidget(fa_form)
+        fa_form.show()
+
+        # Initialize the color box's color in Preferences -> Global -> Colors
+        self.__init_color_pickers()
+
+        # Button handlers
+        self.ui.pref_save_button.clicked.connect(lambda: self.on_save_button(save_to_file=True))
+        self.ui.pref_apply_button.clicked.connect(lambda: self.on_save_button(save_to_file=False))
+        self.ui.pref_close_button.clicked.connect(self.on_pref_close_button)
+        self.ui.pref_defaults_button.clicked.connect(self.on_restore_defaults_preferences)
+
+        log.debug("Finished Preferences GUI form initialization.")
+
+    def __init_color_pickers(self):
+        # Init Gerber Plot Colors
+        self.ui.gerber_defaults_form.gerber_gen_group.fill_color_entry.set_value(self.defaults['gerber_plot_fill'])
+        self.ui.gerber_defaults_form.gerber_gen_group.line_color_entry.set_value(self.defaults['gerber_plot_line'])
+
+        self.ui.gerber_defaults_form.gerber_gen_group.gerber_alpha_entry.set_value(
+            int(self.defaults['gerber_plot_fill'][7:9], 16))    # alpha
+
+        # Init Excellon Plot Colors
+        self.ui.excellon_defaults_form.excellon_gen_group.fill_color_entry.set_value(
+            self.defaults['excellon_plot_fill'])
+        self.ui.excellon_defaults_form.excellon_gen_group.line_color_entry.set_value(
+            self.defaults['excellon_plot_line'])
+
+        self.ui.excellon_defaults_form.excellon_gen_group.excellon_alpha_entry.set_value(
+            int(self.defaults['excellon_plot_fill'][7:9], 16))
+
+        # Init Geometry Plot Colors
+        self.ui.geometry_defaults_form.geometry_gen_group.line_color_entry.set_value(
+            self.defaults['geometry_plot_line'])
+
+        # Init CNCJob Travel Line Colors
+        self.ui.cncjob_defaults_form.cncjob_gen_group.tfill_color_entry.set_value(
+            self.defaults['cncjob_travel_fill'])
+        self.ui.cncjob_defaults_form.cncjob_gen_group.tline_color_entry.set_value(
+            self.defaults['cncjob_travel_line'])
+
+        self.ui.cncjob_defaults_form.cncjob_gen_group.cncjob_alpha_entry.set_value(
+            int(self.defaults['cncjob_travel_fill'][7:9], 16))      # alpha
+
+        # Init CNCJob Plot Colors
+        self.ui.cncjob_defaults_form.cncjob_gen_group.fill_color_entry.set_value(
+            self.defaults['cncjob_plot_fill'])
+
+        self.ui.cncjob_defaults_form.cncjob_gen_group.line_color_entry.set_value(
+            self.defaults['cncjob_plot_line'])
+
+        # Init Left-Right Selection colors
+        self.ui.general_defaults_form.general_gui_group.sf_color_entry.set_value(self.defaults['global_sel_fill'])
+        self.ui.general_defaults_form.general_gui_group.sl_color_entry.set_value(self.defaults['global_sel_line'])
+
+        self.ui.general_defaults_form.general_gui_group.left_right_alpha_entry.set_value(
+            int(self.defaults['global_sel_fill'][7:9], 16))
+
+        # Init Right-Left Selection colors
+        self.ui.general_defaults_form.general_gui_group.alt_sf_color_entry.set_value(
+            self.defaults['global_alt_sel_fill'])
+        self.ui.general_defaults_form.general_gui_group.alt_sl_color_entry.set_value(
+            self.defaults['global_alt_sel_line'])
+
+        self.ui.general_defaults_form.general_gui_group.right_left_alpha_entry.set_value(
+            int(self.defaults['global_sel_fill'][7:9], 16))
+
+        # Init Draw color and Selection Draw Color
+        self.ui.general_defaults_form.general_gui_group.draw_color_entry.set_value(
+            self.defaults['global_draw_color'])
+
+        self.ui.general_defaults_form.general_gui_group.sel_draw_color_entry.set_value(
+            self.defaults['global_sel_draw_color'])
+
+        # Init Project Items color
+        self.ui.general_defaults_form.general_gui_group.proj_color_entry.set_value(
+            self.defaults['global_proj_item_color'])
+
+        # Init Project Disabled Items color
+        self.ui.general_defaults_form.general_gui_group.proj_color_dis_entry.set_value(
+            self.defaults['global_proj_item_dis_color'])
+
+        # Init Mouse Cursor color
+        self.ui.general_defaults_form.general_app_set_group.mouse_cursor_entry.set_value(
+            self.defaults['global_cursor_color'])
+
+        # Init the Annotation CNC Job color
+        self.ui.cncjob_defaults_form.cncjob_adv_opt_group.annotation_fontcolor_entry.set_value(
+            self.defaults['cncjob_annotation_fontcolor'])
+
+        # Init the Tool Film color
+        self.ui.tools_defaults_form.tools_film_group.film_color_entry.set_value(
+            self.defaults['tools_film_color'])
+
+        # Init the Tool QRCode colors
+        self.ui.tools2_defaults_form.tools2_qrcode_group.fill_color_entry.set_value(
+            self.defaults['tools_qrcode_fill_color'])
+
+        self.ui.tools2_defaults_form.tools2_qrcode_group.back_color_entry.set_value(
+            self.defaults['tools_qrcode_back_color'])
+
+    def on_save_button(self, save_to_file=True):
+        log.debug("on_save_button() --> Applying preferences to file.")
+
+        # Preferences saved, update flag
+        self.preferences_changed_flag = False
+
+        # Preferences save, update the color of the Preferences Tab text
+        for idx in range(self.ui.plot_tab_area.count()):
+            if self.ui.plot_tab_area.tabText(idx) == _("Preferences"):
+                self.ui.plot_tab_area.tabBar.setTabTextColor(idx, self.old_color)
+
+        # restore the default stylesheet by setting a blank one
+        self.ui.pref_apply_button.setStyleSheet("")
+        self.ui.pref_apply_button.setIcon(QtGui.QIcon(self.ui.app.resource_location + '/apply32.png'))
+
+        self.inform.emit('%s' % _("Preferences applied."))
+
+        # make sure we update the self.current_defaults dict used to undo changes to self.defaults
+        self.defaults.current_defaults.update(self.defaults)
+
+        # deal with theme change
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if theme_settings.contains("theme"):
+            theme = theme_settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        should_restart = False
+        theme_new_val = self.ui.general_defaults_form.general_gui_group.theme_radio.get_value()
+
+        ge = self.defaults["global_graphic_engine"]
+        ge_val = self.ui.general_defaults_form.general_app_group.ge_radio.get_value()
+
+        if theme_new_val != theme or ge != ge_val:
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setText(_("Are you sure you want to continue?"))
+            msgbox.setWindowTitle(_("Application will restart"))
+            msgbox.setWindowIcon(QtGui.QIcon(self.ui.app.resource_location + '/warning.png'))
+            msgbox.setIcon(QtWidgets.QMessageBox.Question)
+
+            bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.YesRole)
+            msgbox.addButton(_('Cancel'), QtWidgets.QMessageBox.NoRole)
+
+            msgbox.setDefaultButton(bt_yes)
+            msgbox.exec_()
+            response = msgbox.clickedButton()
+
+            if theme_new_val != theme:
+                if response == bt_yes:
+                    theme_settings.setValue('theme', theme_new_val)
+
+                    # This will write the setting to the platform specific storage.
+                    del theme_settings
+
+                    should_restart = True
+                else:
+                    self.ui.general_defaults_form.general_gui_group.theme_radio.set_value(theme)
+            else:
+                if response == bt_yes:
+                    self.defaults["global_graphic_engine"] = ge_val
+                    should_restart = True
+                else:
+                    self.ui.general_defaults_form.general_app_group.ge_radio.set_value(ge)
+
+        if save_to_file or should_restart is True:
+            # Re-fresh project options
+            self.ui.app.on_options_app2project()
+
+            self.save_defaults(silent=False)
+            # load the defaults so they are updated into the app
+            self.defaults.load(filename=os.path.join(self.data_path, 'current_defaults.FlatConfig'), inform=self.inform)
+
+        settgs = QSettings("Open Source", "FlatCAM")
+
+        # save the notebook font size
+        fsize = self.ui.general_defaults_form.general_app_set_group.notebook_font_size_spinner.get_value()
+        settgs.setValue('notebook_font_size', fsize)
+
+        # save the axis font size
+        g_fsize = self.ui.general_defaults_form.general_app_set_group.axis_font_size_spinner.get_value()
+        settgs.setValue('axis_font_size', g_fsize)
+
+        # save the textbox font size
+        tb_fsize = self.ui.general_defaults_form.general_app_set_group.textbox_font_size_spinner.get_value()
+        settgs.setValue('textbox_font_size', tb_fsize)
+
+        # save the HUD font size
+        hud_fsize = self.ui.general_defaults_form.general_app_set_group.hud_font_size_spinner.get_value()
+        settgs.setValue('hud_font_size', hud_fsize)
+
+        settgs.setValue(
+            'machinist',
+            1 if self.ui.general_defaults_form.general_app_set_group.machinist_cb.get_value() else 0
+        )
+
+        # This will write the setting to the platform specific storage.
+        del settgs
+
+        if save_to_file:
+            # close the tab and delete it
+            for idx in range(self.ui.plot_tab_area.count()):
+                if self.ui.plot_tab_area.tabText(idx) == _("Preferences"):
+                    self.ui.plot_tab_area.tabBar.setTabTextColor(idx, self.old_color)
+                    self.ui.plot_tab_area.closeTab(idx)
+                    break
+
+        if should_restart is True:
+            self.ui.app.on_app_restart()
+
+    def on_pref_close_button(self):
+        # Preferences saved, update flag
+        self.preferences_changed_flag = False
+        self.ignore_tab_close_event = True
+
+        # restore stylesheet to default for the statusBar icon
+        self.ui.pref_status_label.setStyleSheet("")
+
+        try:
+            self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
+        self.defaults_write_form(source_dict=self.defaults.current_defaults)
+        self.ui.general_defaults_form.general_app_group.units_radio.activated_custom.connect(
+            lambda: self.ui.app.on_toggle_units(no_pref=False))
+        self.defaults.update(self.defaults.current_defaults)
+
+        # Preferences save, update the color of the Preferences Tab text
+        for idx in range(self.ui.plot_tab_area.count()):
+            if self.ui.plot_tab_area.tabText(idx) == _("Preferences"):
+                self.ui.plot_tab_area.tabBar.setTabTextColor(idx, self.old_color)
+                self.ui.plot_tab_area.closeTab(idx)
+                break
+
+        self.inform.emit('%s' % _("Preferences closed without saving."))
+        self.ignore_tab_close_event = False
+
+    def on_restore_defaults_preferences(self):
+        """
+        Loads the application's factory default settings into ``self.defaults``.
+
+        :return: None
+        """
+        log.debug("on_restore_defaults_preferences()")
+        self.defaults.reset_to_factory_defaults()
+        self.on_preferences_edited()
+        self.inform.emit('[success] %s' % _("Preferences default values are restored."))
+
+    def save_defaults(self, silent=False, data_path=None, first_time=False):
+        """
+        Saves application default options
+        ``self.defaults`` to current_defaults.FlatConfig file.
+        Save the toolbars visibility status to the preferences file (current_defaults.FlatConfig) to be
+        used at the next launch of the application.
+
+        :param silent:      Whether to display a message in status bar or not; boolean
+        :param data_path:   The path where to save the preferences file (current_defaults.FlatConfig)
+        When the application is portable it should be a mobile location.
+        :param first_time:  Boolean. If True will execute some code when the app is run first time
+        :return:            None
+        """
+        log.debug("App.PreferencesUIManager.save_defaults()")
+
+        if data_path is None:
+            data_path = self.data_path
+
+        self.defaults.propagate_defaults()
+
+        if first_time is False:
+            self.save_toolbar_view()
+
+        # Save the options to disk
+        filename = os.path.join(data_path, "current_defaults.FlatConfig")
+        try:
+            self.defaults.write(filename=filename)
+        except Exception as e:
+            log.error("save_defaults() --> Failed to write defaults to file %s" % str(e))
+            self.inform.emit('[ERROR_NOTCL] %s %s' % (_("Failed to write defaults to file."), str(filename)))
+            return
+
+        if not silent:
+            self.inform.emit('[success] %s' % _("Preferences saved."))
+
+        # update the autosave timer
+        self.ui.app.save_project_auto_update()
+
+    def save_toolbar_view(self):
+        """
+        Will save the toolbar view state to the defaults
+
+        :return:            None
+        """
+
+        # Save the toolbar view
+        tb_status = 0
+        if self.ui.toolbarfile.isVisible():
+            tb_status += 1
+
+        if self.ui.toolbaredit.isVisible():
+            tb_status += 2
+
+        if self.ui.toolbarview.isVisible():
+            tb_status += 4
+
+        if self.ui.toolbartools.isVisible():
+            tb_status += 8
+
+        if self.ui.exc_edit_toolbar.isVisible():
+            tb_status += 16
+
+        if self.ui.geo_edit_toolbar.isVisible():
+            tb_status += 32
+
+        if self.ui.grb_edit_toolbar.isVisible():
+            tb_status += 64
+
+        if self.ui.status_toolbar.isVisible():
+            tb_status += 128
+
+        if self.ui.toolbarshell.isVisible():
+            tb_status += 256
+
+        self.defaults["global_toolbar_view"] = tb_status
+
+    def on_preferences_edited(self):
+        """
+        Executed when a preference was changed in the Edit -> Preferences tab.
+        Will color the Preferences tab text to Red color.
+        :return:
+        """
+        if self.preferences_changed_flag is False:
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Preferences edited but not saved."))
+
+            for idx in range(self.ui.plot_tab_area.count()):
+                if self.ui.plot_tab_area.tabText(idx) == _("Preferences"):
+                    self.old_color = self.ui.plot_tab_area.tabBar.tabTextColor(idx)
+                    self.ui.plot_tab_area.tabBar.setTabTextColor(idx, QtGui.QColor('red'))
+
+            self.ui.pref_apply_button.setStyleSheet("QPushButton {color: red;}")
+            self.ui.pref_apply_button.setIcon(QtGui.QIcon(self.ui.app.resource_location + '/apply_red32.png'))
+
+            self.preferences_changed_flag = True
+
+    def on_close_preferences_tab(self):
+        if self.ignore_tab_close_event:
+            return
+
+        # restore stylesheet to default for the statusBar icon
+        self.ui.pref_status_label.setStyleSheet("")
+
+        # disconnect
+        for idx in range(self.ui.pref_tab_area.count()):
+            for tb in self.ui.pref_tab_area.widget(idx).findChildren(QtCore.QObject):
+                try:
+                    tb.textEdited.disconnect(self.on_preferences_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+                try:
+                    tb.modificationChanged.disconnect(self.on_preferences_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+                try:
+                    tb.toggled.disconnect(self.on_preferences_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+                try:
+                    tb.valueChanged.disconnect(self.on_preferences_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+                try:
+                    tb.currentIndexChanged.disconnect(self.on_preferences_edited)
+                except (TypeError, AttributeError):
+                    pass
+
+        # Prompt user to save
+        if self.preferences_changed_flag is True:
+            msgbox = QtWidgets.QMessageBox()
+            msgbox.setText(_("One or more values are changed.\n"
+                             "Do you want to save the Preferences?"))
+            msgbox.setWindowTitle(_("Save Preferences"))
+            msgbox.setWindowIcon(QtGui.QIcon(self.ui.app.resource_location + '/save_as.png'))
+            msgbox.setIcon(QtWidgets.QMessageBox.Question)
+
+            bt_yes = msgbox.addButton(_('Yes'), QtWidgets.QMessageBox.YesRole)
+            msgbox.addButton(_('No'), QtWidgets.QMessageBox.NoRole)
+
+            msgbox.setDefaultButton(bt_yes)
+            msgbox.exec_()
+            response = msgbox.clickedButton()
+
+            if response == bt_yes:
+                self.on_save_button(save_to_file=True)
+                self.inform.emit('[success] %s' % _("Preferences saved."))
+            else:
+                self.preferences_changed_flag = False
+                self.inform.emit('')
+                return

+ 16 - 0
appGUI/preferences/__init__.py

@@ -0,0 +1,16 @@
+from appGUI.GUIElements import *
+from PyQt5.QtCore import QSettings
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0

+ 215 - 0
appGUI/preferences/cncjob/CNCJobAdvOptPrefGroupUI.py

@@ -0,0 +1,215 @@
+from PyQt5 import QtWidgets, QtGui
+from PyQt5.QtCore import QSettings, Qt
+
+from appGUI.GUIElements import FCComboBox, FCSpinner, FCColorEntry, FCLabel, FCDoubleSpinner, RadioSet
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class CNCJobAdvOptPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "CNC Job Advanced Options Preferences", parent=None)
+        super(CNCJobAdvOptPrefGroupUI, self).__init__(self, parent=parent)
+        self.decimals = decimals
+
+        self.setTitle(str(_("CNC Job Adv. Options")))
+
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
+        # ## Export G-Code
+        self.export_gcode_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.export_gcode_label.setToolTip(
+            _("Export and save G-Code to\n"
+              "make this object to a file.")
+        )
+        grid0.addWidget(self.export_gcode_label, 0, 0, 1, 2)
+
+        # Annotation Font Size
+        self.annotation_fontsize_label = QtWidgets.QLabel('%s:' % _("Annotation Size"))
+        self.annotation_fontsize_label.setToolTip(
+            _("The font size of the annotation text. In pixels.")
+        )
+        self.annotation_fontsize_sp = FCSpinner()
+        self.annotation_fontsize_sp.set_range(0, 9999)
+
+        grid0.addWidget(self.annotation_fontsize_label, 2, 0)
+        grid0.addWidget(self.annotation_fontsize_sp, 2, 1)
+
+        # Annotation Font Color
+        self.annotation_color_label = QtWidgets.QLabel('%s:' % _('Annotation Color'))
+        self.annotation_color_label.setToolTip(
+            _("Set the font color for the annotation texts.")
+        )
+        self.annotation_fontcolor_entry = FCColorEntry()
+
+        grid0.addWidget(self.annotation_color_label, 4, 0)
+        grid0.addWidget(self.annotation_fontcolor_entry, 4, 1)
+
+        # ## Autolevelling
+        self.autolevelling_gcode_label = QtWidgets.QLabel("<b>%s</b>" % _("Autolevelling"))
+        self.autolevelling_gcode_label.setToolTip(
+            _("Parameters for the autolevelling.")
+        )
+        grid0.addWidget(self.autolevelling_gcode_label, 6, 0, 1, 2)
+
+        # Probe points mode
+        al_mode_lbl = FCLabel('%s:' % _("Mode"))
+        al_mode_lbl.setToolTip(_("Choose a mode for height map generation.\n"
+                                 "- Manual: will pick a selection of probe points by clicking on canvas\n"
+                                 "- Grid: will automatically generate a grid of probe points"))
+
+        self.al_mode_radio = RadioSet(
+            [
+                {'label': _('Manual'), 'value': 'manual'},
+                {'label': _('Grid'), 'value': 'grid'}
+            ])
+        grid0.addWidget(al_mode_lbl, 8, 0)
+        grid0.addWidget(self.al_mode_radio, 8, 1)
+
+        # AUTOLEVELL METHOD
+        self.al_method_lbl = FCLabel('%s:' % _("Method"))
+        self.al_method_lbl.setToolTip(_("Choose a method for approximation of heights from autolevelling data.\n"
+                                        "- Voronoi: will generate a Voronoi diagram\n"
+                                        "- Bilinear: will use bilinear interpolation. Usable only for grid mode."))
+
+        self.al_method_radio = RadioSet(
+            [
+                {'label': _('Voronoi'), 'value': 'v'},
+                {'label': _('Bilinear'), 'value': 'b'}
+            ])
+        grid0.addWidget(self.al_method_lbl, 9, 0)
+        grid0.addWidget(self.al_method_radio, 9, 1)
+
+        # ## Columns
+        self.al_columns_entry = FCSpinner()
+
+        self.al_columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
+        self.al_columns_label.setToolTip(
+            _("The number of grid columns.")
+        )
+        grid0.addWidget(self.al_columns_label, 10, 0)
+        grid0.addWidget(self.al_columns_entry, 10, 1)
+
+        # ## Rows
+        self.al_rows_entry = FCSpinner()
+
+        self.al_rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
+        self.al_rows_label.setToolTip(
+            _("The number of grid rows.")
+        )
+        grid0.addWidget(self.al_rows_label, 12, 0)
+        grid0.addWidget(self.al_rows_entry, 12, 1)
+
+        # Travel Z Probe
+        self.ptravelz_label = QtWidgets.QLabel('%s:' % _("Probe Z travel"))
+        self.ptravelz_label.setToolTip(
+            _("The safe Z for probe travelling between probe points.")
+        )
+        self.ptravelz_entry = FCDoubleSpinner()
+        self.ptravelz_entry.set_precision(self.decimals)
+        self.ptravelz_entry.set_range(0.0000, 10000.0000)
+
+        grid0.addWidget(self.ptravelz_label, 14, 0)
+        grid0.addWidget(self.ptravelz_entry, 14, 1)
+
+        # Probe depth
+        self.pdepth_label = QtWidgets.QLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label.setToolTip(
+            _("The maximum depth that the probe is allowed\n"
+              "to probe. Negative value, in current units.")
+        )
+        self.pdepth_entry = FCDoubleSpinner()
+        self.pdepth_entry.set_precision(self.decimals)
+        self.pdepth_entry.set_range(-910000.0000, 0.0000)
+
+        grid0.addWidget(self.pdepth_label, 16, 0)
+        grid0.addWidget(self.pdepth_entry, 16, 1)
+
+        # Probe feedrate
+        self.feedrate_probe_label = QtWidgets.QLabel('%s:' % _("Probe Feedrate"))
+        self.feedrate_probe_label.setToolTip(
+            _("The feedrate used while the probe is probing.")
+        )
+        self.feedrate_probe_entry = FCDoubleSpinner()
+        self.feedrate_probe_entry.set_precision(self.decimals)
+        self.feedrate_probe_entry.set_range(0, 910000.0000)
+
+        grid0.addWidget(self.feedrate_probe_label, 18, 0)
+        grid0.addWidget(self.feedrate_probe_entry, 18, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 20, 0, 1, 2)
+
+        self.al_controller_label = FCLabel('%s:' % _("Controller"))
+        self.al_controller_label.setToolTip(
+            _("The kind of controller for which to generate\n"
+              "height map gcode.")
+        )
+
+        self.al_controller_combo = FCComboBox()
+        self.al_controller_combo.addItems(["MACH3", "MACH4", "LinuxCNC", "GRBL"])
+        grid0.addWidget(self.al_controller_label, 22, 0)
+        grid0.addWidget(self.al_controller_combo, 22, 1)
+
+        # JOG Step
+        self.jog_step_label = FCLabel('%s:' % _("Step"))
+        self.jog_step_label.setToolTip(
+            _("Each jog action will move the axes with this value.")
+        )
+
+        self.jog_step_entry = FCDoubleSpinner()
+        self.jog_step_entry.set_precision(self.decimals)
+        self.jog_step_entry.set_range(0, 910000.0000)
+
+        grid0.addWidget(self.jog_step_label, 24, 0)
+        grid0.addWidget(self.jog_step_entry, 24, 1)
+
+        # JOG Feedrate
+        self.jog_fr_label = FCLabel('%s:' % _("Feedrate"))
+        self.jog_fr_label.setToolTip(
+            _("Feedrate when jogging.")
+        )
+
+        self.jog_fr_entry = FCDoubleSpinner()
+        self.jog_fr_entry.set_precision(self.decimals)
+        self.jog_fr_entry.set_range(0, 910000.0000)
+
+        grid0.addWidget(self.jog_fr_label, 26, 0)
+        grid0.addWidget(self.jog_fr_entry, 26, 1)
+
+        # JOG Travel Z
+        self.jog_travelz_label = FCLabel('%s:' % _("Travel Z"))
+        self.jog_travelz_label.setToolTip(
+            _("Safe height (Z) distance when jogging to origin.")
+        )
+
+        self.jog_travelz_entry = FCDoubleSpinner()
+        self.jog_travelz_entry.set_precision(self.decimals)
+        self.jog_travelz_entry.set_range(0, 910000.0000)
+
+        grid0.addWidget(self.jog_travelz_label, 28, 0)
+        grid0.addWidget(self.jog_travelz_entry, 28, 1)
+
+        self.layout.addStretch()
+
+        self.annotation_fontcolor_entry.editingFinished.connect(self.on_annotation_fontcolor_entry)
+
+    def on_annotation_fontcolor_entry(self):
+        self.app.defaults['cncjob_annotation_fontcolor'] = self.annotation_fontcolor_entry.get_value()

+ 79 - 0
appGUI/preferences/cncjob/CNCJobEditorPrefGroupUI.py

@@ -0,0 +1,79 @@
+from PyQt5 import QtWidgets, QtGui
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCTextArea
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class CNCJobEditorPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "CNC Job Options Preferences", parent=None)
+        super(CNCJobEditorPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("CNC Job Editor")))
+        self.decimals = decimals
+
+        # Editor Parameters
+        self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.param_label.setToolTip(
+            _("A list of Editor parameters.")
+        )
+        self.layout.addWidget(self.param_label)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("textbox_font_size"):
+            tb_fsize = qsettings.value('textbox_font_size', type=int)
+        else:
+            tb_fsize = 10
+        font = QtGui.QFont()
+        font.setPointSize(tb_fsize)
+
+        # Prepend to G-Code
+        prependlabel = QtWidgets.QLabel('%s:' % _('Prepend to G-Code'))
+        prependlabel.setToolTip(
+            _("Type here any G-Code commands you would\n"
+              "like to add at the beginning of the G-Code file.")
+        )
+        self.layout.addWidget(prependlabel)
+
+        self.prepend_text = FCTextArea()
+        self.prepend_text.setPlaceholderText(
+            _("Type here any G-Code commands you would "
+              "like to add at the beginning of the G-Code file.")
+        )
+        self.layout.addWidget(self.prepend_text)
+        self.prepend_text.setFont(font)
+
+        # Append text to G-Code
+        appendlabel = QtWidgets.QLabel('%s:' % _('Append to G-Code'))
+        appendlabel.setToolTip(
+            _("Type here any G-Code commands you would\n"
+              "like to append to the generated file.\n"
+              "I.e.: M2 (End of program)")
+        )
+        self.layout.addWidget(appendlabel)
+
+        self.append_text = FCTextArea()
+        self.append_text.setPlaceholderText(
+            _("Type here any G-Code commands you would "
+              "like to append to the generated file.\n"
+              "I.e.: M2 (End of program)")
+        )
+        self.layout.addWidget(self.append_text)
+        self.append_text.setFont(font)
+
+        self.layout.addStretch()

+ 240 - 0
appGUI/preferences/cncjob/CNCJobGenPrefGroupUI.py

@@ -0,0 +1,240 @@
+from PyQt5 import QtWidgets, QtCore, QtGui
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCCheckBox, RadioSet, FCSpinner, FCDoubleSpinner, FCSliderWithSpinner, FCColorEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class CNCJobGenPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "CNC Job General Preferences", parent=None)
+        super(CNCJobGenPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("CNC Job General")))
+        self.decimals = decimals
+
+        # ## Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
+        self.layout.addWidget(self.plot_options_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        # Plot CB
+        # self.plot_cb = QtWidgets.QCheckBox('Plot')
+        self.plot_cb = FCCheckBox(_('Plot Object'))
+        self.plot_cb.setToolTip(_("Plot (show) this object."))
+        grid0.addWidget(self.plot_cb, 0, 0, 1, 2)
+
+        # ###################################################################
+        # Number of circle steps for circular aperture linear approximation #
+        # ###################################################################
+        self.steps_per_circle_label = QtWidgets.QLabel('%s:' % _("Circle Steps"))
+        self.steps_per_circle_label.setToolTip(
+            _("The number of circle steps for <b>GCode</b> \n"
+              "circle and arc shapes linear approximation.")
+        )
+        grid0.addWidget(self.steps_per_circle_label, 3, 0)
+        self.steps_per_circle_entry = FCSpinner()
+        self.steps_per_circle_entry.set_range(0, 99999)
+        grid0.addWidget(self.steps_per_circle_entry, 3, 1)
+
+        # Tool dia for plot
+        tdlabel = QtWidgets.QLabel('%s:' % _('Travel dia'))
+        tdlabel.setToolTip(
+            _("The width of the travel lines to be\n"
+              "rendered in the plot.")
+        )
+        self.tooldia_entry = FCDoubleSpinner()
+        self.tooldia_entry.set_range(0, 99999)
+        self.tooldia_entry.set_precision(self.decimals)
+        self.tooldia_entry.setSingleStep(0.1)
+        self.tooldia_entry.setWrapping(True)
+
+        grid0.addWidget(tdlabel, 4, 0)
+        grid0.addWidget(self.tooldia_entry, 4, 1)
+
+        # add a space
+        grid0.addWidget(QtWidgets.QLabel('<b>%s:</b>' % _("G-code Decimals")), 5, 0, 1, 2)
+
+        # Number of decimals to use in GCODE coordinates
+        cdeclabel = QtWidgets.QLabel('%s:' % _('Coordinates'))
+        cdeclabel.setToolTip(
+            _("The number of decimals to be used for \n"
+              "the X, Y, Z coordinates in CNC code (GCODE, etc.)")
+        )
+        self.coords_dec_entry = FCSpinner()
+        self.coords_dec_entry.set_range(0, 9)
+        self.coords_dec_entry.setWrapping(True)
+
+        grid0.addWidget(cdeclabel, 6, 0)
+        grid0.addWidget(self.coords_dec_entry, 6, 1)
+
+        # Number of decimals to use in GCODE feedrate
+        frdeclabel = QtWidgets.QLabel('%s:' % _('Feedrate'))
+        frdeclabel.setToolTip(
+            _("The number of decimals to be used for \n"
+              "the Feedrate parameter in CNC code (GCODE, etc.)")
+        )
+        self.fr_dec_entry = FCSpinner()
+        self.fr_dec_entry.set_range(0, 9)
+        self.fr_dec_entry.setWrapping(True)
+
+        grid0.addWidget(frdeclabel, 7, 0)
+        grid0.addWidget(self.fr_dec_entry, 7, 1)
+
+        # The type of coordinates used in the Gcode: Absolute or Incremental
+        coords_type_label = QtWidgets.QLabel('%s:' % _('Coordinates type'))
+        coords_type_label.setToolTip(
+            _("The type of coordinates to be used in Gcode.\n"
+              "Can be:\n"
+              "- Absolute G90 -> the reference is the origin x=0, y=0\n"
+              "- Incremental G91 -> the reference is the previous position")
+        )
+        self.coords_type_radio = RadioSet([
+            {"label": _("Absolute"), "value": "G90"},
+            {"label": _("Incremental"), "value": "G91"}
+        ], orientation='vertical', stretch=False)
+        grid0.addWidget(coords_type_label, 8, 0)
+        grid0.addWidget(self.coords_type_radio, 8, 1)
+
+        # hidden for the time being, until implemented
+        coords_type_label.hide()
+        self.coords_type_radio.hide()
+
+        # Line Endings
+        self.line_ending_cb = FCCheckBox(_("Force Windows style line-ending"))
+        self.line_ending_cb.setToolTip(
+            _("When checked will force a Windows style line-ending\n"
+              "(\\r\\n) on non-Windows OS's.")
+        )
+
+        grid0.addWidget(self.line_ending_cb, 9, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 12, 0, 1, 2)
+
+        # Travel Line Color
+        self.travel_color_label = QtWidgets.QLabel('<b>%s</b>' % _('Travel Line Color'))
+        grid0.addWidget(self.travel_color_label, 13, 0, 1, 2)
+
+        # Plot Line Color
+        self.tline_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
+        self.tline_color_label.setToolTip(
+            _("Set the travel line color for plotted objects.")
+        )
+        self.tline_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.tline_color_label, 14, 0)
+        grid0.addWidget(self.tline_color_entry, 14, 1)
+
+        # Plot Fill Color
+        self.tfill_color_label = QtWidgets.QLabel('%s:' % _('Fill'))
+        self.tfill_color_label.setToolTip(
+            _("Set the fill color for plotted objects.\n"
+              "First 6 digits are the color and the last 2\n"
+              "digits are for alpha (transparency) level.")
+        )
+        self.tfill_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.tfill_color_label, 15, 0)
+        grid0.addWidget(self.tfill_color_entry, 15, 1)
+
+        # Plot Fill Transparency Level
+        self.cncjob_alpha_label = QtWidgets.QLabel('%s:' % _('Alpha'))
+        self.cncjob_alpha_label.setToolTip(
+            _("Set the fill transparency for plotted objects.")
+        )
+        self.cncjob_alpha_entry = FCSliderWithSpinner(0, 255, 1)
+
+        grid0.addWidget(self.cncjob_alpha_label, 16, 0)
+        grid0.addWidget(self.cncjob_alpha_entry, 16, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 17, 0, 1, 2)
+
+        # CNCJob Object Color
+        self.cnc_color_label = QtWidgets.QLabel('<b>%s</b>' % _('Object Color'))
+        grid0.addWidget(self.cnc_color_label, 18, 0, 1, 2)
+
+        # Plot Line Color
+        self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
+        self.line_color_label.setToolTip(
+            _("Set the color for plotted objects.")
+        )
+        self.line_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.line_color_label, 19, 0)
+        grid0.addWidget(self.line_color_entry, 19, 1)
+
+        # Plot Fill Color
+        self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill'))
+        self.fill_color_label.setToolTip(
+            _("Set the fill color for plotted objects.\n"
+              "First 6 digits are the color and the last 2\n"
+              "digits are for alpha (transparency) level.")
+        )
+        self.fill_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.fill_color_label, 20, 0)
+        grid0.addWidget(self.fill_color_entry, 20, 1)
+
+        self.layout.addStretch()
+
+        # Setting plot colors signals
+        self.tline_color_entry.editingFinished.connect(self.on_tline_color_entry)
+        self.tfill_color_entry.editingFinished.connect(self.on_tfill_color_entry)
+
+        self.cncjob_alpha_entry.valueChanged.connect(self.on_cncjob_alpha_changed)  # alpha
+
+        self.line_color_entry.editingFinished.connect(self.on_line_color_entry)
+        self.fill_color_entry.editingFinished.connect(self.on_fill_color_entry)
+
+    # ------------------------------------------------------
+    # Setting travel colors handlers
+    # ------------------------------------------------------
+    def on_tfill_color_entry(self):
+        self.app.defaults['cncjob_travel_fill'] = self.tfill_color_entry.get_value()[:7] + \
+                                                  self.app.defaults['cncjob_travel_fill'][7:9]
+
+    def on_tline_color_entry(self):
+        self.app.defaults['cncjob_travel_line'] = self.tline_color_entry.get_value()[:7] + \
+                                                  self.app.defaults['cncjob_travel_line'][7:9]
+
+    def on_cncjob_alpha_changed(self, spinner_value):
+        self.app.defaults['cncjob_travel_fill'] = \
+            self.app.defaults['cncjob_travel_fill'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.app.defaults['cncjob_travel_line'] = \
+            self.app.defaults['cncjob_travel_line'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+
+    # ------------------------------------------------------
+    # Setting plot colors handlers
+    # ------------------------------------------------------
+    def on_fill_color_entry(self):
+        self.app.defaults['cncjob_plot_fill'] = self.fill_color_entry.get_value()[:7] + \
+                                                  self.app.defaults['cncjob_plot_fill'][7:9]
+
+    def on_line_color_entry(self):
+        self.app.defaults['cncjob_plot_line'] = self.line_color_entry.get_value()[:7] + \
+                                                  self.app.defaults['cncjob_plot_line'][7:9]

+ 81 - 0
appGUI/preferences/cncjob/CNCJobOptPrefGroupUI.py

@@ -0,0 +1,81 @@
+from PyQt5 import QtWidgets, QtGui
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCCheckBox
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class CNCJobOptPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "CNC Job Options Preferences", parent=None)
+        super(CNCJobOptPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("CNC Job Options")))
+        self.decimals = decimals
+
+        # ## Export G-Code
+        self.export_gcode_label = QtWidgets.QLabel("<b>%s:</b>" % _("Export G-Code"))
+        self.export_gcode_label.setToolTip(
+            _("Export and save G-Code to\n"
+              "make this object to a file.")
+        )
+        self.layout.addWidget(self.export_gcode_label)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("textbox_font_size"):
+            tb_fsize = qsettings.value('textbox_font_size', type=int)
+        else:
+            tb_fsize = 10
+        font = QtGui.QFont()
+        font.setPointSize(tb_fsize)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        # Plot Kind
+        self.cncplot_method_label = QtWidgets.QLabel('%s:' % _("Plot kind"))
+        self.cncplot_method_label.setToolTip(
+            _("This selects the kind of geometries on the canvas to plot.\n"
+              "Those can be either of type 'Travel' which means the moves\n"
+              "above the work piece or it can be of type 'Cut',\n"
+              "which means the moves that cut into the material.")
+        )
+
+        self.cncplot_method_radio = RadioSet([
+            {"label": _("All"), "value": "all"},
+            {"label": _("Travel"), "value": "travel"},
+            {"label": _("Cut"), "value": "cut"}
+        ], orientation='vertical', stretch=False)
+
+        grid0.addWidget(self.cncplot_method_label, 1, 0)
+        grid0.addWidget(self.cncplot_method_radio, 1, 1)
+        grid0.addWidget(QtWidgets.QLabel(''), 1, 2)
+
+        # Display Annotation
+        self.annotation_cb = FCCheckBox(_("Display Annotation"))
+        self.annotation_cb.setToolTip(
+            _("This selects if to display text annotation on the plot.\n"
+              "When checked it will display numbers in order for each end\n"
+              "of a travel line."
+              )
+        )
+
+        grid0.addWidget(self.annotation_cb, 2, 0, 1, 3)
+
+        self.layout.addStretch()

+ 36 - 0
appGUI/preferences/cncjob/CNCJobPreferencesUI.py

@@ -0,0 +1,36 @@
+from PyQt5 import QtWidgets
+
+from appGUI.preferences.cncjob.CNCJobAdvOptPrefGroupUI import CNCJobAdvOptPrefGroupUI
+from appGUI.preferences.cncjob.CNCJobOptPrefGroupUI import CNCJobOptPrefGroupUI
+from appGUI.preferences.cncjob.CNCJobGenPrefGroupUI import CNCJobGenPrefGroupUI
+from appGUI.preferences.cncjob.CNCJobEditorPrefGroupUI import CNCJobEditorPrefGroupUI
+
+
+class CNCJobPreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, decimals, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QHBoxLayout()
+        self.setLayout(self.layout)
+        self.decimals = decimals
+
+        self.cncjob_gen_group = CNCJobGenPrefGroupUI(decimals=self.decimals)
+        self.cncjob_gen_group.setMinimumWidth(260)
+
+        self.cncjob_opt_group = CNCJobOptPrefGroupUI(decimals=self.decimals)
+        self.cncjob_opt_group.setMinimumWidth(260)
+        self.cncjob_adv_opt_group = CNCJobAdvOptPrefGroupUI(decimals=self.decimals)
+        self.cncjob_adv_opt_group.setMinimumWidth(260)
+
+        self.cncjob_editor_group = CNCJobEditorPrefGroupUI(decimals=self.decimals)
+        self.cncjob_editor_group.setMinimumWidth(260)
+
+        vlay = QtWidgets.QVBoxLayout()
+        vlay.addWidget(self.cncjob_opt_group)
+        vlay.addWidget(self.cncjob_adv_opt_group)
+
+        self.layout.addWidget(self.cncjob_gen_group)
+        self.layout.addLayout(vlay)
+        self.layout.addWidget(self.cncjob_editor_group)
+
+        self.layout.addStretch()

+ 0 - 0
flatcamParsers/__init__.py → appGUI/preferences/cncjob/__init__.py


+ 62 - 0
appGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py

@@ -0,0 +1,62 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, RadioSet, FCCheckBox, NumericalEvalTupleEntry, NumericalEvalEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ExcellonAdvOptPrefGroupUI(OptionsGroupUI):
+
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Excellon Advanced Options", parent=parent)
+        super(ExcellonAdvOptPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Excellon Adv. Options")))
+        self.decimals = decimals
+
+        # #######################
+        # ## ADVANCED OPTIONS ###
+        # #######################
+
+        self.exc_label = QtWidgets.QLabel('<b>%s:</b>' % _('Advanced Options'))
+        self.exc_label.setToolTip(
+            _("A list of advanced parameters.\n"
+              "Those parameters are available only for\n"
+              "Advanced App. Level.")
+        )
+        self.layout.addWidget(self.exc_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
+        # Table Visibility CB
+        self.table_visibility_cb = FCCheckBox(label=_('Table Show/Hide'))
+        self.table_visibility_cb.setToolTip(
+            _("Toggle the display of the Tools Table.")
+        )
+        grid0.addWidget(self.table_visibility_cb, 0, 0, 1, 2)
+
+        # Auto Load Tools from DB
+        self.autoload_db_cb = FCCheckBox('%s' % _("Auto load from DB"))
+        self.autoload_db_cb.setToolTip(
+            _("Automatic replacement of the tools from related application tools\n"
+              "with tools from DB that have a close diameter value.")
+        )
+        grid0.addWidget(self.autoload_db_cb, 1, 0, 1, 2)
+
+        self.layout.addStretch()

+ 306 - 0
appGUI/preferences/excellon/ExcellonEditorPrefGroupUI.py

@@ -0,0 +1,306 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCSpinner, FCDoubleSpinner, RadioSet
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ExcellonEditorPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        super(ExcellonEditorPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Excellon Editor")))
+        self.decimals = decimals
+
+        # Excellon Editor Parameters
+        self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.param_label.setToolTip(
+            _("A list of Excellon Editor parameters.")
+        )
+        self.layout.addWidget(self.param_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Selection Limit
+        self.sel_limit_label = QtWidgets.QLabel('%s:' % _("Selection limit"))
+        self.sel_limit_label.setToolTip(
+            _("Set the number of selected Excellon geometry\n"
+              "items above which the utility geometry\n"
+              "becomes just a selection rectangle.\n"
+              "Increases the performance when moving a\n"
+              "large number of geometric elements.")
+        )
+        self.sel_limit_entry = FCSpinner()
+        self.sel_limit_entry.set_range(0, 99999)
+
+        grid0.addWidget(self.sel_limit_label, 0, 0)
+        grid0.addWidget(self.sel_limit_entry, 0, 1)
+
+        # New Diameter
+        self.addtool_entry_lbl = QtWidgets.QLabel('%s:' % _('New Dia'))
+        self.addtool_entry_lbl.setToolTip(
+            _("Diameter for the new tool")
+        )
+
+        self.addtool_entry = FCDoubleSpinner()
+        self.addtool_entry.set_range(0.000001, 99.9999)
+        self.addtool_entry.set_precision(self.decimals)
+
+        grid0.addWidget(self.addtool_entry_lbl, 1, 0)
+        grid0.addWidget(self.addtool_entry, 1, 1)
+
+        # Number of drill holes in a drill array
+        self.drill_array_size_label = QtWidgets.QLabel('%s:' % _('Nr of drills'))
+        self.drill_array_size_label.setToolTip(
+            _("Specify how many drills to be in the array.")
+        )
+        # self.drill_array_size_label.setMinimumWidth(100)
+
+        self.drill_array_size_entry = FCSpinner()
+        self.drill_array_size_entry.set_range(0, 9999)
+
+        grid0.addWidget(self.drill_array_size_label, 2, 0)
+        grid0.addWidget(self.drill_array_size_entry, 2, 1)
+
+        self.drill_array_linear_label = QtWidgets.QLabel('<b>%s:</b>' % _('Linear Drill Array'))
+        grid0.addWidget(self.drill_array_linear_label, 3, 0, 1, 2)
+
+        # Linear Drill Array direction
+        self.drill_axis_label = QtWidgets.QLabel('%s:' % _('Linear Direction'))
+        self.drill_axis_label.setToolTip(
+            _("Direction on which the linear array is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the array inclination")
+        )
+        # self.drill_axis_label.setMinimumWidth(100)
+        self.drill_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                          {'label': _('Y'), 'value': 'Y'},
+                                          {'label': _('Angle'), 'value': 'A'}])
+
+        grid0.addWidget(self.drill_axis_label, 4, 0)
+        grid0.addWidget(self.drill_axis_radio, 4, 1)
+
+        # Linear Drill Array pitch distance
+        self.drill_pitch_label = QtWidgets.QLabel('%s:' % _('Pitch'))
+        self.drill_pitch_label.setToolTip(
+            _("Pitch = Distance between elements of the array.")
+        )
+        # self.drill_pitch_label.setMinimumWidth(100)
+        self.drill_pitch_entry = FCDoubleSpinner()
+        self.drill_pitch_entry.set_range(0, 910000.0000)
+        self.drill_pitch_entry.set_precision(self.decimals)
+
+        grid0.addWidget(self.drill_pitch_label, 5, 0)
+        grid0.addWidget(self.drill_pitch_entry, 5, 1)
+
+        # Linear Drill Array custom angle
+        self.drill_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
+        self.drill_angle_label.setToolTip(
+            _("Angle at which each element in circular array is placed.")
+        )
+        self.drill_angle_entry = FCDoubleSpinner()
+        self.drill_pitch_entry.set_range(-360, 360)
+        self.drill_pitch_entry.set_precision(self.decimals)
+        self.drill_angle_entry.setWrapping(True)
+        self.drill_angle_entry.setSingleStep(5)
+
+        grid0.addWidget(self.drill_angle_label, 6, 0)
+        grid0.addWidget(self.drill_angle_entry, 6, 1)
+
+        self.drill_array_circ_label = QtWidgets.QLabel('<b>%s:</b>' % _('Circular Drill Array'))
+        grid0.addWidget(self.drill_array_circ_label, 7, 0, 1, 2)
+
+        # Circular Drill Array direction
+        self.drill_circular_direction_label = QtWidgets.QLabel('%s:' % _('Circular Direction'))
+        self.drill_circular_direction_label.setToolTip(
+            _("Direction for circular array.\n"
+              "Can be CW = clockwise or CCW = counter clockwise.")
+        )
+
+        self.drill_circular_dir_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                                  {'label': _('CCW'), 'value': 'CCW'}])
+
+        grid0.addWidget(self.drill_circular_direction_label, 8, 0)
+        grid0.addWidget(self.drill_circular_dir_radio, 8, 1)
+
+        # Circular Drill Array Angle
+        self.drill_circular_angle_label = QtWidgets.QLabel('%s:' % _('Circular Angle'))
+        self.drill_circular_angle_label.setToolTip(
+            _("Angle at which each element in circular array is placed.")
+        )
+        self.drill_circular_angle_entry = FCDoubleSpinner()
+        self.drill_circular_angle_entry.set_range(-360, 360)
+        self.drill_circular_angle_entry.set_precision(self.decimals)
+        self.drill_circular_angle_entry.setWrapping(True)
+        self.drill_circular_angle_entry.setSingleStep(5)
+
+        grid0.addWidget(self.drill_circular_angle_label, 9, 0)
+        grid0.addWidget(self.drill_circular_angle_entry, 9, 1)
+
+        # ##### SLOTS #####
+        # #################
+        self.drill_array_circ_label = QtWidgets.QLabel('<b>%s:</b>' % _('Slots'))
+        grid0.addWidget(self.drill_array_circ_label, 10, 0, 1, 2)
+
+        # Slot length
+        self.slot_length_label = QtWidgets.QLabel('%s:' % _('Length'))
+        self.slot_length_label.setToolTip(
+            _("Length. The length of the slot.")
+        )
+        self.slot_length_label.setMinimumWidth(100)
+
+        self.slot_length_entry = FCDoubleSpinner()
+        self.slot_length_entry.set_range(0, 99999)
+        self.slot_length_entry.set_precision(self.decimals)
+        self.slot_length_entry.setWrapping(True)
+        self.slot_length_entry.setSingleStep(1)
+
+        grid0.addWidget(self.slot_length_label, 11, 0)
+        grid0.addWidget(self.slot_length_entry, 11, 1)
+
+        # Slot direction
+        self.slot_axis_label = QtWidgets.QLabel('%s:' % _('Direction'))
+        self.slot_axis_label.setToolTip(
+            _("Direction on which the slot is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the slot inclination")
+        )
+        self.slot_axis_label.setMinimumWidth(100)
+
+        self.slot_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                         {'label': _('Y'), 'value': 'Y'},
+                                         {'label': _('Angle'), 'value': 'A'}])
+        grid0.addWidget(self.slot_axis_label, 12, 0)
+        grid0.addWidget(self.slot_axis_radio, 12, 1)
+
+        # Slot custom angle
+        self.slot_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
+        self.slot_angle_label.setToolTip(
+            _("Angle at which the slot is placed.\n"
+              "The precision is of max 2 decimals.\n"
+              "Min value is: -360.00 degrees.\n"
+              "Max value is: 360.00 degrees.")
+        )
+        self.slot_angle_label.setMinimumWidth(100)
+
+        self.slot_angle_spinner = FCDoubleSpinner()
+        self.slot_angle_spinner.set_precision(self.decimals)
+        self.slot_angle_spinner.setWrapping(True)
+        self.slot_angle_spinner.setRange(-359.99, 360.00)
+        self.slot_angle_spinner.setSingleStep(5)
+
+        grid0.addWidget(self.slot_angle_label, 13, 0)
+        grid0.addWidget(self.slot_angle_spinner, 13, 1)
+
+        # #### SLOTS ARRAY #######
+        # ########################
+
+        self.slot_array_linear_label = QtWidgets.QLabel('<b>%s:</b>' % _('Linear Slot Array'))
+        grid0.addWidget(self.slot_array_linear_label, 14, 0, 1, 2)
+
+        # Number of slot holes in a drill array
+        self.slot_array_size_label = QtWidgets.QLabel('%s:' % _('Nr of slots'))
+        self.drill_array_size_label.setToolTip(
+            _("Specify how many slots to be in the array.")
+        )
+        # self.slot_array_size_label.setMinimumWidth(100)
+
+        self.slot_array_size_entry = FCSpinner()
+        self.slot_array_size_entry.set_range(0, 999999)
+
+        grid0.addWidget(self.slot_array_size_label, 15, 0)
+        grid0.addWidget(self.slot_array_size_entry, 15, 1)
+
+        # Linear Slot Array direction
+        self.slot_array_axis_label = QtWidgets.QLabel('%s:' % _('Linear Direction'))
+        self.slot_array_axis_label.setToolTip(
+            _("Direction on which the linear array is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the array inclination")
+        )
+        # self.slot_axis_label.setMinimumWidth(100)
+        self.slot_array_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                               {'label': _('Y'), 'value': 'Y'},
+                                               {'label': _('Angle'), 'value': 'A'}])
+
+        grid0.addWidget(self.slot_array_axis_label, 16, 0)
+        grid0.addWidget(self.slot_array_axis_radio, 16, 1)
+
+        # Linear Slot Array pitch distance
+        self.slot_array_pitch_label = QtWidgets.QLabel('%s:' % _('Pitch'))
+        self.slot_array_pitch_label.setToolTip(
+            _("Pitch = Distance between elements of the array.")
+        )
+        # self.drill_pitch_label.setMinimumWidth(100)
+        self.slot_array_pitch_entry = FCDoubleSpinner()
+        self.slot_array_pitch_entry.set_precision(self.decimals)
+        self.slot_array_pitch_entry.setWrapping(True)
+        self.slot_array_pitch_entry.setRange(0, 999999)
+        self.slot_array_pitch_entry.setSingleStep(1)
+
+        grid0.addWidget(self.slot_array_pitch_label, 17, 0)
+        grid0.addWidget(self.slot_array_pitch_entry, 17, 1)
+
+        # Linear Slot Array custom angle
+        self.slot_array_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
+        self.slot_array_angle_label.setToolTip(
+            _("Angle at which each element in circular array is placed.")
+        )
+        self.slot_array_angle_entry = FCDoubleSpinner()
+        self.slot_array_angle_entry.set_precision(self.decimals)
+        self.slot_array_angle_entry.setWrapping(True)
+        self.slot_array_angle_entry.setRange(-360, 360)
+        self.slot_array_angle_entry.setSingleStep(5)
+
+        grid0.addWidget(self.slot_array_angle_label, 18, 0)
+        grid0.addWidget(self.slot_array_angle_entry, 18, 1)
+
+        self.slot_array_circ_label = QtWidgets.QLabel('<b>%s:</b>' % _('Circular Slot Array'))
+        grid0.addWidget(self.slot_array_circ_label, 19, 0, 1, 2)
+
+        # Circular Slot Array direction
+        self.slot_array_circular_direction_label = QtWidgets.QLabel('%s:' % _('Circular Direction'))
+        self.slot_array_circular_direction_label.setToolTip(
+            _("Direction for circular array.\n"
+              "Can be CW = clockwise or CCW = counter clockwise.")
+        )
+
+        self.slot_array_circular_dir_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                                       {'label': _('CCW'), 'value': 'CCW'}])
+
+        grid0.addWidget(self.slot_array_circular_direction_label, 20, 0)
+        grid0.addWidget(self.slot_array_circular_dir_radio, 20, 1)
+
+        # Circular Slot Array Angle
+        self.slot_array_circular_angle_label = QtWidgets.QLabel('%s:' % _('Circular Angle'))
+        self.slot_array_circular_angle_label.setToolTip(
+            _("Angle at which each element in circular array is placed.")
+        )
+        self.slot_array_circular_angle_entry = FCDoubleSpinner()
+        self.slot_array_circular_angle_entry.set_precision(self.decimals)
+        self.slot_array_circular_angle_entry.setWrapping(True)
+        self.slot_array_circular_angle_entry.setRange(-360, 360)
+        self.slot_array_circular_angle_entry.setSingleStep(5)
+
+        grid0.addWidget(self.slot_array_circular_angle_label, 21, 0)
+        grid0.addWidget(self.slot_array_circular_angle_entry, 21, 1)
+
+        self.layout.addStretch()

+ 168 - 0
appGUI/preferences/excellon/ExcellonExpPrefGroupUI.py

@@ -0,0 +1,168 @@
+from PyQt5 import QtWidgets, QtCore
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCSpinner
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ExcellonExpPrefGroupUI(OptionsGroupUI):
+
+    def __init__(self, decimals=4, parent=None):
+        super(ExcellonExpPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Excellon Export")))
+        self.decimals = decimals
+
+        # Plot options
+        self.export_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Export Options"))
+        self.export_options_label.setToolTip(
+            _("The parameters set here are used in the file exported\n"
+              "when using the File -> Export -> Export Excellon menu entry.")
+        )
+        self.layout.addWidget(self.export_options_label)
+
+        form = QtWidgets.QFormLayout()
+        self.layout.addLayout(form)
+
+        # Excellon Units
+        self.excellon_units_label = QtWidgets.QLabel('%s:' % _('Units'))
+        self.excellon_units_label.setToolTip(
+            _("The units used in the Excellon file.")
+        )
+
+        self.excellon_units_radio = RadioSet([{'label': _('Inch'), 'value': 'INCH'},
+                                              {'label': _('mm'), 'value': 'METRIC'}])
+        self.excellon_units_radio.setToolTip(
+            _("The units used in the Excellon file.")
+        )
+
+        form.addRow(self.excellon_units_label, self.excellon_units_radio)
+
+        # Excellon non-decimal format
+        self.digits_label = QtWidgets.QLabel("%s:" % _("Int/Decimals"))
+        self.digits_label.setToolTip(
+            _("The NC drill files, usually named Excellon files\n"
+              "are files that can be found in different formats.\n"
+              "Here we set the format used when the provided\n"
+              "coordinates are not using period.")
+        )
+
+        hlay1 = QtWidgets.QHBoxLayout()
+
+        self.format_whole_entry = FCSpinner()
+        self.format_whole_entry.set_range(0, 9)
+        self.format_whole_entry.setMinimumWidth(30)
+        self.format_whole_entry.setToolTip(
+            _("This numbers signify the number of digits in\n"
+              "the whole part of Excellon coordinates.")
+        )
+        hlay1.addWidget(self.format_whole_entry, QtCore.Qt.AlignLeft)
+
+        excellon_separator_label = QtWidgets.QLabel(':')
+        excellon_separator_label.setFixedWidth(5)
+        hlay1.addWidget(excellon_separator_label, QtCore.Qt.AlignLeft)
+
+        self.format_dec_entry = FCSpinner()
+        self.format_dec_entry.set_range(0, 9)
+        self.format_dec_entry.setMinimumWidth(30)
+        self.format_dec_entry.setToolTip(
+            _("This numbers signify the number of digits in\n"
+              "the decimal part of Excellon coordinates.")
+        )
+        hlay1.addWidget(self.format_dec_entry, QtCore.Qt.AlignLeft)
+        hlay1.addStretch()
+
+        form.addRow(self.digits_label, hlay1)
+
+        # Select the Excellon Format
+        self.format_label = QtWidgets.QLabel("%s:" % _("Format"))
+        self.format_label.setToolTip(
+            _("Select the kind of coordinates format used.\n"
+              "Coordinates can be saved with decimal point or without.\n"
+              "When there is no decimal point, it is required to specify\n"
+              "the number of digits for integer part and the number of decimals.\n"
+              "Also it will have to be specified if LZ = leading zeros are kept\n"
+              "or TZ = trailing zeros are kept.")
+        )
+        self.format_radio = RadioSet([{'label': _('Decimal'), 'value': 'dec'},
+                                      {'label': _('No-Decimal'), 'value': 'ndec'}])
+        self.format_radio.setToolTip(
+            _("Select the kind of coordinates format used.\n"
+              "Coordinates can be saved with decimal point or without.\n"
+              "When there is no decimal point, it is required to specify\n"
+              "the number of digits for integer part and the number of decimals.\n"
+              "Also it will have to be specified if LZ = leading zeros are kept\n"
+              "or TZ = trailing zeros are kept.")
+        )
+
+        form.addRow(self.format_label, self.format_radio)
+
+        # Excellon Zeros
+        self.zeros_label = QtWidgets.QLabel('%s:' % _('Zeros'))
+        self.zeros_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.zeros_label.setToolTip(
+            _("This sets the type of Excellon zeros.\n"
+              "If LZ then Leading Zeros are kept and\n"
+              "Trailing Zeros are removed.\n"
+              "If TZ is checked then Trailing Zeros are kept\n"
+              "and Leading Zeros are removed.")
+        )
+
+        self.zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'LZ'},
+                                     {'label': _('TZ'), 'value': 'TZ'}])
+        self.zeros_radio.setToolTip(
+            _("This sets the default type of Excellon zeros.\n"
+              "If LZ then Leading Zeros are kept and\n"
+              "Trailing Zeros are removed.\n"
+              "If TZ is checked then Trailing Zeros are kept\n"
+              "and Leading Zeros are removed.")
+        )
+
+        form.addRow(self.zeros_label, self.zeros_radio)
+
+        # Slot type
+        self.slot_type_label = QtWidgets.QLabel('%s:' % _('Slot type'))
+        self.slot_type_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.slot_type_label.setToolTip(
+            _("This sets how the slots will be exported.\n"
+              "If ROUTED then the slots will be routed\n"
+              "using M15/M16 commands.\n"
+              "If DRILLED(G85) the slots will be exported\n"
+              "using the Drilled slot command (G85).")
+        )
+
+        self.slot_type_radio = RadioSet([{'label': _('Routed'), 'value': 'routing'},
+                                         {'label': _('Drilled(G85)'), 'value': 'drilling'}])
+        self.slot_type_radio.setToolTip(
+            _("This sets how the slots will be exported.\n"
+              "If ROUTED then the slots will be routed\n"
+              "using M15/M16 commands.\n"
+              "If DRILLED(G85) the slots will be exported\n"
+              "using the Drilled slot command (G85).")
+        )
+
+        form.addRow(self.slot_type_label, self.slot_type_radio)
+
+        self.layout.addStretch()
+        self.format_radio.activated_custom.connect(self.optimization_selection)
+
+    def optimization_selection(self):
+        if self.format_radio.get_value() == 'dec':
+            self.zeros_label.setDisabled(True)
+            self.zeros_radio.setDisabled(True)
+        else:
+            self.zeros_label.setDisabled(False)
+            self.zeros_radio.setDisabled(False)

+ 468 - 0
appGUI/preferences/excellon/ExcellonGenPrefGroupUI.py

@@ -0,0 +1,468 @@
+import platform
+
+from PyQt5 import QtWidgets, QtCore
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCSliderWithSpinner, FCColorEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ExcellonGenPrefGroupUI(OptionsGroupUI):
+
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Excellon Options", parent=parent)
+        super(ExcellonGenPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Excellon General")))
+        self.decimals = decimals
+
+        # Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
+        self.layout.addWidget(self.plot_options_label)
+
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label=_('Plot'))
+        self.plot_cb.setToolTip(
+            "Plot (show) this object."
+        )
+        grid1.addWidget(self.plot_cb, 0, 0)
+
+        # Solid CB
+        self.solid_cb = FCCheckBox(label=_('Solid'))
+        self.solid_cb.setToolTip(
+            "Plot as solid circles."
+        )
+        grid1.addWidget(self.solid_cb, 0, 1)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label='%s' % _('M-Color'))
+        self.multicolored_cb.setToolTip(
+            _("Draw polygons in different colors.")
+        )
+        grid1.addWidget(self.multicolored_cb, 0, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 1, 0, 1, 3)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid2)
+        grid2.setColumnStretch(0, 0)
+        grid2.setColumnStretch(1, 1)
+
+        # Excellon format
+        self.excellon_format_label = QtWidgets.QLabel("<b>%s:</b>" % _("Excellon Format"))
+        self.excellon_format_label.setToolTip(
+            _("The NC drill files, usually named Excellon files\n"
+              "are files that can be found in different formats.\n"
+              "Here we set the format used when the provided\n"
+              "coordinates are not using period.\n"
+              "\n"
+              "Possible presets:\n"
+              "\n"
+              "PROTEUS 3:3 MM LZ\n"
+              "DipTrace 5:2 MM TZ\n"
+              "DipTrace 4:3 MM LZ\n"
+              "\n"
+              "EAGLE 3:3 MM TZ\n"
+              "EAGLE 4:3 MM TZ\n"
+              "EAGLE 2:5 INCH TZ\n"
+              "EAGLE 3:5 INCH TZ\n"
+              "\n"
+              "ALTIUM 2:4 INCH LZ\n"
+              "Sprint Layout 2:4 INCH LZ"
+              "\n"
+              "KiCAD 3:5 INCH TZ")
+        )
+        grid2.addWidget(self.excellon_format_label, 0, 0, 1, 2)
+
+        self.excellon_format_in_label = QtWidgets.QLabel('%s:' % _("INCH"))
+        self.excellon_format_in_label.setToolTip(_("Default values for INCH are 2:4"))
+
+        hlay1 = QtWidgets.QHBoxLayout()
+        self.excellon_format_upper_in_entry = FCSpinner()
+        self.excellon_format_upper_in_entry.set_range(0, 9)
+        self.excellon_format_upper_in_entry.setMinimumWidth(30)
+        self.excellon_format_upper_in_entry.setToolTip(
+           _("This numbers signify the number of digits in\n"
+             "the whole part of Excellon coordinates.")
+        )
+        hlay1.addWidget(self.excellon_format_upper_in_entry)
+
+        excellon_separator_in_label = QtWidgets.QLabel(':')
+        excellon_separator_in_label.setFixedWidth(5)
+        hlay1.addWidget(excellon_separator_in_label)
+
+        self.excellon_format_lower_in_entry = FCSpinner()
+        self.excellon_format_lower_in_entry.set_range(0, 9)
+        self.excellon_format_lower_in_entry.setMinimumWidth(30)
+        self.excellon_format_lower_in_entry.setToolTip(
+            _("This numbers signify the number of digits in\n"
+              "the decimal part of Excellon coordinates.")
+        )
+        hlay1.addWidget(self.excellon_format_lower_in_entry)
+
+        grid2.addWidget(self.excellon_format_in_label, 1, 0)
+        grid2.addLayout(hlay1, 1, 1)
+
+        self.excellon_format_mm_label = QtWidgets.QLabel('%s:' % _("METRIC"))
+        self.excellon_format_mm_label.setToolTip(_("Default values for METRIC are 3:3"))
+
+        hlay2 = QtWidgets.QHBoxLayout()
+        self.excellon_format_upper_mm_entry = FCSpinner()
+        self.excellon_format_upper_mm_entry.set_range(0, 9)
+        self.excellon_format_upper_mm_entry.setMinimumWidth(30)
+        self.excellon_format_upper_mm_entry.setToolTip(
+            _("This numbers signify the number of digits in\n"
+              "the whole part of Excellon coordinates.")
+        )
+        hlay2.addWidget(self.excellon_format_upper_mm_entry)
+
+        excellon_separator_mm_label = QtWidgets.QLabel(':')
+        excellon_separator_mm_label.setFixedWidth(5)
+        hlay2.addWidget(excellon_separator_mm_label, QtCore.Qt.AlignLeft)
+
+        self.excellon_format_lower_mm_entry = FCSpinner()
+        self.excellon_format_lower_mm_entry.set_range(0, 9)
+        self.excellon_format_lower_mm_entry.setMinimumWidth(30)
+        self.excellon_format_lower_mm_entry.setToolTip(
+            _("This numbers signify the number of digits in\n"
+              "the decimal part of Excellon coordinates.")
+        )
+        hlay2.addWidget(self.excellon_format_lower_mm_entry)
+
+        grid2.addWidget(self.excellon_format_mm_label, 2, 0)
+        grid2.addLayout(hlay2, 2, 1)
+
+        self.excellon_zeros_label = QtWidgets.QLabel('%s:' % _('Zeros'))
+        self.excellon_zeros_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.excellon_zeros_label.setToolTip(
+            _("This sets the type of Excellon zeros.\n"
+              "If LZ then Leading Zeros are kept and\n"
+              "Trailing Zeros are removed.\n"
+              "If TZ is checked then Trailing Zeros are kept\n"
+              "and Leading Zeros are removed.\n\n"
+              "This is used when there is no information\n"
+              "stored in the Excellon file.")
+        )
+        grid2.addWidget(self.excellon_zeros_label, 3, 0)
+
+        self.excellon_zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'L'},
+                                              {'label': _('TZ'), 'value': 'T'}])
+
+        grid2.addWidget(self.excellon_zeros_radio, 3, 1)
+
+        self.excellon_units_label = QtWidgets.QLabel('%s:' % _('Units'))
+        self.excellon_units_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.excellon_units_label.setToolTip(
+            _("This sets the default units of Excellon files.\n"
+              "If it is not detected in the parsed file the value here\n"
+              "will be used."
+              "Some Excellon files don't have an header\n"
+              "therefore this parameter will be used.")
+        )
+
+        self.excellon_units_radio = RadioSet([{'label': _('Inch'), 'value': 'INCH'},
+                                              {'label': _('mm'), 'value': 'METRIC'}])
+        self.excellon_units_radio.setToolTip(
+            _("This sets the units of Excellon files.\n"
+              "Some Excellon files don't have an header\n"
+              "therefore this parameter will be used.")
+        )
+
+        grid2.addWidget(self.excellon_units_label, 4, 0)
+        grid2.addWidget(self.excellon_units_radio, 4, 1)
+
+        self.update_excellon_cb = FCCheckBox(label=_('Update Export settings'))
+        self.update_excellon_cb.setToolTip(
+            "If checked, the Excellon Export settings will be updated with the ones above."
+        )
+        grid2.addWidget(self.update_excellon_cb, 5, 0, 1, 2)
+
+        # Adding the Excellon Format Defaults Button
+        self.excellon_defaults_button = QtWidgets.QPushButton()
+        self.excellon_defaults_button.setText(str(_("Restore Defaults")))
+        self.excellon_defaults_button.setMinimumWidth(80)
+        grid2.addWidget(self.excellon_defaults_button, 6, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line, 7, 0, 1, 2)
+
+        self.excellon_general_label = QtWidgets.QLabel("<b>%s:</b>" % _("Path Optimization"))
+        grid2.addWidget(self.excellon_general_label, 8, 0, 1, 2)
+
+        self.excellon_optimization_label = QtWidgets.QLabel(_('Algorithm:'))
+        self.excellon_optimization_label.setToolTip(
+            _("This sets the optimization type for the Excellon drill path.\n"
+              "If <<MetaHeuristic>> is checked then Google OR-Tools algorithm with\n"
+              "MetaHeuristic Guided Local Path is used. Default search time is 3sec.\n"
+              "If <<Basic>> is checked then Google OR-Tools Basic algorithm is used.\n"
+              "If <<TSA>> is checked then Travelling Salesman algorithm is used for\n"
+              "drill path optimization.\n"
+              "\n"
+              "Some options are disabled when the application works in 32bit mode.")
+        )
+
+        self.excellon_optimization_radio = RadioSet([{'label': _('MetaHeuristic'), 'value': 'M'},
+                                                     {'label': _('Basic'), 'value': 'B'},
+                                                     {'label': _('TSA'), 'value': 'T'}],
+                                                    orientation='vertical', stretch=False)
+
+        grid2.addWidget(self.excellon_optimization_label, 9, 0)
+        grid2.addWidget(self.excellon_optimization_radio, 9, 1)
+
+        self.optimization_time_label = QtWidgets.QLabel('%s:' % _('Duration'))
+        self.optimization_time_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.optimization_time_label.setToolTip(
+            _("When OR-Tools Metaheuristic (MH) is enabled there is a\n"
+              "maximum threshold for how much time is spent doing the\n"
+              "path optimization. This max duration is set here.\n"
+              "In seconds.")
+
+        )
+
+        self.optimization_time_entry = FCSpinner()
+        self.optimization_time_entry.set_range(0, 999)
+
+        grid2.addWidget(self.optimization_time_label, 10, 0)
+        grid2.addWidget(self.optimization_time_entry, 10, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line, 11, 0, 1, 2)
+
+        # Fuse Tools
+        self.join_geo_label = QtWidgets.QLabel('<b>%s</b>:' % _('Join Option'))
+        grid2.addWidget(self.join_geo_label, 12, 0, 1, 2)
+
+        self.fuse_tools_cb = FCCheckBox(_("Fuse Tools"))
+        self.fuse_tools_cb.setToolTip(
+            _("When checked, the tools will be merged\n"
+              "but only if they share some of their attributes.")
+        )
+        grid2.addWidget(self.fuse_tools_cb, 13, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid2.addWidget(separator_line, 14, 0, 1, 2)
+
+        # Excellon Object Color
+        self.gerber_color_label = QtWidgets.QLabel('<b>%s</b>' % _('Object Color'))
+        grid2.addWidget(self.gerber_color_label, 17, 0, 1, 2)
+
+        # Plot Line Color
+        self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
+        self.line_color_label.setToolTip(
+            _("Set the line color for plotted objects.")
+        )
+        self.line_color_entry = FCColorEntry()
+
+        grid2.addWidget(self.line_color_label, 19, 0)
+        grid2.addWidget(self.line_color_entry, 19, 1)
+
+        # Plot Fill Color
+        self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill'))
+        self.fill_color_label.setToolTip(
+            _("Set the fill color for plotted objects.\n"
+              "First 6 digits are the color and the last 2\n"
+              "digits are for alpha (transparency) level.")
+        )
+        self.fill_color_entry = FCColorEntry()
+
+        grid2.addWidget(self.fill_color_label, 22, 0)
+        grid2.addWidget(self.fill_color_entry, 22, 1)
+
+        # Plot Fill Transparency Level
+        self.excellon_alpha_label = QtWidgets.QLabel('%s:' % _('Alpha'))
+        self.excellon_alpha_label.setToolTip(
+            _("Set the fill transparency for plotted objects.")
+        )
+        self.excellon_alpha_entry = FCSliderWithSpinner(0, 255, 1)
+
+        grid2.addWidget(self.excellon_alpha_label, 24, 0)
+        grid2.addWidget(self.excellon_alpha_entry, 24, 1)
+
+        self.layout.addStretch()
+
+        current_platform = platform.architecture()[0]
+        if current_platform == '64bit':
+            self.excellon_optimization_radio.setOptionsDisabled([_('MetaHeuristic'), _('Basic')], False)
+            self.optimization_time_label.setDisabled(False)
+            self.optimization_time_entry.setDisabled(False)
+        else:
+            self.excellon_optimization_radio.setOptionsDisabled([_('MetaHeuristic'), _('Basic')], True)
+            self.optimization_time_label.setDisabled(True)
+            self.optimization_time_entry.setDisabled(True)
+
+        # Setting plot colors signals
+        self.line_color_entry.editingFinished.connect(self.on_line_color_entry)
+        self.fill_color_entry.editingFinished.connect(self.on_fill_color_entry)
+
+        self.excellon_alpha_entry.valueChanged.connect(self.on_excellon_alpha_changed)  # alpha
+
+        # Load the defaults values into the Excellon Format and Excellon Zeros fields
+        self.excellon_defaults_button.clicked.connect(self.on_excellon_defaults_button)
+        # Make sure that when the Excellon loading parameters are changed, the change is reflected in the
+        # Export Excellon parameters.
+        self.update_excellon_cb.stateChanged.connect(self.on_update_exc_export)
+
+        # call it once to make sure it is updated at startup
+        self.on_update_exc_export(state=self.app.defaults["excellon_update"])
+
+        self.excellon_optimization_radio.activated_custom.connect(self.optimization_selection)
+
+    def optimization_selection(self):
+        if self.excellon_optimization_radio.get_value() == 'M':
+            self.optimization_time_label.setDisabled(False)
+            self.optimization_time_entry.setDisabled(False)
+        else:
+            self.optimization_time_label.setDisabled(True)
+            self.optimization_time_entry.setDisabled(True)
+
+    # Setting plot colors handlers
+    def on_fill_color_entry(self):
+        self.app.defaults['excellon_plot_fill'] = self.fill_color_entry.get_value()[:7] + \
+            self.app.defaults['excellon_plot_fill'][7:9]
+
+    def on_line_color_entry(self):
+        self.app.defaults['excellon_plot_line'] = self.line_color_entry.get_value()[:7] + \
+                                                self.app.defaults['excellon_plot_line'][7:9]
+
+    def on_excellon_alpha_changed(self, spinner_value):
+        self.app.defaults['excellon_plot_fill'] = \
+            self.app.defaults['excellon_plot_fill'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.app.defaults['excellon_plot_line'] = \
+            self.app.defaults['excellon_plot_line'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+
+    def on_excellon_defaults_button(self):
+        self.app.preferencesUiManager.defaults_form_fields["excellon_format_lower_in"].set_value('4')
+        self.app.preferencesUiManager.defaults_form_fields["excellon_format_upper_in"].set_value('2')
+        self.app.preferencesUiManager.defaults_form_fields["excellon_format_lower_mm"].set_value('3')
+        self.app.preferencesUiManager.defaults_form_fields["excellon_format_upper_mm"].set_value('3')
+        self.app.preferencesUiManager.defaults_form_fields["excellon_zeros"].set_value('L')
+        self.app.preferencesUiManager.defaults_form_fields["excellon_units"].set_value('INCH')
+
+    def on_update_exc_export(self, state):
+        """
+        This is handling the update of Excellon Export parameters based on the ones in the Excellon General but only
+        if the update_excellon_cb checkbox is checked
+
+        :param state: state of the checkbox whose signals is tied to his slot
+        :return:
+        """
+        if state:
+            # first try to disconnect
+            try:
+                self.excellon_format_upper_in_entry.returnPressed.disconnect(self.on_excellon_format_changed)
+            except TypeError:
+                pass
+            try:
+                self.excellon_format_lower_in_entry.returnPressed.disconnect(self.on_excellon_format_changed)
+            except TypeError:
+                pass
+            try:
+                self.excellon_format_upper_mm_entry.returnPressed.disconnect(self.on_excellon_format_changed)
+            except TypeError:
+                pass
+            try:
+                self.excellon_format_lower_mm_entry.returnPressed.disconnect(self.on_excellon_format_changed)
+            except TypeError:
+                pass
+
+            try:
+                self.excellon_zeros_radio.activated_custom.disconnect(self.on_excellon_zeros_changed)
+            except TypeError:
+                pass
+            try:
+                self.excellon_units_radio.activated_custom.disconnect(self.on_excellon_zeros_changed)
+            except TypeError:
+                pass
+
+            # the connect them
+            self.excellon_format_upper_in_entry.returnPressed.connect(self.on_excellon_format_changed)
+            self.excellon_format_lower_in_entry.returnPressed.connect(self.on_excellon_format_changed)
+            self.excellon_format_upper_mm_entry.returnPressed.connect(self.on_excellon_format_changed)
+            self.excellon_format_lower_mm_entry.returnPressed.connect(self.on_excellon_format_changed)
+            self.excellon_zeros_radio.activated_custom.connect(self.on_excellon_zeros_changed)
+            self.excellon_units_radio.activated_custom.connect(self.on_excellon_units_changed)
+        else:
+            # disconnect the signals
+            try:
+                self.excellon_format_upper_in_entry.returnPressed.disconnect(self.on_excellon_format_changed)
+            except TypeError:
+                pass
+            try:
+                self.excellon_format_lower_in_entry.returnPressed.disconnect(self.on_excellon_format_changed)
+            except TypeError:
+                pass
+            try:
+                self.excellon_format_upper_mm_entry.returnPressed.disconnect(self.on_excellon_format_changed)
+            except TypeError:
+                pass
+            try:
+                self.excellon_format_lower_mm_entry.returnPressed.disconnect(self.on_excellon_format_changed)
+            except TypeError:
+                pass
+
+            try:
+                self.excellon_zeros_radio.activated_custom.disconnect(self.on_excellon_zeros_changed)
+            except TypeError:
+                pass
+            try:
+                self.excellon_units_radio.activated_custom.disconnect(self.on_excellon_zeros_changed)
+            except TypeError:
+                pass
+
+    def on_excellon_format_changed(self):
+        """
+        Slot activated when the user changes the Excellon format values in Preferences -> Excellon -> Excellon General
+        :return: None
+        """
+        if self.excellon_units_radio.get_value().upper() == 'METRIC':
+            self.app.ui.excellon_defaults_form.excellon_exp_group.format_whole_entry.set_value(
+                self.excellon_format_upper_mm_entry.get_value())
+            self.app.ui.excellon_defaults_form.excellon_exp_group.format_dec_entry.set_value(
+                self.excellon_format_lower_mm_entry.get_value())
+        else:
+            self.app.ui.excellon_defaults_form.excellon_exp_group.format_whole_entry.set_value(
+                self.excellon_format_upper_in_entry.get_value())
+            self.app.ui.excellon_defaults_form.excellon_exp_group.format_dec_entry.set_value(
+                self.excellon_format_lower_in_entry.get_value())
+
+    def on_excellon_zeros_changed(self, val):
+        """
+        Slot activated when the user changes the Excellon zeros values in Preferences -> Excellon -> Excellon General
+        :return: None
+        """
+        self.app.ui.excellon_defaults_form.excellon_exp_group.zeros_radio.set_value(val + 'Z')
+
+    def on_excellon_units_changed(self, val):
+        """
+        Slot activated when the user changes the Excellon unit values in Preferences -> Excellon -> Excellon General
+        :return: None
+        """
+        self.app.ui.excellon_defaults_form.excellon_exp_group.excellon_units_radio.set_value(val)
+        self.on_excellon_format_changed()

+ 122 - 0
appGUI/preferences/excellon/ExcellonOptPrefGroupUI.py

@@ -0,0 +1,122 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import Qt, QSettings
+
+from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCEntry, FCSpinner, OptionalInputSection, \
+    FCComboBox, NumericalEvalTupleEntry
+from appGUI.preferences import machinist_setting
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ExcellonOptPrefGroupUI(OptionsGroupUI):
+
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Excellon Options", parent=parent)
+        super(ExcellonOptPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Excellon Options")))
+        self.decimals = decimals
+
+        # ## Create CNC Job
+        self.cncjob_label = QtWidgets.QLabel('<b>%s</b>' % _('Create CNCJob'))
+        self.cncjob_label.setToolTip(
+            _("Parameters used to create a CNC Job object\n"
+              "for this drill object.")
+        )
+        self.layout.addWidget(self.cncjob_label)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid2)
+        grid2.setColumnStretch(0, 0)
+        grid2.setColumnStretch(1, 1)
+
+        # Operation Type
+        self.operation_label = QtWidgets.QLabel('<b>%s:</b>' % _('Operation'))
+        self.operation_label.setToolTip(
+            _("Operation type:\n"
+              "- Drilling -> will drill the drills/slots associated with this tool\n"
+              "- Milling -> will mill the drills/slots")
+        )
+        self.operation_radio = RadioSet(
+            [
+                {'label': _('Drilling'), 'value': 'drill'},
+                {'label': _("Milling"), 'value': 'mill'}
+            ]
+        )
+
+        grid2.addWidget(self.operation_label, 0, 0)
+        grid2.addWidget(self.operation_radio, 0, 1)
+
+        self.mill_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
+        self.mill_type_label.setToolTip(
+            _("Milling type:\n"
+              "- Drills -> will mill the drills associated with this tool\n"
+              "- Slots -> will mill the slots associated with this tool\n"
+              "- Both -> will mill both drills and mills or whatever is available")
+        )
+        self.milling_type_radio = RadioSet(
+            [
+                {'label': _('Drills'), 'value': 'drills'},
+                {'label': _("Slots"), 'value': 'slots'},
+                {'label': _("Both"), 'value': 'both'},
+            ]
+        )
+
+        grid2.addWidget(self.mill_type_label, 1, 0)
+        grid2.addWidget(self.milling_type_radio, 1, 1)
+
+        self.mill_dia_label = QtWidgets.QLabel('%s:' % _('Milling Diameter'))
+        self.mill_dia_label.setToolTip(
+            _("The diameter of the tool who will do the milling")
+        )
+
+        self.mill_dia_entry = FCDoubleSpinner()
+        self.mill_dia_entry.set_precision(self.decimals)
+        self.mill_dia_entry.set_range(0.0000, 10000.0000)
+
+        grid2.addWidget(self.mill_dia_label, 2, 0)
+        grid2.addWidget(self.mill_dia_entry, 2, 1)
+
+        # ### Milling Holes ## ##
+        self.mill_hole_label = QtWidgets.QLabel('<b>%s</b>' % _('Mill Holes'))
+        self.mill_hole_label.setToolTip(
+            _("Create Geometry for milling holes.")
+        )
+        grid2.addWidget(self.mill_hole_label, 16, 0, 1, 2)
+
+        tdlabel = QtWidgets.QLabel('%s:' % _('Drill Tool dia'))
+        tdlabel.setToolTip(
+            _("Diameter of the cutting tool.")
+        )
+        self.tooldia_entry = FCDoubleSpinner()
+        self.tooldia_entry.set_precision(self.decimals)
+        self.tooldia_entry.set_range(0, 999.9999)
+
+        grid2.addWidget(tdlabel, 18, 0)
+        grid2.addWidget(self.tooldia_entry, 18, 1)
+
+        stdlabel = QtWidgets.QLabel('%s:' % _('Slot Tool dia'))
+        stdlabel.setToolTip(
+            _("Diameter of the cutting tool\n"
+              "when milling slots.")
+        )
+        self.slot_tooldia_entry = FCDoubleSpinner()
+        self.slot_tooldia_entry.set_precision(self.decimals)
+        self.slot_tooldia_entry.set_range(0, 999.9999)
+
+        grid2.addWidget(stdlabel, 21, 0)
+        grid2.addWidget(self.slot_tooldia_entry, 21, 1)
+
+        self.layout.addStretch()

+ 53 - 0
appGUI/preferences/excellon/ExcellonPreferencesUI.py

@@ -0,0 +1,53 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.preferences.excellon.ExcellonEditorPrefGroupUI import ExcellonEditorPrefGroupUI
+from appGUI.preferences.excellon.ExcellonExpPrefGroupUI import ExcellonExpPrefGroupUI
+from appGUI.preferences.excellon.ExcellonAdvOptPrefGroupUI import ExcellonAdvOptPrefGroupUI
+from appGUI.preferences.excellon.ExcellonOptPrefGroupUI import ExcellonOptPrefGroupUI
+from appGUI.preferences.excellon.ExcellonGenPrefGroupUI import ExcellonGenPrefGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ExcellonPreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, decimals, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QHBoxLayout()
+        self.setLayout(self.layout)
+        self.decimals = decimals
+
+        self.excellon_gen_group = ExcellonGenPrefGroupUI(decimals=self.decimals)
+        self.excellon_gen_group.setMinimumWidth(240)
+        self.excellon_opt_group = ExcellonOptPrefGroupUI(decimals=self.decimals)
+        self.excellon_opt_group.setMinimumWidth(290)
+        self.excellon_exp_group = ExcellonExpPrefGroupUI(decimals=self.decimals)
+        self.excellon_exp_group.setMinimumWidth(250)
+        self.excellon_adv_opt_group = ExcellonAdvOptPrefGroupUI(decimals=self.decimals)
+        self.excellon_adv_opt_group.setMinimumWidth(250)
+        self.excellon_editor_group = ExcellonEditorPrefGroupUI(decimals=self.decimals)
+        self.excellon_editor_group.setMinimumWidth(260)
+
+        self.vlay = QtWidgets.QVBoxLayout()
+        self.vlay.addWidget(self.excellon_opt_group)
+        self.vlay.addWidget(self.excellon_adv_opt_group)
+        self.vlay.addWidget(self.excellon_exp_group)
+
+        self.layout.addWidget(self.excellon_gen_group)
+        self.layout.addLayout(self.vlay)
+        self.layout.addWidget(self.excellon_editor_group)
+
+        self.layout.addStretch()

+ 0 - 0
postprocessors/__init__.py → appGUI/preferences/excellon/__init__.py


+ 472 - 0
appGUI/preferences/general/GeneralAPPSetGroupUI.py

@@ -0,0 +1,472 @@
+from PyQt5 import QtCore, QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCComboBox, RadioSet, OptionalInputSection, FCSpinner, \
+    FCColorEntry
+from appGUI.preferences import settings
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GeneralAPPSetGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        super(GeneralAPPSetGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("App Settings")))
+        self.decimals = decimals
+
+        theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+        if theme_settings.contains("theme"):
+            theme = theme_settings.value('theme', type=str)
+        else:
+            theme = 'white'
+
+        if theme == 'white':
+            self.resource_loc = 'assets/resources'
+        else:
+            self.resource_loc = 'assets/resources'
+
+        # Create a grid layout for the Application general settings
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        # GRID Settings
+        self.grid_label = QtWidgets.QLabel('<b>%s</b>' % _('Grid Settings'))
+        grid0.addWidget(self.grid_label, 0, 0, 1, 2)
+
+        # Grid X Entry
+        self.gridx_label = QtWidgets.QLabel('%s:' % _('X value'))
+        self.gridx_label.setToolTip(
+           _("This is the Grid snap value on X axis.")
+        )
+        self.gridx_entry = FCDoubleSpinner()
+        self.gridx_entry.set_precision(self.decimals)
+        self.gridx_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.gridx_label, 1, 0)
+        grid0.addWidget(self.gridx_entry, 1, 1)
+
+        # Grid Y Entry
+        self.gridy_label = QtWidgets.QLabel('%s:' % _('Y value'))
+        self.gridy_label.setToolTip(
+            _("This is the Grid snap value on Y axis.")
+        )
+        self.gridy_entry = FCDoubleSpinner()
+        self.gridy_entry.set_precision(self.decimals)
+        self.gridy_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.gridy_label, 2, 0)
+        grid0.addWidget(self.gridy_entry, 2, 1)
+
+        # Snap Max Entry
+        self.snap_max_label = QtWidgets.QLabel('%s:' % _('Snap Max'))
+        self.snap_max_label.setToolTip(_("Max. magnet distance"))
+        self.snap_max_dist_entry = FCDoubleSpinner()
+        self.snap_max_dist_entry.set_precision(self.decimals)
+        self.snap_max_dist_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.snap_max_label, 3, 0)
+        grid0.addWidget(self.snap_max_dist_entry, 3, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 4, 0, 1, 2)
+
+        # Workspace
+        self.workspace_label = QtWidgets.QLabel('<b>%s</b>' % _('Workspace Settings'))
+        grid0.addWidget(self.workspace_label, 5, 0, 1, 2)
+
+        self.workspace_cb = FCCheckBox('%s' % _('Active'))
+        self.workspace_cb.setToolTip(
+           _("Draw a delimiting rectangle on canvas.\n"
+             "The purpose is to illustrate the limits for our work.")
+        )
+
+        grid0.addWidget(self.workspace_cb, 6, 0, 1, 2)
+
+        self.workspace_type_lbl = QtWidgets.QLabel('%s:' % _('Size'))
+        self.workspace_type_lbl.setToolTip(
+           _("Select the type of rectangle to be used on canvas,\n"
+             "as valid workspace.")
+        )
+        self.wk_cb = FCComboBox()
+
+        grid0.addWidget(self.workspace_type_lbl, 7, 0)
+        grid0.addWidget(self.wk_cb, 7, 1)
+
+        self.pagesize = {}
+        self.pagesize.update(
+            {
+                'A0': (841, 1189),
+                'A1': (594, 841),
+                'A2': (420, 594),
+                'A3': (297, 420),
+                'A4': (210, 297),
+                'A5': (148, 210),
+                'A6': (105, 148),
+                'A7': (74, 105),
+                'A8': (52, 74),
+                'A9': (37, 52),
+                'A10': (26, 37),
+
+                'B0': (1000, 1414),
+                'B1': (707, 1000),
+                'B2': (500, 707),
+                'B3': (353, 500),
+                'B4': (250, 353),
+                'B5': (176, 250),
+                'B6': (125, 176),
+                'B7': (88, 125),
+                'B8': (62, 88),
+                'B9': (44, 62),
+                'B10': (31, 44),
+
+                'C0': (917, 1297),
+                'C1': (648, 917),
+                'C2': (458, 648),
+                'C3': (324, 458),
+                'C4': (229, 324),
+                'C5': (162, 229),
+                'C6': (114, 162),
+                'C7': (81, 114),
+                'C8': (57, 81),
+                'C9': (40, 57),
+                'C10': (28, 40),
+
+                # American paper sizes
+                'LETTER': (8.5, 11),
+                'LEGAL': (8.5, 14),
+                'ELEVENSEVENTEEN': (11, 17),
+
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5, 8),
+                'HALF_LETTER': (5.5, 8),
+                'GOV_LETTER': (8, 10.5),
+                'GOV_LEGAL': (8.5, 13),
+                'LEDGER': (17, 11),
+            }
+        )
+
+        page_size_list = list(self.pagesize.keys())
+
+        self.wk_cb.addItems(page_size_list)
+
+        # Page orientation
+        self.wk_orientation_label = QtWidgets.QLabel('%s:' % _("Orientation"))
+        self.wk_orientation_label.setToolTip(_("Can be:\n"
+                                               "- Portrait\n"
+                                               "- Landscape"))
+
+        self.wk_orientation_radio = RadioSet([{'label': _('Portrait'), 'value': 'p'},
+                                              {'label': _('Landscape'), 'value': 'l'},
+                                              ], stretch=False)
+
+        grid0.addWidget(self.wk_orientation_label, 8, 0)
+        grid0.addWidget(self.wk_orientation_radio, 8, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
+
+        # Font Size
+        self.font_size_label = QtWidgets.QLabel('<b>%s</b>' % _('Font Size'))
+        grid0.addWidget(self.font_size_label, 10, 0, 1, 2)
+
+        # Notebook Font Size
+        self.notebook_font_size_label = QtWidgets.QLabel('%s:' % _('Notebook'))
+        self.notebook_font_size_label.setToolTip(
+            _("This sets the font size for the elements found in the Notebook.\n"
+              "The notebook is the collapsible area in the left side of the GUI,\n"
+              "and include the Project, Selected and Tool tabs.")
+        )
+
+        self.notebook_font_size_spinner = FCSpinner()
+        self.notebook_font_size_spinner.set_range(8, 40)
+        self.notebook_font_size_spinner.setWrapping(True)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("notebook_font_size"):
+            self.notebook_font_size_spinner.set_value(qsettings.value('notebook_font_size', type=int))
+        else:
+            self.notebook_font_size_spinner.set_value(12)
+
+        grid0.addWidget(self.notebook_font_size_label, 11, 0)
+        grid0.addWidget(self.notebook_font_size_spinner, 11, 1)
+
+        # Axis Font Size
+        self.axis_font_size_label = QtWidgets.QLabel('%s:' % _('Axis'))
+        self.axis_font_size_label.setToolTip(
+            _("This sets the font size for canvas axis.")
+        )
+
+        self.axis_font_size_spinner = FCSpinner()
+        self.axis_font_size_spinner.set_range(0, 40)
+        self.axis_font_size_spinner.setWrapping(True)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("axis_font_size"):
+            self.axis_font_size_spinner.set_value(qsettings.value('axis_font_size', type=int))
+        else:
+            self.axis_font_size_spinner.set_value(8)
+
+        grid0.addWidget(self.axis_font_size_label, 12, 0)
+        grid0.addWidget(self.axis_font_size_spinner, 12, 1)
+
+        # TextBox Font Size
+        self.textbox_font_size_label = QtWidgets.QLabel('%s:' % _('Textbox'))
+        self.textbox_font_size_label.setToolTip(
+            _("This sets the font size for the Textbox GUI\n"
+              "elements that are used in the application.")
+        )
+
+        self.textbox_font_size_spinner = FCSpinner()
+        self.textbox_font_size_spinner.set_range(8, 40)
+        self.textbox_font_size_spinner.setWrapping(True)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("textbox_font_size"):
+            self.textbox_font_size_spinner.set_value(settings.value('textbox_font_size', type=int))
+        else:
+            self.textbox_font_size_spinner.set_value(10)
+
+        grid0.addWidget(self.textbox_font_size_label, 13, 0)
+        grid0.addWidget(self.textbox_font_size_spinner, 13, 1)
+
+        # HUD Font Size
+        self.hud_font_size_label = QtWidgets.QLabel('%s:' % _('HUD'))
+        self.hud_font_size_label.setToolTip(
+            _("This sets the font size for the Heads Up Display.")
+        )
+
+        self.hud_font_size_spinner = FCSpinner()
+        self.hud_font_size_spinner.set_range(8, 40)
+        self.hud_font_size_spinner.setWrapping(True)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("hud_font_size"):
+            self.hud_font_size_spinner.set_value(settings.value('hud_font_size', type=int))
+        else:
+            self.hud_font_size_spinner.set_value(8)
+
+        grid0.addWidget(self.hud_font_size_label, 14, 0)
+        grid0.addWidget(self.hud_font_size_spinner, 14, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 16, 0, 1, 2)
+
+        # -----------------------------------------------------------
+        # -------------- MOUSE SETTINGS -----------------------------
+        # -----------------------------------------------------------
+
+        self.mouse_lbl = QtWidgets.QLabel('<b>%s</b>' % _('Mouse Settings'))
+        grid0.addWidget(self.mouse_lbl, 21, 0, 1, 2)
+
+        # Mouse Cursor Shape
+        self.cursor_lbl = QtWidgets.QLabel('%s:' % _('Cursor Shape'))
+        self.cursor_lbl.setToolTip(
+           _("Choose a mouse cursor shape.\n"
+             "- Small -> with a customizable size.\n"
+             "- Big -> Infinite lines")
+        )
+
+        self.cursor_radio = RadioSet([
+            {"label": _("Small"), "value": "small"},
+            {"label": _("Big"), "value": "big"}
+        ], orientation='horizontal', stretch=False)
+
+        grid0.addWidget(self.cursor_lbl, 22, 0)
+        grid0.addWidget(self.cursor_radio, 22, 1)
+
+        # Mouse Cursor Size
+        self.cursor_size_lbl = QtWidgets.QLabel('%s:' % _('Cursor Size'))
+        self.cursor_size_lbl.setToolTip(
+           _("Set the size of the mouse cursor, in pixels.")
+        )
+
+        self.cursor_size_entry = FCSpinner()
+        self.cursor_size_entry.set_range(10, 70)
+        self.cursor_size_entry.setWrapping(True)
+
+        grid0.addWidget(self.cursor_size_lbl, 23, 0)
+        grid0.addWidget(self.cursor_size_entry, 23, 1)
+
+        # Cursor Width
+        self.cursor_width_lbl = QtWidgets.QLabel('%s:' % _('Cursor Width'))
+        self.cursor_width_lbl.setToolTip(
+           _("Set the line width of the mouse cursor, in pixels.")
+        )
+
+        self.cursor_width_entry = FCSpinner()
+        self.cursor_width_entry.set_range(1, 10)
+        self.cursor_width_entry.setWrapping(True)
+
+        grid0.addWidget(self.cursor_width_lbl, 24, 0)
+        grid0.addWidget(self.cursor_width_entry, 24, 1)
+
+        # Cursor Color Enable
+        self.mouse_cursor_color_cb = FCCheckBox(label='%s' % _('Cursor Color'))
+        self.mouse_cursor_color_cb.setToolTip(
+            _("Check this box to color mouse cursor.")
+        )
+        grid0.addWidget(self.mouse_cursor_color_cb, 25, 0, 1, 2)
+
+        # Cursor Color
+        self.mouse_color_label = QtWidgets.QLabel('%s:' % _('Cursor Color'))
+        self.mouse_color_label.setToolTip(
+            _("Set the color of the mouse cursor.")
+        )
+        self.mouse_cursor_entry = FCColorEntry()
+
+        grid0.addWidget(self.mouse_color_label, 26, 0)
+        grid0.addWidget(self.mouse_cursor_entry, 26, 1)
+
+        self.mois = OptionalInputSection(
+            self.mouse_cursor_color_cb,
+            [
+                self.mouse_color_label,
+                self.mouse_cursor_entry
+            ]
+        )
+        # Select mouse pan button
+        self.panbuttonlabel = QtWidgets.QLabel('%s:' % _('Pan Button'))
+        self.panbuttonlabel.setToolTip(
+            _("Select the mouse button to use for panning:\n"
+              "- MMB --> Middle Mouse Button\n"
+              "- RMB --> Right Mouse Button")
+        )
+        self.pan_button_radio = RadioSet([{'label': _('MMB'), 'value': '3'},
+                                          {'label': _('RMB'), 'value': '2'}])
+
+        grid0.addWidget(self.panbuttonlabel, 27, 0)
+        grid0.addWidget(self.pan_button_radio, 27, 1)
+
+        # Multiple Selection Modifier Key
+        self.mselectlabel = QtWidgets.QLabel('%s:' % _('Multiple Selection'))
+        self.mselectlabel.setToolTip(
+            _("Select the key used for multiple selection.")
+        )
+        self.mselect_radio = RadioSet([{'label': _('CTRL'), 'value': 'Control'},
+                                       {'label': _('SHIFT'), 'value': 'Shift'}])
+
+        grid0.addWidget(self.mselectlabel, 28, 0)
+        grid0.addWidget(self.mselect_radio, 28, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 29, 0, 1, 2)
+
+        # Delete confirmation
+        self.delete_conf_cb = FCCheckBox(_('Delete object confirmation'))
+        self.delete_conf_cb.setToolTip(
+            _("When checked the application will ask for user confirmation\n"
+              "whenever the Delete object(s) event is triggered, either by\n"
+              "menu shortcut or key shortcut.")
+        )
+        grid0.addWidget(self.delete_conf_cb, 30, 0, 1, 2)
+
+        self.allow_edit_cb = FCCheckBox(_("Allow Edit"))
+        self.allow_edit_cb.setToolTip(
+            _("When checked, the user can edit the object names in the Project Tab\n"
+              "by clicking on the object name. Active after restart.")
+        )
+        grid0.addWidget(self.allow_edit_cb, 31, 0, 1, 2)
+
+        # Open behavior
+        self.open_style_cb = FCCheckBox('%s' % _('"Open" behavior'))
+        self.open_style_cb.setToolTip(
+            _("When checked the path for the last saved file is used when saving files,\n"
+              "and the path for the last opened file is used when opening files.\n\n"
+              "When unchecked the path for opening files is the one used last: either the\n"
+              "path for saving files or the path for opening files.")
+        )
+
+        grid0.addWidget(self.open_style_cb, 32, 0, 1, 2)
+
+        # Enable/Disable ToolTips globally
+        self.toggle_tooltips_cb = FCCheckBox(label=_('Enable ToolTips'))
+        self.toggle_tooltips_cb.setToolTip(
+            _("Check this box if you want to have toolTips displayed\n"
+              "when hovering with mouse over items throughout the App.")
+        )
+
+        grid0.addWidget(self.toggle_tooltips_cb, 33, 0, 1, 2)
+
+        # Machinist settings that allow unsafe settings
+        self.machinist_cb = FCCheckBox(_("Allow Machinist Unsafe Settings"))
+        self.machinist_cb.setToolTip(
+            _("If checked, some of the application settings will be allowed\n"
+              "to have values that are usually unsafe to use.\n"
+              "Like Z travel negative values or Z Cut positive values.\n"
+              "It will applied at the next application start.\n"
+              "<<WARNING>>: Don't change this unless you know what you are doing !!!")
+        )
+
+        grid0.addWidget(self.machinist_cb, 34, 0, 1, 2)
+
+        # Bookmarks Limit in the Help Menu
+        self.bm_limit_spinner = FCSpinner()
+        self.bm_limit_spinner.set_range(0, 9999)
+        self.bm_limit_label = QtWidgets.QLabel('%s:' % _('Bookmarks limit'))
+        self.bm_limit_label.setToolTip(
+            _("The maximum number of bookmarks that may be installed in the menu.\n"
+              "The number of bookmarks in the bookmark manager may be greater\n"
+              "but the menu will hold only so much.")
+        )
+
+        grid0.addWidget(self.bm_limit_label, 35, 0)
+        grid0.addWidget(self.bm_limit_spinner, 35, 1)
+
+        # Activity monitor icon
+        self.activity_label = QtWidgets.QLabel('%s:' % _("Activity Icon"))
+        self.activity_label.setToolTip(
+            _("Select the GIF that show activity when FlatCAM is active.")
+        )
+        self.activity_combo = FCComboBox()
+        self.activity_combo.addItems(['Ball black', 'Ball green', 'Arrow green', 'Eclipse green'])
+
+        grid0.addWidget(self.activity_label, 36, 0)
+        grid0.addWidget(self.activity_combo, 36, 1)
+
+        self.layout.addStretch()
+
+        self.mouse_cursor_color_cb.stateChanged.connect(self.on_mouse_cursor_color_enable)
+        self.mouse_cursor_entry.editingFinished.connect(self.on_mouse_cursor_entry)
+
+    def on_mouse_cursor_color_enable(self, val):
+        if val:
+            self.app.cursor_color_3D = self.app.defaults["global_cursor_color"]
+        else:
+            theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+            if theme_settings.contains("theme"):
+                theme = theme_settings.value('theme', type=str)
+            else:
+                theme = 'white'
+
+            if theme == 'white':
+                self.app.cursor_color_3D = 'black'
+            else:
+                self.app.cursor_color_3D = 'gray'
+
+    def on_mouse_cursor_entry(self):
+        self.app.defaults['global_cursor_color'] = self.mouse_cursor_entry.get_value()
+        self.app.cursor_color_3D = self.app.defaults["global_cursor_color"]

+ 401 - 0
appGUI/preferences/general/GeneralAppPrefGroupUI.py

@@ -0,0 +1,401 @@
+import sys
+
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCSpinner, FCCheckBox, FCComboBox, FCButton, OptionalInputSection, \
+    FCDoubleSpinner
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GeneralAppPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        super(GeneralAppPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(_("App Preferences"))
+        self.decimals = decimals
+
+        # Create a form layout for the Application general settings
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        # Units for FlatCAM
+        self.unitslabel = QtWidgets.QLabel('<span style="color:red;"><b>%s:</b></span>' % _('Units'))
+        self.unitslabel.setToolTip(_("The default value for FlatCAM units.\n"
+                                     "Whatever is selected here is set every time\n"
+                                     "FlatCAM is started."))
+        self.units_radio = RadioSet([{'label': _('MM'), 'value': 'MM'},
+                                     {'label': _('IN'), 'value': 'IN'}])
+
+        grid0.addWidget(self.unitslabel, 0, 0)
+        grid0.addWidget(self.units_radio, 0, 1)
+
+        # Precision Metric
+        self.precision_metric_label = QtWidgets.QLabel('%s:' % _('Precision MM'))
+        self.precision_metric_label.setToolTip(
+            _("The number of decimals used throughout the application\n"
+              "when the set units are in METRIC system.\n"
+              "Any change here require an application restart.")
+        )
+        self.precision_metric_entry = FCSpinner()
+        self.precision_metric_entry.set_range(2, 16)
+        self.precision_metric_entry.setWrapping(True)
+
+        grid0.addWidget(self.precision_metric_label, 1, 0)
+        grid0.addWidget(self.precision_metric_entry, 1, 1)
+
+        # Precision Inch
+        self.precision_inch_label = QtWidgets.QLabel('%s:' % _('Precision Inch'))
+        self.precision_inch_label.setToolTip(
+            _("The number of decimals used throughout the application\n"
+              "when the set units are in INCH system.\n"
+              "Any change here require an application restart.")
+        )
+        self.precision_inch_entry = FCSpinner()
+        self.precision_inch_entry.set_range(2, 16)
+        self.precision_inch_entry.setWrapping(True)
+
+        grid0.addWidget(self.precision_inch_label, 2, 0)
+        grid0.addWidget(self.precision_inch_entry, 2, 1)
+
+        # Graphic Engine for FlatCAM
+        self.ge_label = QtWidgets.QLabel('<b>%s:</b>' % _('Graphic Engine'))
+        self.ge_label.setToolTip(_("Choose what graphic engine to use in FlatCAM.\n"
+                                   "Legacy(2D) -> reduced functionality, slow performance but enhanced compatibility.\n"
+                                   "OpenGL(3D) -> full functionality, high performance\n"
+                                   "Some graphic cards are too old and do not work in OpenGL(3D) mode, like:\n"
+                                   "Intel HD3000 or older. In this case the plot area will be black therefore\n"
+                                   "use the Legacy(2D) mode."))
+        self.ge_radio = RadioSet([{'label': _('Legacy(2D)'), 'value': '2D'},
+                                  {'label': _('OpenGL(3D)'), 'value': '3D'}],
+                                 orientation='vertical')
+
+        grid0.addWidget(self.ge_label, 3, 0)
+        grid0.addWidget(self.ge_radio, 3, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 4, 0, 1, 2)
+
+        # Application Level for FlatCAM
+        self.app_level_label = QtWidgets.QLabel('<span style="color:red;"><b>%s:</b></span>' % _('APPLICATION LEVEL'))
+        self.app_level_label.setToolTip(_("Choose the default level of usage for FlatCAM.\n"
+                                          "BASIC level -> reduced functionality, best for beginner's.\n"
+                                          "ADVANCED level -> full functionality.\n\n"
+                                          "The choice here will influence the parameters in\n"
+                                          "the Selected Tab for all kinds of FlatCAM objects."))
+        self.app_level_radio = RadioSet([{'label': _('Basic'), 'value': 'b'},
+                                         {'label': _('Advanced'), 'value': 'a'}])
+
+        grid0.addWidget(self.app_level_label, 5, 0, 1, 2)
+        grid0.addWidget(self.app_level_radio, 6, 0, 1, 2)
+
+        # Portability for FlatCAM
+        self.portability_cb = FCCheckBox('%s' % _('Portable app'))
+        self.portability_cb.setToolTip(_("Choose if the application should run as portable.\n\n"
+                                         "If Checked the application will run portable,\n"
+                                         "which means that the preferences files will be saved\n"
+                                         "in the application folder, in the lib\\config subfolder."))
+
+        grid0.addWidget(self.portability_cb, 7, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 8, 0, 1, 2)
+
+        # Languages for FlatCAM
+        self.languagelabel = QtWidgets.QLabel('<b>%s</b>' % _('Languages'))
+        self.languagelabel.setToolTip(_("Set the language used throughout FlatCAM."))
+        self.language_cb = FCComboBox()
+
+        grid0.addWidget(self.languagelabel, 9, 0, 1, 2)
+        grid0.addWidget(self.language_cb, 10, 0, 1, 2)
+
+        self.language_apply_btn = FCButton(_("Apply Language"))
+        self.language_apply_btn.setToolTip(_("Set the language used throughout FlatCAM.\n"
+                                             "The app will restart after click."))
+
+        grid0.addWidget(self.language_apply_btn, 15, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 16, 0, 1, 2)
+
+        # -----------------------------------------------------------
+        # ----------- APPLICATION STARTUP SETTINGS ------------------
+        # -----------------------------------------------------------
+
+        self.startup_label = QtWidgets.QLabel('<b>%s</b>' % _('Startup Settings'))
+        grid0.addWidget(self.startup_label, 17, 0, 1, 2)
+
+        # Splash Screen
+        self.splash_cb = FCCheckBox('%s' % _('Splash Screen'))
+        self.splash_cb.setToolTip(
+            _("Enable display of the splash screen at application startup.")
+        )
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.value("splash_screen"):
+            self.splash_cb.set_value(True)
+        else:
+            self.splash_cb.set_value(False)
+
+        grid0.addWidget(self.splash_cb, 18, 0, 1, 2)
+
+        # Sys Tray Icon
+        self.systray_cb = FCCheckBox('%s' % _('Sys Tray Icon'))
+        self.systray_cb.setToolTip(
+            _("Enable display of FlatCAM icon in Sys Tray.")
+        )
+        grid0.addWidget(self.systray_cb, 19, 0, 1, 2)
+
+        # Shell StartUp CB
+        self.shell_startup_cb = FCCheckBox(label='%s' % _('Show Shell'))
+        self.shell_startup_cb.setToolTip(
+            _("Check this box if you want the shell to\n"
+              "start automatically at startup.")
+        )
+
+        grid0.addWidget(self.shell_startup_cb, 20, 0, 1, 2)
+
+        # Project at StartUp CB
+        self.project_startup_cb = FCCheckBox(label='%s' % _('Show Project'))
+        self.project_startup_cb.setToolTip(
+            _("Check this box if you want the project/selected/tool tab area to\n"
+              "to be shown automatically at startup.")
+        )
+        grid0.addWidget(self.project_startup_cb, 21, 0, 1, 2)
+
+        # Version Check CB
+        self.version_check_cb = FCCheckBox(label='%s' % _('Version Check'))
+        self.version_check_cb.setToolTip(
+            _("Check this box if you want to check\n"
+              "for a new version automatically at startup.")
+        )
+
+        grid0.addWidget(self.version_check_cb, 22, 0, 1, 2)
+
+        # Send Stats CB
+        self.send_stats_cb = FCCheckBox(label='%s' % _('Send Statistics'))
+        self.send_stats_cb.setToolTip(
+            _("Check this box if you agree to send anonymous\n"
+              "stats automatically at startup, to help improve FlatCAM.")
+        )
+
+        grid0.addWidget(self.send_stats_cb, 23, 0, 1, 2)
+
+        self.ois_version_check = OptionalInputSection(self.version_check_cb, [self.send_stats_cb])
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 24, 0, 1, 2)
+
+        # Worker Numbers
+        self.worker_number_label = QtWidgets.QLabel('%s:' % _('Workers number'))
+        self.worker_number_label.setToolTip(
+            _("The number of Qthreads made available to the App.\n"
+              "A bigger number may finish the jobs more quickly but\n"
+              "depending on your computer speed, may make the App\n"
+              "unresponsive. Can have a value between 2 and 16.\n"
+              "Default value is 2.\n"
+              "After change, it will be applied at next App start.")
+        )
+        self.worker_number_sb = FCSpinner()
+        self.worker_number_sb.set_range(2, 16)
+
+        grid0.addWidget(self.worker_number_label, 25, 0)
+        grid0.addWidget(self.worker_number_sb, 25, 1)
+
+        # Geometric tolerance
+        tol_label = QtWidgets.QLabel('%s:' % _("Geo Tolerance"))
+        tol_label.setToolTip(_(
+            "This value can counter the effect of the Circle Steps\n"
+            "parameter. Default value is 0.005.\n"
+            "A lower value will increase the detail both in image\n"
+            "and in Gcode for the circles, with a higher cost in\n"
+            "performance. Higher value will provide more\n"
+            "performance at the expense of level of detail."
+        ))
+        self.tol_entry = FCDoubleSpinner()
+        self.tol_entry.setSingleStep(0.001)
+        self.tol_entry.set_precision(6)
+
+        grid0.addWidget(tol_label, 26, 0)
+        grid0.addWidget(self.tol_entry, 26, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 27, 0, 1, 2)
+
+        # Save Settings
+        self.save_label = QtWidgets.QLabel('<b>%s</b>' % _("Save Settings"))
+        grid0.addWidget(self.save_label, 28, 0, 1, 2)
+
+        # Save compressed project CB
+        self.save_type_cb = FCCheckBox(_('Save Compressed Project'))
+        self.save_type_cb.setToolTip(
+            _("Whether to save a compressed or uncompressed project.\n"
+              "When checked it will save a compressed FlatCAM project.")
+        )
+
+        grid0.addWidget(self.save_type_cb, 29, 0, 1, 2)
+
+        # Project LZMA Comppression Level
+        self.compress_spinner = FCSpinner()
+        self.compress_spinner.set_range(0, 9)
+        self.compress_label = QtWidgets.QLabel('%s:' % _('Compression'))
+        self.compress_label.setToolTip(
+            _("The level of compression used when saving\n"
+              "a FlatCAM project. Higher value means better compression\n"
+              "but require more RAM usage and more processing time.")
+        )
+
+        grid0.addWidget(self.compress_label, 30, 0)
+        grid0.addWidget(self.compress_spinner, 30, 1)
+
+        self.proj_ois = OptionalInputSection(self.save_type_cb, [self.compress_label, self.compress_spinner], True)
+
+        # Auto save CB
+        self.autosave_cb = FCCheckBox(_('Enable Auto Save'))
+        self.autosave_cb.setToolTip(
+            _("Check to enable the autosave feature.\n"
+              "When enabled, the application will try to save a project\n"
+              "at the set interval.")
+        )
+
+        grid0.addWidget(self.autosave_cb, 31, 0, 1, 2)
+
+        # Auto Save Timeout Interval
+        self.autosave_entry = FCSpinner()
+        self.autosave_entry.set_range(0, 9999999)
+        self.autosave_label = QtWidgets.QLabel('%s:' % _('Interval'))
+        self.autosave_label.setToolTip(
+            _("Time interval for autosaving. In milliseconds.\n"
+              "The application will try to save periodically but only\n"
+              "if the project was saved manually at least once.\n"
+              "While active, some operations may block this feature.")
+        )
+
+        grid0.addWidget(self.autosave_label, 32, 0)
+        grid0.addWidget(self.autosave_entry, 32, 1)
+
+        # self.as_ois = OptionalInputSection(self.autosave_cb, [self.autosave_label, self.autosave_entry], True)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 33, 0, 1, 2)
+
+        self.pdf_param_label = QtWidgets.QLabel('<B>%s:</b>' % _("Text to PDF parameters"))
+        self.pdf_param_label.setToolTip(
+            _("Used when saving text in Code Editor or in FlatCAM Document objects.")
+        )
+        grid0.addWidget(self.pdf_param_label, 34, 0, 1, 2)
+
+        # Top Margin value
+        self.tmargin_entry = FCDoubleSpinner()
+        self.tmargin_entry.set_precision(self.decimals)
+        self.tmargin_entry.set_range(0.0000, 10000.0000)
+
+        self.tmargin_label = QtWidgets.QLabel('%s:' % _("Top Margin"))
+        self.tmargin_label.setToolTip(
+            _("Distance between text body and the top of the PDF file.")
+        )
+
+        grid0.addWidget(self.tmargin_label, 35, 0)
+        grid0.addWidget(self.tmargin_entry, 35, 1)
+
+        # Bottom Margin value
+        self.bmargin_entry = FCDoubleSpinner()
+        self.bmargin_entry.set_precision(self.decimals)
+        self.bmargin_entry.set_range(0.0000, 10000.0000)
+
+        self.bmargin_label = QtWidgets.QLabel('%s:' % _("Bottom Margin"))
+        self.bmargin_label.setToolTip(
+            _("Distance between text body and the bottom of the PDF file.")
+        )
+
+        grid0.addWidget(self.bmargin_label, 36, 0)
+        grid0.addWidget(self.bmargin_entry, 36, 1)
+
+        # Left Margin value
+        self.lmargin_entry = FCDoubleSpinner()
+        self.lmargin_entry.set_precision(self.decimals)
+        self.lmargin_entry.set_range(0.0000, 10000.0000)
+
+        self.lmargin_label = QtWidgets.QLabel('%s:' % _("Left Margin"))
+        self.lmargin_label.setToolTip(
+            _("Distance between text body and the left of the PDF file.")
+        )
+
+        grid0.addWidget(self.lmargin_label, 37, 0)
+        grid0.addWidget(self.lmargin_entry, 37, 1)
+
+        # Right Margin value
+        self.rmargin_entry = FCDoubleSpinner()
+        self.rmargin_entry.set_precision(self.decimals)
+        self.rmargin_entry.set_range(0.0000, 10000.0000)
+
+        self.rmargin_label = QtWidgets.QLabel('%s:' % _("Right Margin"))
+        self.rmargin_label.setToolTip(
+            _("Distance between text body and the right of the PDF file.")
+        )
+
+        grid0.addWidget(self.rmargin_label, 38, 0)
+        grid0.addWidget(self.rmargin_entry, 38, 1)
+
+        self.layout.addStretch()
+
+        if sys.platform != 'win32':
+            self.portability_cb.hide()
+
+        # splash screen button signal
+        self.splash_cb.stateChanged.connect(self.on_splash_changed)
+
+        # Monitor the checkbox from the Application Defaults Tab and show the TCL shell or not depending on it's value
+        self.shell_startup_cb.clicked.connect(self.on_toggle_shell_from_settings)
+
+        self.language_apply_btn.clicked.connect(lambda: fcTranslate.on_language_apply_click(app=self.app, restart=True))
+
+    def on_toggle_shell_from_settings(self, state):
+        """
+        Toggle shell ui: if is visible close it, if it is closed then open it
+
+        :return: None
+        """
+
+        if state is True:
+            if not self.app.ui.shell_dock.isVisible():
+                self.app.ui.shell_dock.show()
+        else:
+            if self.app.ui.shell_dock.isVisible():
+                self.app.ui.shell_dock.hide()
+
+    @staticmethod
+    def on_splash_changed(state):
+        qsettings = QSettings("Open Source", "FlatCAM")
+        qsettings.setValue('splash_screen', 1) if state else qsettings.setValue('splash_screen', 0)
+
+        # This will write the setting to the platform specific storage.
+        del qsettings

+ 316 - 0
appGUI/preferences/general/GeneralAppSettingsGroupUI.py

@@ -0,0 +1,316 @@
+
+from PyQt5 import QtCore
+from PyQt5.QtCore import QSettings
+from appGUI.GUIElements import OptionalInputSection
+from appGUI.preferences import settings
+from appGUI.preferences.OptionUI import *
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI2
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class GeneralAppSettingsGroupUI(OptionsGroupUI2):
+    def __init__(self, decimals=4, **kwargs):
+        self.decimals = decimals
+        self.pagesize = {}
+        self.pagesize.update(
+            {
+                'A0': (841, 1189),
+                'A1': (594, 841),
+                'A2': (420, 594),
+                'A3': (297, 420),
+                'A4': (210, 297),
+                'A5': (148, 210),
+                'A6': (105, 148),
+                'A7': (74, 105),
+                'A8': (52, 74),
+                'A9': (37, 52),
+                'A10': (26, 37),
+
+                'B0': (1000, 1414),
+                'B1': (707, 1000),
+                'B2': (500, 707),
+                'B3': (353, 500),
+                'B4': (250, 353),
+                'B5': (176, 250),
+                'B6': (125, 176),
+                'B7': (88, 125),
+                'B8': (62, 88),
+                'B9': (44, 62),
+                'B10': (31, 44),
+
+                'C0': (917, 1297),
+                'C1': (648, 917),
+                'C2': (458, 648),
+                'C3': (324, 458),
+                'C4': (229, 324),
+                'C5': (162, 229),
+                'C6': (114, 162),
+                'C7': (81, 114),
+                'C8': (57, 81),
+                'C9': (40, 57),
+                'C10': (28, 40),
+
+                # American paper sizes
+                'LETTER': (8.5, 11),
+                'LEGAL': (8.5, 14),
+                'ELEVENSEVENTEEN': (11, 17),
+
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5, 8),
+                'HALF_LETTER': (5.5, 8),
+                'GOV_LETTER': (8, 10.5),
+                'GOV_LEGAL': (8.5, 13),
+                'LEDGER': (17, 11),
+            }
+        )
+        super().__init__(**kwargs)
+
+        self.setTitle(str(_("App Settings")))
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+
+        self.notebook_font_size_field = self.option_dict()["notebook_font_size"].get_field()
+        if qsettings.contains("notebook_font_size"):
+            self.notebook_font_size_field.set_value(qsettings.value('notebook_font_size', type=int))
+        else:
+            self.notebook_font_size_field.set_value(12)
+
+        self.axis_font_size_field = self.option_dict()["axis_font_size"].get_field()
+        if qsettings.contains("axis_font_size"):
+            self.axis_font_size_field.set_value(qsettings.value('axis_font_size', type=int))
+        else:
+            self.axis_font_size_field.set_value(8)
+
+        self.textbox_font_size_field = self.option_dict()["textbox_font_size"].get_field()
+        if qsettings.contains("textbox_font_size"):
+            self.textbox_font_size_field.set_value(settings.value('textbox_font_size', type=int))
+        else:
+            self.textbox_font_size_field.set_value(10)
+
+        self.workspace_enabled_field = self.option_dict()["global_workspace"].get_field()
+        self.workspace_type_field = self.option_dict()["global_workspaceT"].get_field()
+        self.workspace_type_label = self.option_dict()["global_workspaceT"].label_widget
+        self.workspace_orientation_field = self.option_dict()["global_workspace_orientation"].get_field()
+        self.workspace_orientation_label = self.option_dict()["global_workspace_orientation"].label_widget
+        self.wks = OptionalInputSection(
+            self.workspace_enabled_field,
+            [
+                self.workspace_type_label,
+                self.workspace_type_field,
+                self.workspace_orientation_label,
+                self.workspace_orientation_field
+            ]
+        )
+
+        self.mouse_cursor_color_enabled_field = self.option_dict()["global_cursor_color_enabled"].get_field()
+        self.mouse_cursor_color_field = self.option_dict()["global_cursor_color"].get_field()
+        self.mouse_cursor_color_label = self.option_dict()["global_cursor_color"].label_widget
+        self.mois = OptionalInputSection(
+            self.mouse_cursor_color_enabled_field,
+            [
+                self.mouse_cursor_color_label,
+                self.mouse_cursor_color_field
+            ]
+        )
+        self.mouse_cursor_color_enabled_field.stateChanged.connect(self.on_mouse_cursor_color_enable)
+        self.mouse_cursor_color_field.entry.editingFinished.connect(self.on_mouse_cursor_entry)
+
+    def build_options(self) -> [OptionUI]:
+        return [
+            HeadingOptionUI(label_text="Grid Settings", label_tooltip=None),
+            DoubleSpinnerOptionUI(
+                option="global_gridx",
+                label_text="X value",
+                label_tooltip="This is the Grid snap value on X axis.",
+                step=0.1,
+                decimals=self.decimals
+            ),
+            DoubleSpinnerOptionUI(
+                option="global_gridy",
+                label_text='Y value',
+                label_tooltip="This is the Grid snap value on Y axis.",
+                step=0.1,
+                decimals=self.decimals
+            ),
+            DoubleSpinnerOptionUI(
+                option="global_snap_max",
+                label_text="Snap Max",
+                label_tooltip="Max. magnet distance",
+                step=0.1,
+                decimals=self.decimals
+            ),
+            SeparatorOptionUI(),
+
+            HeadingOptionUI(label_text="Workspace Settings", label_tooltip=None),
+            CheckboxOptionUI(
+                option="global_workspace",
+                label_text="Active",
+                label_tooltip="Draw a delimiting rectangle on canvas.\n"
+                              "The purpose is to illustrate the limits for our work."
+            ),
+            ComboboxOptionUI(
+                option="global_workspaceT",
+                label_text="Size",
+                label_tooltip="Select the type of rectangle to be used on canvas,\nas valid workspace.",
+                choices=list(self.pagesize.keys())
+            ),
+            RadioSetOptionUI(
+                option="global_workspace_orientation",
+                label_text="Orientation",
+                label_tooltip="Can be:\n- Portrait\n- Landscape",
+                choices=[
+                    {'label': _('Portrait'), 'value': 'p'},
+                    {'label': _('Landscape'), 'value': 'l'},
+                ]
+            ),
+            # FIXME enabling OptionalInputSection ??
+            SeparatorOptionUI(),
+
+            HeadingOptionUI(label_text="Font Size", label_tooltip=None),
+            SpinnerOptionUI(
+                option="notebook_font_size",
+                label_text="Notebook",
+                label_tooltip="This sets the font size for the elements found in the Notebook.\n"
+                              "The notebook is the collapsible area in the left side of the GUI,\n"
+                              "and include the Project, Selected and Tool tabs.",
+                min_value=8, max_value=40, step=1
+            ),
+            SpinnerOptionUI(
+                option="axis_font_size",
+                label_text="Axis",
+                label_tooltip="This sets the font size for canvas axis.",
+                min_value=8, max_value=40, step=1
+            ),
+            SpinnerOptionUI(
+                option="textbox_font_size",
+                label_text="Textbox",
+                label_tooltip="This sets the font size for the Textbox GUI\n"
+                              "elements that are used in the application.",
+                min_value=8, max_value=40, step=1
+            ),
+            SeparatorOptionUI(),
+
+            HeadingOptionUI(label_text="Mouse Settings", label_tooltip=None),
+            RadioSetOptionUI(
+                option="global_cursor_type",
+                label_text="Cursor Shape",
+                label_tooltip="Choose a mouse cursor shape.\n"
+                              "- Small -> with a customizable size.\n"
+                              "- Big -> Infinite lines",
+                choices=[
+                    {"label": _("Small"), "value": "small"},
+                    {"label": _("Big"), "value": "big"}
+                ]
+            ),
+            SpinnerOptionUI(
+                option="global_cursor_size",
+                label_text="Cursor Size",
+                label_tooltip="Set the size of the mouse cursor, in pixels.",
+                min_value=10, max_value=70, step=1
+            ),
+            SpinnerOptionUI(
+                option="global_cursor_width",
+                label_text="Cursor Width",
+                label_tooltip="Set the line width of the mouse cursor, in pixels.",
+                min_value=1, max_value=10, step=1
+            ),
+            CheckboxOptionUI(
+                option="global_cursor_color_enabled",
+                label_text="Cursor Color",
+                label_tooltip="Check this box to color mouse cursor."
+            ),
+            ColorOptionUI(
+                option="global_cursor_color",
+                label_text="Cursor Color",
+                label_tooltip="Set the color of the mouse cursor."
+            ),
+            # FIXME enabling of cursor color
+            RadioSetOptionUI(
+                option="global_pan_button",
+                label_text="Pan Button",
+                label_tooltip="Select the mouse button to use for panning:\n"
+                              "- MMB --> Middle Mouse Button\n"
+                              "- RMB --> Right Mouse Button",
+                choices=[{'label': _('MMB'), 'value': '3'},
+                         {'label': _('RMB'), 'value': '2'}]
+            ),
+            RadioSetOptionUI(
+                option="global_mselect_key",
+                label_text="Multiple Selection",
+                label_tooltip="Select the key used for multiple selection.",
+                choices=[{'label': _('CTRL'),  'value': 'Control'},
+                         {'label': _('SHIFT'), 'value': 'Shift'}]
+            ),
+            SeparatorOptionUI(),
+
+            CheckboxOptionUI(
+                option="global_delete_confirmation",
+                label_text="Delete object confirmation",
+                label_tooltip="When checked the application will ask for user confirmation\n"
+                              "whenever the Delete object(s) event is triggered, either by\n"
+                              "menu shortcut or key shortcut."
+            ),
+            CheckboxOptionUI(
+                option="global_open_style",
+                label_text='"Open" behavior',
+                label_tooltip="When checked the path for the last saved file is used when saving files,\n"
+                              "and the path for the last opened file is used when opening files.\n\n"
+                              "When unchecked the path for opening files is the one used last: either the\n"
+                              "path for saving files or the path for opening files."
+            ),
+            CheckboxOptionUI(
+                option="global_toggle_tooltips",
+                label_text="Enable ToolTips",
+                label_tooltip="Check this box if you want to have toolTips displayed\n"
+                              "when hovering with mouse over items throughout the App."
+            ),
+            CheckboxOptionUI(
+                option="global_machinist_setting",
+                label_text="Allow Machinist Unsafe Settings",
+                label_tooltip="If checked, some of the application settings will be allowed\n"
+                              "to have values that are usually unsafe to use.\n"
+                              "Like Z travel negative values or Z Cut positive values.\n"
+                              "It will applied at the next application start.\n"
+                              "<<WARNING>>: Don't change this unless you know what you are doing !!!"
+            ),
+            SpinnerOptionUI(
+                option="global_bookmarks_limit",
+                label_text="Bookmarks limit",
+                label_tooltip="The maximum number of bookmarks that may be installed in the menu.\n"
+                              "The number of bookmarks in the bookmark manager may be greater\n"
+                              "but the menu will hold only so much.",
+                min_value=0, max_value=9999, step=1
+            ),
+            ComboboxOptionUI(
+                option="global_activity_icon",
+                label_text="Activity Icon",
+                label_tooltip="Select the GIF that show activity when FlatCAM is active.",
+                choices=['Ball black', 'Ball green', 'Arrow green', 'Eclipse green']
+            )
+
+        ]
+
+    def on_mouse_cursor_color_enable(self, val):
+        if val:
+            self.app.cursor_color_3D = self.app.defaults["global_cursor_color"]
+        else:
+            theme_settings = QtCore.QSettings("Open Source", "FlatCAM")
+            if theme_settings.contains("theme"):
+                theme = theme_settings.value('theme', type=str)
+            else:
+                theme = 'white'
+
+            if theme == 'white':
+                self.app.cursor_color_3D = 'black'
+            else:
+                self.app.cursor_color_3D = 'gray'
+
+    def on_mouse_cursor_entry(self):
+        self.app.defaults['global_cursor_color'] = self.mouse_cursor_color_field.get_value()
+        self.app.cursor_color_3D = self.app.defaults["global_cursor_color"]

+ 409 - 0
appGUI/preferences/general/GeneralGUIPrefGroupUI.py

@@ -0,0 +1,409 @@
+from PyQt5 import QtWidgets, QtCore, QtGui
+from PyQt5.QtCore import QSettings, Qt
+
+from appGUI.GUIElements import RadioSet, FCCheckBox, FCComboBox, FCSliderWithSpinner, FCColorEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GeneralGUIPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        super(GeneralGUIPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("GUI Preferences")))
+        self.decimals = decimals
+
+        # Create a grid layout for the Application general settings
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        # Theme selection
+        self.theme_label = QtWidgets.QLabel('%s:' % _('Theme'))
+        self.theme_label.setToolTip(
+            _("Select a theme for the application.\n"
+              "It will theme the plot area.")
+        )
+
+        self.theme_radio = RadioSet([
+            {"label": _("Light"), "value": "white"},
+            {"label": _("Dark"), "value": "black"}
+        ], orientation='vertical')
+
+        grid0.addWidget(self.theme_label, 0, 0)
+        grid0.addWidget(self.theme_radio, 0, 1)
+
+        # Enable Gray Icons
+        self.gray_icons_cb = FCCheckBox('%s' % _('Use Gray Icons'))
+        self.gray_icons_cb.setToolTip(
+            _("Check this box to use a set of icons with\n"
+              "a lighter (gray) color. To be used when a\n"
+              "full dark theme is applied.")
+        )
+        grid0.addWidget(self.gray_icons_cb, 1, 0, 1, 3)
+
+        # self.theme_button = FCButton(_("Apply Theme"))
+        # self.theme_button.setToolTip(
+        #     _("Select a theme for FlatCAM.\n"
+        #       "It will theme the plot area.\n"
+        #       "The application will restart after change.")
+        # )
+        # grid0.addWidget(self.theme_button, 2, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 3, 0, 1, 2)
+
+        # Layout selection
+        self.layout_label = QtWidgets.QLabel('%s:' % _('Layout'))
+        self.layout_label.setToolTip(
+            _("Select a layout for the application.\n"
+              "It is applied immediately.")
+        )
+        self.layout_combo = FCComboBox()
+        # don't translate the QCombo items as they are used in QSettings and identified by name
+        self.layout_combo.addItem("standard")
+        self.layout_combo.addItem("compact")
+        self.layout_combo.addItem("minimal")
+
+        grid0.addWidget(self.layout_label, 4, 0)
+        grid0.addWidget(self.layout_combo, 4, 1)
+
+        # Set the current index for layout_combo
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("layout"):
+            layout = qsettings.value('layout', type=str)
+            idx = self.layout_combo.findText(layout.capitalize())
+            self.layout_combo.setCurrentIndex(idx)
+
+        # Style selection
+        self.style_label = QtWidgets.QLabel('%s:' % _('Style'))
+        self.style_label.setToolTip(
+            _("Select a style for the application.\n"
+              "It will be applied at the next app start.")
+        )
+        self.style_combo = FCComboBox()
+        self.style_combo.addItems(QtWidgets.QStyleFactory.keys())
+        # find current style
+        index = self.style_combo.findText(QtWidgets.qApp.style().objectName(), QtCore.Qt.MatchFixedString)
+        self.style_combo.setCurrentIndex(index)
+        self.style_combo.activated[str].connect(self.handle_style)
+
+        grid0.addWidget(self.style_label, 5, 0)
+        grid0.addWidget(self.style_combo, 5, 1)
+
+        # Enable High DPI Support
+        self.hdpi_cb = FCCheckBox('%s' % _('HDPI Support'))
+        self.hdpi_cb.setToolTip(
+            _("Enable High DPI support for the application.\n"
+              "It will be applied at the next app start.")
+        )
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("hdpi"):
+            self.hdpi_cb.set_value(qsettings.value('hdpi', type=int))
+        else:
+            self.hdpi_cb.set_value(False)
+        self.hdpi_cb.stateChanged.connect(self.handle_hdpi)
+
+        grid0.addWidget(self.hdpi_cb, 6, 0, 1, 3)
+
+        # Enable Hover box
+        self.hover_cb = FCCheckBox('%s' % _('Hover Shape'))
+        self.hover_cb.setToolTip(
+            _("Enable display of a hover shape for the application objects.\n"
+              "It is displayed whenever the mouse cursor is hovering\n"
+              "over any kind of not-selected object.")
+        )
+        grid0.addWidget(self.hover_cb, 8, 0, 1, 3)
+
+        # Enable Selection box
+        self.selection_cb = FCCheckBox('%s' % _('Selection Shape'))
+        self.selection_cb.setToolTip(
+            _("Enable the display of a selection shape for the application objects.\n"
+              "It is displayed whenever the mouse selects an object\n"
+              "either by clicking or dragging mouse from left to right or\n"
+              "right to left.")
+        )
+        grid0.addWidget(self.selection_cb, 9, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 14, 0, 1, 2)
+
+        # Plot Selection (left - right) Color
+        self.sel_lr_label = QtWidgets.QLabel('<b>%s</b>' % _('Left-Right Selection Color'))
+        grid0.addWidget(self.sel_lr_label, 15, 0, 1, 2)
+
+        self.sl_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
+        self.sl_color_label.setToolTip(
+            _("Set the line color for the 'left to right' selection box.")
+        )
+        self.sl_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.sl_color_label, 16, 0)
+        grid0.addWidget(self.sl_color_entry, 16, 1)
+
+        self.sf_color_label = QtWidgets.QLabel('%s:' % _('Fill'))
+        self.sf_color_label.setToolTip(
+            _("Set the fill color for the selection box\n"
+              "in case that the selection is done from left to right.\n"
+              "First 6 digits are the color and the last 2\n"
+              "digits are for alpha (transparency) level.")
+        )
+        self.sf_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.sf_color_label, 17, 0)
+        grid0.addWidget(self.sf_color_entry, 17, 1)
+
+        # Plot Selection (left - right) Fill Transparency Level
+        self.left_right_alpha_label = QtWidgets.QLabel('%s:' % _('Alpha'))
+        self.left_right_alpha_label.setToolTip(
+            _("Set the fill transparency for the 'left to right' selection box.")
+        )
+        self.left_right_alpha_entry = FCSliderWithSpinner(0, 255, 1)
+
+        grid0.addWidget(self.left_right_alpha_label, 18, 0)
+        grid0.addWidget(self.left_right_alpha_entry, 18, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 19, 0, 1, 2)
+
+        # Plot Selection (left - right) Color
+        self.sel_rl_label = QtWidgets.QLabel('<b>%s</b>' % _('Right-Left Selection Color'))
+        grid0.addWidget(self.sel_rl_label, 20, 0, 1, 2)
+
+        # Plot Selection (right - left) Line Color
+        self.alt_sl_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
+        self.alt_sl_color_label.setToolTip(
+            _("Set the line color for the 'right to left' selection box.")
+        )
+        self.alt_sl_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.alt_sl_color_label, 21, 0)
+        grid0.addWidget(self.alt_sl_color_entry, 21, 1)
+
+        # Plot Selection (right - left) Fill Color
+        self.alt_sf_color_label = QtWidgets.QLabel('%s:' % _('Fill'))
+        self.alt_sf_color_label.setToolTip(
+            _("Set the fill color for the selection box\n"
+              "in case that the selection is done from right to left.\n"
+              "First 6 digits are the color and the last 2\n"
+              "digits are for alpha (transparency) level.")
+        )
+        self.alt_sf_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.alt_sf_color_label, 22, 0)
+        grid0.addWidget(self.alt_sf_color_entry, 22, 1)
+
+        # Plot Selection (right - left) Fill Transparency Level
+        self.right_left_alpha_label = QtWidgets.QLabel('%s:' % _('Alpha'))
+        self.right_left_alpha_label.setToolTip(
+            _("Set the fill transparency for selection 'right to left' box.")
+        )
+        self.right_left_alpha_entry = FCSliderWithSpinner(0, 255, 1)
+
+        grid0.addWidget(self.right_left_alpha_label, 23, 0)
+        grid0.addWidget(self.right_left_alpha_entry, 23, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 24, 0, 1, 2)
+
+        # ------------------------------------------------------------------
+        # ----------------------- Editor Color -----------------------------
+        # ------------------------------------------------------------------
+
+        self.editor_color_label = QtWidgets.QLabel('<b>%s</b>' % _('Editor Color'))
+        grid0.addWidget(self.editor_color_label, 25, 0, 1, 2)
+
+        # Editor Draw Color
+        self.draw_color_label = QtWidgets.QLabel('%s:' % _('Drawing'))
+        self.alt_sf_color_label.setToolTip(
+            _("Set the color for the shape.")
+        )
+        self.draw_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.draw_color_label, 26, 0)
+        grid0.addWidget(self.draw_color_entry, 26, 1)
+
+        # Editor Draw Selection Color
+        self.sel_draw_color_label = QtWidgets.QLabel('%s:' % _('Selection'))
+        self.sel_draw_color_label.setToolTip(
+            _("Set the color of the shape when selected.")
+        )
+        self.sel_draw_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.sel_draw_color_label, 27, 0)
+        grid0.addWidget(self.sel_draw_color_entry, 27, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 28, 0, 1, 2)
+
+        # ------------------------------------------------------------------
+        # ----------------------- Project Settings -----------------------------
+        # ------------------------------------------------------------------
+
+        self.proj_settings_label = QtWidgets.QLabel('<b>%s</b>' % _('Project Items Color'))
+        grid0.addWidget(self.proj_settings_label, 29, 0, 1, 2)
+
+        # Project Tab items color
+        self.proj_color_label = QtWidgets.QLabel('%s:' % _('Enabled'))
+        self.proj_color_label.setToolTip(
+            _("Set the color of the items in Project Tab Tree.")
+        )
+        self.proj_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.proj_color_label, 30, 0)
+        grid0.addWidget(self.proj_color_entry, 30, 1)
+
+        self.proj_color_dis_label = QtWidgets.QLabel('%s:' % _('Disabled'))
+        self.proj_color_dis_label.setToolTip(
+            _("Set the color of the items in Project Tab Tree,\n"
+              "for the case when the items are disabled.")
+        )
+        self.proj_color_dis_entry = FCColorEntry()
+
+        grid0.addWidget(self.proj_color_dis_label, 31, 0)
+        grid0.addWidget(self.proj_color_dis_entry, 31, 1)
+
+        # Project autohide CB
+        self.project_autohide_cb = FCCheckBox(label=_('Project AutoHide'))
+        self.project_autohide_cb.setToolTip(
+            _("Check this box if you want the project/selected/tool tab area to\n"
+              "hide automatically when there are no objects loaded and\n"
+              "to show whenever a new object is created.")
+        )
+
+        grid0.addWidget(self.project_autohide_cb, 32, 0, 1, 2)
+
+        # Just to add empty rows
+        grid0.addWidget(QtWidgets.QLabel(''), 33, 0, 1, 2)
+
+        self.layout.addStretch()
+
+        # #############################################################################
+        # ############################# GUI COLORS SIGNALS ############################
+        # #############################################################################
+
+        # Setting selection (left - right) colors signals
+        self.sf_color_entry.editingFinished.connect(self.on_sf_color_entry)
+        self.sl_color_entry.editingFinished.connect(self.on_sl_color_entry)
+
+        self.left_right_alpha_entry.valueChanged.connect(self.on_left_right_alpha_changed)  # alpha
+
+        # Setting selection (right - left) colors signals
+        self.alt_sf_color_entry.editingFinished.connect(self.on_alt_sf_color_entry)
+        self.alt_sl_color_entry.editingFinished.connect(self.on_alt_sl_color_entry)
+
+        self.right_left_alpha_entry.valueChanged.connect(self.on_right_left_alpha_changed)  # alpha
+
+        # Setting Editor Draw colors signals
+        self.draw_color_entry.editingFinished.connect(self.on_draw_color_entry)
+        self.sel_draw_color_entry.editingFinished.connect(self.on_sel_draw_color_entry)
+
+        self.proj_color_entry.editingFinished.connect(self.on_proj_color_entry)
+        self.proj_color_dis_entry.editingFinished.connect(self.on_proj_color_dis_entry)
+
+        self.layout_combo.activated.connect(self.app.on_layout)
+
+    @staticmethod
+    def handle_style(style):
+        # set current style
+        qsettings = QSettings("Open Source", "FlatCAM")
+        qsettings.setValue('style', style)
+
+        # This will write the setting to the platform specific storage.
+        del qsettings
+
+    @staticmethod
+    def handle_hdpi(state):
+        # set current HDPI
+        qsettings = QSettings("Open Source", "FlatCAM")
+        qsettings.setValue('hdpi', state)
+
+        # This will write the setting to the platform specific storage.
+        del qsettings
+
+    # Setting selection colors (left - right) handlers
+    def on_sf_color_entry(self):
+        self.app.defaults['global_sel_fill'] = self.app.defaults['global_sel_fill'][7:9]
+
+    def on_sl_color_entry(self):
+        self.app.defaults['global_sel_line'] = self.sl_color_entry.get_value()[:7] + \
+            self.app.defaults['global_sel_line'][7:9]
+
+    def on_left_right_alpha_changed(self, spinner_value):
+        """
+        Change the alpha level for the color of the selection box when selection is done left to right.
+        Called on valueChanged of a FCSliderWithSpinner.
+
+        :param spinner_value:   passed value within [0, 255]
+        :type spinner_value:    int
+        :return:                None
+        :rtype:
+        """
+
+        self.app.defaults['global_sel_fill'] = self.app.defaults['global_sel_fill'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.app.defaults['global_sel_line'] = self.app.defaults['global_sel_line'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+
+    # Setting selection colors (right - left) handlers
+    def on_alt_sf_color_entry(self):
+        self.app.defaults['global_alt_sel_fill'] = self.alt_sf_color_entry.get_value()[:7] + \
+                                                   self.app.defaults['global_alt_sel_fill'][7:9]
+
+    def on_alt_sl_color_entry(self):
+        self.app.defaults['global_alt_sel_line'] = self.alt_sl_color_entry.get_value()[:7] + \
+                                                   self.app.defaults['global_alt_sel_line'][7:9]
+
+    def on_right_left_alpha_changed(self, spinner_value):
+        """
+        Change the alpha level for the color of the selection box when selection is done right to left.
+        Called on valueChanged of a FCSliderWithSpinner.
+
+        :param spinner_value:   passed value within [0, 255]
+        :type spinner_value:    int
+        :return:                None
+        :rtype:
+        """
+
+        self.app.defaults['global_alt_sel_fill'] = self.app.defaults['global_alt_sel_fill'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.app.defaults['global_alt_sel_line'] = self.app.defaults['global_alt_sel_line'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+
+    # Setting Editor colors
+    def on_draw_color_entry(self):
+        self.app.defaults['global_draw_color'] = self.draw_color_entry.get_value()
+
+    def on_sel_draw_color_entry(self):
+        self.app.defaults['global_sel_draw_color'] = self.sel_draw_color_entry.get_value()
+
+    def on_proj_color_entry(self):
+        self.app.defaults['global_proj_item_color'] = self.proj_color_entry.get_value()
+
+    def on_proj_color_dis_entry(self):
+        self.app.defaults['global_proj_item_dis_color'] = self.proj_color_dis_entry.get_value()

+ 43 - 0
appGUI/preferences/general/GeneralPreferencesUI.py

@@ -0,0 +1,43 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.preferences.general.GeneralAppPrefGroupUI import GeneralAppPrefGroupUI
+from appGUI.preferences.general.GeneralAPPSetGroupUI import GeneralAPPSetGroupUI
+from appGUI.preferences.general.GeneralGUIPrefGroupUI import GeneralGUIPrefGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GeneralPreferencesUI(QtWidgets.QWidget):
+    def __init__(self, decimals, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QHBoxLayout()
+        self.setLayout(self.layout)
+        self.decimals = decimals
+
+        self.general_app_group = GeneralAppPrefGroupUI(decimals=self.decimals)
+        self.general_app_group.setMinimumWidth(250)
+
+        self.general_gui_group = GeneralGUIPrefGroupUI(decimals=self.decimals)
+        self.general_gui_group.setMinimumWidth(250)
+
+        self.general_app_set_group = GeneralAPPSetGroupUI(decimals=self.decimals)
+        self.general_app_set_group.setMinimumWidth(250)
+
+        self.layout.addWidget(self.general_app_group)
+        self.layout.addWidget(self.general_gui_group)
+        self.layout.addWidget(self.general_app_set_group)
+
+        self.layout.addStretch()

+ 0 - 0
appGUI/preferences/general/__init__.py


+ 349 - 0
appGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py

@@ -0,0 +1,349 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCLabel, NumericalEvalTupleEntry, \
+    NumericalEvalEntry, FCComboBox2
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Geometry Advanced Options Preferences", parent=parent)
+        super(GeometryAdvOptPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Geometry Adv. Options")))
+        self.decimals = decimals
+
+        # ------------------------------
+        # ## Advanced Options
+        # ------------------------------
+        self.geo_label = FCLabel('<b>%s:</b>' % _('Advanced Options'))
+        self.geo_label.setToolTip(
+            _("A list of advanced parameters.\n"
+              "Those parameters are available only for\n"
+              "Advanced App. Level.")
+        )
+        self.layout.addWidget(self.geo_label)
+
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+
+        # Toolchange X,Y
+        toolchange_xy_label = FCLabel('%s:' % _('Toolchange X-Y'))
+        toolchange_xy_label.setToolTip(
+            _("Toolchange X,Y position.")
+        )
+        self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
+        grid1.addWidget(toolchange_xy_label, 1, 0)
+        grid1.addWidget(self.toolchangexy_entry, 1, 1)
+
+        # Start move Z
+        startzlabel = FCLabel('%s:' % _('Start Z'))
+        startzlabel.setToolTip(
+            _("Height of the tool just after starting the work.\n"
+              "Delete the value if you don't need this feature.")
+        )
+        self.gstartz_entry = NumericalEvalEntry(border_color='#0069A9')
+
+        grid1.addWidget(startzlabel, 2, 0)
+        grid1.addWidget(self.gstartz_entry, 2, 1)
+
+        # Feedrate rapids
+        fr_rapid_label = FCLabel('%s:' % _('Feedrate Rapids'))
+        fr_rapid_label.setToolTip(
+            _("Cutting speed in the XY plane\n"
+              "(in units per minute).\n"
+              "This is for the rapid move G00.\n"
+              "It is useful only for Marlin,\n"
+              "ignore for any other cases.")
+        )
+        self.feedrate_rapid_entry = FCDoubleSpinner()
+        self.feedrate_rapid_entry.set_range(0, 910000.0000)
+        self.feedrate_rapid_entry.set_precision(self.decimals)
+        self.feedrate_rapid_entry.setSingleStep(0.1)
+        self.feedrate_rapid_entry.setWrapping(True)
+
+        grid1.addWidget(fr_rapid_label, 4, 0)
+        grid1.addWidget(self.feedrate_rapid_entry, 4, 1)
+
+        # End move extra cut
+        self.extracut_cb = FCCheckBox('%s' % _('Re-cut'))
+        self.extracut_cb.setToolTip(
+            _("In order to remove possible\n"
+              "copper leftovers where first cut\n"
+              "meet with last cut, we generate an\n"
+              "extended cut over the first cut section.")
+        )
+
+        self.e_cut_entry = FCDoubleSpinner()
+        self.e_cut_entry.set_range(0, 99999)
+        self.e_cut_entry.set_precision(self.decimals)
+        self.e_cut_entry.setSingleStep(0.1)
+        self.e_cut_entry.setWrapping(True)
+        self.e_cut_entry.setToolTip(
+            _("In order to remove possible\n"
+              "copper leftovers where first cut\n"
+              "meet with last cut, we generate an\n"
+              "extended cut over the first cut section.")
+        )
+        grid1.addWidget(self.extracut_cb, 5, 0)
+        grid1.addWidget(self.e_cut_entry, 5, 1)
+
+        # Probe depth
+        self.pdepth_label = FCLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label.setToolTip(
+            _("The maximum depth that the probe is allowed\n"
+              "to probe. Negative value, in current units.")
+        )
+        self.pdepth_entry = FCDoubleSpinner()
+        self.pdepth_entry.set_range(-99999, 0.0000)
+        self.pdepth_entry.set_precision(self.decimals)
+        self.pdepth_entry.setSingleStep(0.1)
+        self.pdepth_entry.setWrapping(True)
+
+        grid1.addWidget(self.pdepth_label, 6, 0)
+        grid1.addWidget(self.pdepth_entry, 6, 1)
+
+        # Probe feedrate
+        self.feedrate_probe_label = FCLabel('%s:' % _("Feedrate Probe"))
+        self.feedrate_probe_label.setToolTip(
+            _("The feedrate used while the probe is probing.")
+        )
+        self.feedrate_probe_entry = FCDoubleSpinner()
+        self.feedrate_probe_entry.set_range(0, 910000.0000)
+        self.feedrate_probe_entry.set_precision(self.decimals)
+        self.feedrate_probe_entry.setSingleStep(0.1)
+        self.feedrate_probe_entry.setWrapping(True)
+
+        grid1.addWidget(self.feedrate_probe_label, 7, 0)
+        grid1.addWidget(self.feedrate_probe_entry, 7, 1)
+
+        # Spindle direction
+        spindle_dir_label = FCLabel('%s:' % _('Spindle direction'))
+        spindle_dir_label.setToolTip(
+            _("This sets the direction that the spindle is rotating.\n"
+              "It can be either:\n"
+              "- CW = clockwise or\n"
+              "- CCW = counter clockwise")
+        )
+
+        self.spindledir_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                          {'label': _('CCW'), 'value': 'CCW'}])
+        grid1.addWidget(spindle_dir_label, 8, 0)
+        grid1.addWidget(self.spindledir_radio, 8, 1)
+
+        # Fast Move from Z Toolchange
+        self.fplunge_cb = FCCheckBox('%s' % _('Fast Plunge'))
+        self.fplunge_cb.setToolTip(
+            _("By checking this, the vertical move from\n"
+              "Z_Toolchange to Z_move is done with G0,\n"
+              "meaning the fastest speed available.\n"
+              "WARNING: the move is done at Toolchange X,Y coords.")
+        )
+        grid1.addWidget(self.fplunge_cb, 9, 0, 1, 2)
+
+        # Size of trace segment on X axis
+        segx_label = FCLabel('%s:' % _("Segment X size"))
+        segx_label.setToolTip(
+            _("The size of the trace segment on the X axis.\n"
+              "Useful for auto-leveling.\n"
+              "A value of 0 means no segmentation on the X axis.")
+        )
+        self.segx_entry = FCDoubleSpinner()
+        self.segx_entry.set_range(0, 99999)
+        self.segx_entry.set_precision(self.decimals)
+        self.segx_entry.setSingleStep(0.1)
+        self.segx_entry.setWrapping(True)
+
+        grid1.addWidget(segx_label, 10, 0)
+        grid1.addWidget(self.segx_entry, 10, 1)
+
+        # Size of trace segment on Y axis
+        segy_label = FCLabel('%s:' % _("Segment Y size"))
+        segy_label.setToolTip(
+            _("The size of the trace segment on the Y axis.\n"
+              "Useful for auto-leveling.\n"
+              "A value of 0 means no segmentation on the Y axis.")
+        )
+        self.segy_entry = FCDoubleSpinner()
+        self.segy_entry.set_range(0, 99999)
+        self.segy_entry.set_precision(self.decimals)
+        self.segy_entry.setSingleStep(0.1)
+        self.segy_entry.setWrapping(True)
+
+        grid1.addWidget(segy_label, 11, 0)
+        grid1.addWidget(self.segy_entry, 11, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 12, 0, 1, 2)
+
+        # -----------------------------
+        # --- Area Exclusion ----------
+        # -----------------------------
+        self.area_exc_label = FCLabel('<b>%s:</b>' % _('Area Exclusion'))
+        self.area_exc_label.setToolTip(
+            _("Area exclusion parameters.")
+        )
+        grid1.addWidget(self.area_exc_label, 13, 0, 1, 2)
+
+        # Exclusion Area CB
+        self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas"))
+        self.exclusion_cb.setToolTip(
+            _(
+                "Include exclusion areas.\n"
+                "In those areas the travel of the tools\n"
+                "is forbidden."
+            )
+        )
+        grid1.addWidget(self.exclusion_cb, 14, 0, 1, 2)
+
+        # Area Selection shape
+        self.area_shape_label = FCLabel('%s:' % _("Shape"))
+        self.area_shape_label.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+
+        grid1.addWidget(self.area_shape_label, 15, 0)
+        grid1.addWidget(self.area_shape_radio, 15, 1)
+
+        # Chose Strategy
+        self.strategy_label = FCLabel('%s:' % _("Strategy"))
+        self.strategy_label.setToolTip(_("The strategy followed when encountering an exclusion area.\n"
+                                         "Can be:\n"
+                                         "- Over -> when encountering the area, the tool will go to a set height\n"
+                                         "- Around -> will avoid the exclusion area by going around the area"))
+        self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'},
+                                        {'label': _('Around'), 'value': 'around'}])
+
+        grid1.addWidget(self.strategy_label, 16, 0)
+        grid1.addWidget(self.strategy_radio, 16, 1)
+
+        # Over Z
+        self.over_z_label = FCLabel('%s:' % _("Over Z"))
+        self.over_z_label.setToolTip(_("The height Z to which the tool will rise in order to avoid\n"
+                                       "an interdiction area."))
+        self.over_z_entry = FCDoubleSpinner()
+        self.over_z_entry.set_range(0.000, 10000.0000)
+        self.over_z_entry.set_precision(self.decimals)
+
+        grid1.addWidget(self.over_z_label, 18, 0)
+        grid1.addWidget(self.over_z_entry, 18, 1)
+        
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 20, 0, 1, 2)
+        
+        # -----------------------------
+        # --- Area POLISH ----------
+        # -----------------------------
+        # Add Polish
+        self.polish_cb = FCCheckBox(label=_('Add Polish'))
+        self.polish_cb.setToolTip(_(
+            "Will add a Paint section at the end of the GCode.\n"
+            "A metallic brush will clean the material after milling."))
+        grid1.addWidget(self.polish_cb, 22, 0, 1, 2)
+
+        # Polish Tool Diameter
+        self.polish_dia_lbl = FCLabel('%s:' % _('Tool Dia'))
+        self.polish_dia_lbl.setToolTip(
+            _("Diameter for the polishing tool.")
+        )
+        self.polish_dia_entry = FCDoubleSpinner()
+        self.polish_dia_entry.set_precision(self.decimals)
+        self.polish_dia_entry.set_range(0.000, 10000.0000)
+
+        grid1.addWidget(self.polish_dia_lbl, 24, 0)
+        grid1.addWidget(self.polish_dia_entry, 24, 1)
+
+        # Polish Travel Z
+        self.polish_travelz_lbl = FCLabel('%s:' % _('Travel Z'))
+        self.polish_travelz_lbl.setToolTip(
+            _("Height of the tool when\n"
+              "moving without cutting.")
+        )
+        self.polish_travelz_entry = FCDoubleSpinner()
+        self.polish_travelz_entry.set_precision(self.decimals)
+        self.polish_travelz_entry.set_range(0.00000, 10000.00000)
+        self.polish_travelz_entry.setSingleStep(0.1)
+
+        grid1.addWidget(self.polish_travelz_lbl, 26, 0)
+        grid1.addWidget(self.polish_travelz_entry, 26, 1)
+
+        # Polish Pressure
+        self.polish_pressure_lbl = FCLabel('%s:' % _('Pressure'))
+        self.polish_pressure_lbl.setToolTip(
+            _("Negative value. The higher the absolute value\n"
+              "the stronger the pressure of the brush on the material.")
+        )
+        self.polish_pressure_entry = FCDoubleSpinner()
+        self.polish_pressure_entry.set_precision(self.decimals)
+        self.polish_pressure_entry.set_range(-10000.0000, 10000.0000)
+
+        grid1.addWidget(self.polish_pressure_lbl, 28, 0)
+        grid1.addWidget(self.polish_pressure_entry, 28, 1)
+
+        # Polish Margin
+        self.polish_margin_lbl = FCLabel('%s:' % _('Margin'))
+        self.polish_margin_lbl.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.polish_margin_entry = FCDoubleSpinner()
+        self.polish_margin_entry.set_precision(self.decimals)
+        self.polish_margin_entry.set_range(-10000.0000, 10000.0000)
+
+        grid1.addWidget(self.polish_margin_lbl, 30, 0)
+        grid1.addWidget(self.polish_margin_entry, 30, 1)
+
+        # Polish Overlap
+        self.polish_over_lbl = FCLabel('%s:' % _('Overlap'))
+        self.polish_over_lbl.setToolTip(
+            _("How much (percentage) of the tool width to overlap each tool pass.")
+        )
+        self.polish_over_entry = FCDoubleSpinner(suffix='%')
+        self.polish_over_entry.set_precision(self.decimals)
+        self.polish_over_entry.setWrapping(True)
+        self.polish_over_entry.set_range(0.0000, 99.9999)
+        self.polish_over_entry.setSingleStep(0.1)
+
+        grid1.addWidget(self.polish_over_lbl, 32, 0)
+        grid1.addWidget(self.polish_over_entry, 32, 1)
+
+        # Polish Method
+        self.polish_method_lbl = FCLabel('%s:' % _('Method'))
+        self.polish_method_lbl.setToolTip(
+            _("Algorithm for polishing:\n"
+              "- Standard: Fixed step inwards.\n"
+              "- Seed-based: Outwards from seed.\n"
+              "- Line-based: Parallel lines.")
+        )
+
+        self.polish_method_combo = FCComboBox2()
+        self.polish_method_combo.addItems(
+            [_("Standard"), _("Seed"), _("Lines")]
+        )
+
+        grid1.addWidget(self.polish_method_lbl, 34, 0)
+        grid1.addWidget(self.polish_method_combo, 34, 1)
+
+        self.layout.addStretch()

+ 67 - 0
appGUI/preferences/geometry/GeometryEditorPrefGroupUI.py

@@ -0,0 +1,67 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCSpinner, RadioSet
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GeometryEditorPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Gerber Adv. Options Preferences", parent=parent)
+        super(GeometryEditorPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Geometry Editor")))
+        self.decimals = decimals
+
+        # Editor Parameters
+        self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.param_label.setToolTip(
+            _("A list of Editor parameters.")
+        )
+        self.layout.addWidget(self.param_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Selection Limit
+        self.sel_limit_label = QtWidgets.QLabel('%s:' % _("Selection limit"))
+        self.sel_limit_label.setToolTip(
+            _("Set the number of selected geometry\n"
+              "items above which the utility geometry\n"
+              "becomes just a selection rectangle.\n"
+              "Increases the performance when moving a\n"
+              "large number of geometric elements.")
+        )
+        self.sel_limit_entry = FCSpinner()
+        self.sel_limit_entry.set_range(0, 9999)
+
+        grid0.addWidget(self.sel_limit_label, 0, 0)
+        grid0.addWidget(self.sel_limit_entry, 0, 1)
+
+        # Milling Type
+        milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
+        milling_type_label.setToolTip(
+            _("Milling type:\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+        self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
+                                            {'label': _('Conventional'), 'value': 'cv'}])
+        grid0.addWidget(milling_type_label, 1, 0)
+        grid0.addWidget(self.milling_type_radio, 1, 1)
+
+        self.layout.addStretch()

+ 193 - 0
appGUI/preferences/geometry/GeometryGenPrefGroupUI.py

@@ -0,0 +1,193 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCCheckBox, FCSpinner, FCEntry, FCColorEntry, RadioSet
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import platform
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GeometryGenPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Geometry General Preferences", parent=parent)
+        super(GeometryGenPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Geometry General")))
+        self.decimals = decimals
+
+        # ## Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
+        self.layout.addWidget(self.plot_options_label)
+
+        plot_hlay = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(plot_hlay)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label=_('Plot'))
+        self.plot_cb.setToolTip(
+            _("Plot (show) this object.")
+        )
+        plot_hlay.addWidget(self.plot_cb)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label=_('M-Color'))
+        self.multicolored_cb.setToolTip(
+            _("Draw polygons in different colors.")
+        )
+        plot_hlay.addWidget(self.multicolored_cb)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        # Number of circle steps for circular aperture linear approximation
+        self.circle_steps_label = QtWidgets.QLabel('%s:' % _("Circle Steps"))
+        self.circle_steps_label.setToolTip(
+            _("The number of circle steps for <b>Geometry</b> \n"
+              "circle and arc shapes linear approximation.")
+        )
+        self.circle_steps_entry = FCSpinner()
+        self.circle_steps_entry.set_range(0, 999)
+
+        grid0.addWidget(self.circle_steps_label, 1, 0)
+        grid0.addWidget(self.circle_steps_entry, 1, 1)
+
+        # Tools
+        self.tools_label = QtWidgets.QLabel("<b>%s:</b>" % _("Tools"))
+        grid0.addWidget(self.tools_label, 2, 0, 1, 2)
+
+        # Tooldia
+        tdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
+        tdlabel.setToolTip(
+            _("Diameters of the tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
+        )
+        self.cnctooldia_entry = FCEntry()
+
+        grid0.addWidget(tdlabel, 3, 0)
+        grid0.addWidget(self.cnctooldia_entry, 3, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
+
+        self.opt_label = QtWidgets.QLabel("<b>%s:</b>" % _("Path Optimization"))
+        grid0.addWidget(self.opt_label, 10, 0, 1, 2)
+
+        self.opt_algorithm_label = QtWidgets.QLabel(_('Algorithm:'))
+        self.opt_algorithm_label.setToolTip(
+            _("This sets the path optimization algorithm.\n"
+              "- Rtre -> Rtree algorithm\n"
+              "- MetaHeuristic -> Google OR-Tools algorithm with\n"
+              "MetaHeuristic Guided Local Path is used. Default search time is 3sec.\n"
+              "- Basic -> Using Google OR-Tools Basic algorithm\n"
+              "- TSA -> Using Travelling Salesman algorithm\n"
+              "\n"
+              "Some options are disabled when the application works in 32bit mode.")
+        )
+
+        self.opt_algorithm_radio = RadioSet(
+            [
+                {'label': _('Rtree'), 'value': 'R'},
+                {'label': _('MetaHeuristic'), 'value': 'M'},
+                {'label': _('Basic'), 'value': 'B'},
+                {'label': _('TSA'), 'value': 'T'}
+            ], orientation='vertical', stretch=False)
+
+        grid0.addWidget(self.opt_algorithm_label, 12, 0)
+        grid0.addWidget(self.opt_algorithm_radio, 12, 1)
+
+        self.optimization_time_label = QtWidgets.QLabel('%s:' % _('Duration'))
+        self.optimization_time_label.setToolTip(
+            _("When OR-Tools Metaheuristic (MH) is enabled there is a\n"
+              "maximum threshold for how much time is spent doing the\n"
+              "path optimization. This max duration is set here.\n"
+              "In seconds.")
+
+        )
+
+        self.optimization_time_entry = FCSpinner()
+        self.optimization_time_entry.set_range(0, 999)
+
+        grid0.addWidget(self.optimization_time_label, 14, 0)
+        grid0.addWidget(self.optimization_time_entry, 14, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 16, 0, 1, 2)
+
+        # Fuse Tools
+        self.join_geo_label = QtWidgets.QLabel('<b>%s</b>:' % _('Join Option'))
+        grid0.addWidget(self.join_geo_label, 18, 0, 1, 2)
+
+        self.fuse_tools_cb = FCCheckBox(_("Fuse Tools"))
+        self.fuse_tools_cb.setToolTip(
+            _("When checked, the tools will be merged\n"
+              "but only if they share some of their attributes.")
+        )
+        grid0.addWidget(self.fuse_tools_cb, 20, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 22, 0, 1, 2)
+
+        # Geometry Object Color
+        self.gerber_color_label = QtWidgets.QLabel('<b>%s</b>:' % _('Object Color'))
+        grid0.addWidget(self.gerber_color_label, 24, 0, 1, 2)
+
+        # Plot Line Color
+        self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
+        self.line_color_label.setToolTip(
+            _("Set the line color for plotted objects.")
+        )
+        self.line_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.line_color_label, 26, 0)
+        grid0.addWidget(self.line_color_entry, 26, 1)
+
+        self.layout.addStretch()
+
+        current_platform = platform.architecture()[0]
+        if current_platform == '64bit':
+            self.opt_algorithm_radio.setOptionsDisabled([_('MetaHeuristic'), _('Basic')], False)
+            self.optimization_time_label.setDisabled(False)
+            self.optimization_time_entry.setDisabled(False)
+        else:
+            self.opt_algorithm_radio.setOptionsDisabled([_('MetaHeuristic'), _('Basic')], True)
+            self.optimization_time_label.setDisabled(True)
+            self.optimization_time_entry.setDisabled(True)
+
+        self.opt_algorithm_radio.activated_custom.connect(self.optimization_selection)
+
+        # Setting plot colors signals
+        self.line_color_entry.editingFinished.connect(self.on_line_color_entry)
+
+    def on_line_color_entry(self):
+        self.app.defaults['geometry_plot_line'] = self.line_color_entry.get_value()[:7] + 'FF'
+
+    def optimization_selection(self, val):
+        if val == 'M':
+            self.optimization_time_label.setDisabled(False)
+            self.optimization_time_entry.setDisabled(False)
+        else:
+            self.optimization_time_label.setDisabled(True)
+            self.optimization_time_entry.setDisabled(True)

+ 267 - 0
appGUI/preferences/geometry/GeometryOptPrefGroupUI.py

@@ -0,0 +1,267 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import Qt, QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, OptionalInputSection, FCSpinner, FCComboBox, \
+    NumericalEvalTupleEntry
+from appGUI.preferences import machinist_setting
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GeometryOptPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Geometry Options Preferences", parent=parent)
+        super(GeometryOptPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Geometry Options")))
+        self.decimals = decimals
+
+        # ------------------------------
+        # ## Create CNC Job
+        # ------------------------------
+        self.cncjob_label = QtWidgets.QLabel('<b>%s:</b>' % _('Create CNCJob'))
+        self.cncjob_label.setToolTip(
+            _("Create a CNC Job object\n"
+              "tracing the contours of this\n"
+              "Geometry object.")
+        )
+        self.layout.addWidget(self.cncjob_label)
+
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+        grid1.setColumnStretch(0, 0)
+        grid1.setColumnStretch(1, 1)
+
+        # Cut Z
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel.setToolTip(
+            _("Cutting depth (negative)\n"
+              "below the copper surface.")
+        )
+        self.cutz_entry = FCDoubleSpinner()
+
+        if machinist_setting == 0:
+            self.cutz_entry.set_range(-10000.0000, 0.0000)
+        else:
+            self.cutz_entry.set_range(-10000.0000, 10000.0000)
+
+        self.cutz_entry.set_precision(self.decimals)
+        self.cutz_entry.setSingleStep(0.1)
+        self.cutz_entry.setWrapping(True)
+
+        grid1.addWidget(cutzlabel, 0, 0)
+        grid1.addWidget(self.cutz_entry, 0, 1)
+
+        # Multidepth CheckBox
+        self.multidepth_cb = FCCheckBox(label=_('Multi-Depth'))
+        self.multidepth_cb.setToolTip(
+            _(
+                "Use multiple passes to limit\n"
+                "the cut depth in each pass. Will\n"
+                "cut multiple times until Cut Z is\n"
+                "reached."
+            )
+        )
+        grid1.addWidget(self.multidepth_cb, 1, 0)
+
+        # Depth/pass
+        dplabel = QtWidgets.QLabel('%s:' % _('Depth/Pass'))
+        dplabel.setToolTip(
+            _("The depth to cut on each pass,\n"
+              "when multidepth is enabled.\n"
+              "It has positive value although\n"
+              "it is a fraction from the depth\n"
+              "which has negative value.")
+        )
+
+        self.depthperpass_entry = FCDoubleSpinner()
+        self.depthperpass_entry.set_range(0, 99999)
+        self.depthperpass_entry.set_precision(self.decimals)
+        self.depthperpass_entry.setSingleStep(0.1)
+        self.depthperpass_entry.setWrapping(True)
+
+        grid1.addWidget(dplabel, 2, 0)
+        grid1.addWidget(self.depthperpass_entry, 2, 1)
+
+        self.ois_multidepth = OptionalInputSection(self.multidepth_cb, [self.depthperpass_entry])
+
+        # Travel Z
+        travelzlabel = QtWidgets.QLabel('%s:' % _('Travel Z'))
+        travelzlabel.setToolTip(
+            _("Height of the tool when\n"
+              "moving without cutting.")
+        )
+        self.travelz_entry = FCDoubleSpinner()
+
+        if machinist_setting == 0:
+            self.travelz_entry.set_range(0.0001, 10000.0000)
+        else:
+            self.travelz_entry.set_range(-10000.0000, 10000.0000)
+
+        self.travelz_entry.set_precision(self.decimals)
+        self.travelz_entry.setSingleStep(0.1)
+        self.travelz_entry.setWrapping(True)
+
+        grid1.addWidget(travelzlabel, 3, 0)
+        grid1.addWidget(self.travelz_entry, 3, 1)
+
+        # Tool change:
+        self.toolchange_cb = FCCheckBox('%s' % _("Tool change"))
+        self.toolchange_cb.setToolTip(
+            _(
+                "Include tool-change sequence\n"
+                "in the Machine Code (Pause for tool change)."
+            )
+        )
+        grid1.addWidget(self.toolchange_cb, 4, 0, 1, 2)
+
+        # Toolchange Z
+        toolchangezlabel = QtWidgets.QLabel('%s:' % _('Toolchange Z'))
+        toolchangezlabel.setToolTip(
+            _(
+                "Z-axis position (height) for\n"
+                "tool change."
+            )
+        )
+        self.toolchangez_entry = FCDoubleSpinner()
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.set_range(0.000, 10000.0000)
+        else:
+            self.toolchangez_entry.set_range(-10000.0000, 10000.0000)
+
+        self.toolchangez_entry.set_precision(self.decimals)
+        self.toolchangez_entry.setSingleStep(0.1)
+        self.toolchangez_entry.setWrapping(True)
+
+        grid1.addWidget(toolchangezlabel, 5, 0)
+        grid1.addWidget(self.toolchangez_entry, 5, 1)
+
+        # End move Z
+        endz_label = QtWidgets.QLabel('%s:' % _('End move Z'))
+        endz_label.setToolTip(
+            _("Height of the tool after\n"
+              "the last move at the end of the job.")
+        )
+        self.endz_entry = FCDoubleSpinner()
+
+        if machinist_setting == 0:
+            self.endz_entry.set_range(0.000, 10000.0000)
+        else:
+            self.endz_entry.set_range(-10000.0000, 10000.0000)
+
+        self.endz_entry.set_precision(self.decimals)
+        self.endz_entry.setSingleStep(0.1)
+        self.endz_entry.setWrapping(True)
+
+        grid1.addWidget(endz_label, 6, 0)
+        grid1.addWidget(self.endz_entry, 6, 1)
+
+        # End Move X,Y
+        endmove_xy_label = QtWidgets.QLabel('%s:' % _('End move X,Y'))
+        endmove_xy_label.setToolTip(
+            _("End move X,Y position. In format (x,y).\n"
+              "If no value is entered then there is no move\n"
+              "on X,Y plane at the end of the job.")
+        )
+        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
+        grid1.addWidget(endmove_xy_label, 7, 0)
+        grid1.addWidget(self.endxy_entry, 7, 1)
+
+        # Feedrate X-Y
+        frlabel = QtWidgets.QLabel('%s:' % _('Feedrate X-Y'))
+        frlabel.setToolTip(
+            _("Cutting speed in the XY\n"
+              "plane in units per minute")
+        )
+        self.cncfeedrate_entry = FCDoubleSpinner()
+        self.cncfeedrate_entry.set_range(0, 910000.0000)
+        self.cncfeedrate_entry.set_precision(self.decimals)
+        self.cncfeedrate_entry.setSingleStep(0.1)
+        self.cncfeedrate_entry.setWrapping(True)
+
+        grid1.addWidget(frlabel, 8, 0)
+        grid1.addWidget(self.cncfeedrate_entry, 8, 1)
+
+        # Feedrate Z (Plunge)
+        frz_label = QtWidgets.QLabel('%s:' % _('Feedrate Z'))
+        frz_label.setToolTip(
+            _("Cutting speed in the XY\n"
+              "plane in units per minute.\n"
+              "It is called also Plunge.")
+        )
+        self.feedrate_z_entry = FCDoubleSpinner()
+        self.feedrate_z_entry.set_range(0, 910000.0000)
+        self.feedrate_z_entry.set_precision(self.decimals)
+        self.feedrate_z_entry.setSingleStep(0.1)
+        self.feedrate_z_entry.setWrapping(True)
+
+        grid1.addWidget(frz_label, 9, 0)
+        grid1.addWidget(self.feedrate_z_entry, 9, 1)
+
+        # Spindle Speed
+        spdlabel = QtWidgets.QLabel('%s:' % _('Spindle speed'))
+        spdlabel.setToolTip(
+            _(
+                "Speed of the spindle in RPM (optional).\n"
+                "If LASER preprocessor is used,\n"
+                "this value is the power of laser."
+            )
+        )
+        self.cncspindlespeed_entry = FCSpinner()
+        self.cncspindlespeed_entry.set_range(0, 1000000)
+        self.cncspindlespeed_entry.set_step(100)
+
+        grid1.addWidget(spdlabel, 10, 0)
+        grid1.addWidget(self.cncspindlespeed_entry, 10, 1)
+
+        # Dwell
+        self.dwell_cb = FCCheckBox(label='%s' % _('Enable Dwell'))
+        self.dwell_cb.setToolTip(
+            _("Pause to allow the spindle to reach its\n"
+              "speed before cutting.")
+        )
+        dwelltime = QtWidgets.QLabel('%s:' % _('Duration'))
+        dwelltime.setToolTip(
+            _("Number of time units for spindle to dwell.")
+        )
+        self.dwelltime_entry = FCDoubleSpinner()
+        self.dwelltime_entry.set_range(0, 99999)
+        self.dwelltime_entry.set_precision(self.decimals)
+        self.dwelltime_entry.setSingleStep(0.1)
+        self.dwelltime_entry.setWrapping(True)
+
+        grid1.addWidget(self.dwell_cb, 11, 0)
+        grid1.addWidget(dwelltime, 12, 0)
+        grid1.addWidget(self.dwelltime_entry, 12, 1)
+
+        self.ois_dwell = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        # preprocessor selection
+        pp_label = QtWidgets.QLabel('%s:' % _("Preprocessor"))
+        pp_label.setToolTip(
+            _("The Preprocessor file that dictates\n"
+              "the Machine Code (like GCode, RML, HPGL) output.")
+        )
+        self.pp_geometry_name_cb = FCComboBox()
+        self.pp_geometry_name_cb.setFocusPolicy(Qt.StrongFocus)
+        self.pp_geometry_name_cb.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)
+
+        grid1.addWidget(pp_label, 13, 0)
+        grid1.addWidget(self.pp_geometry_name_cb, 13, 1)
+
+        self.layout.addStretch()

+ 46 - 0
appGUI/preferences/geometry/GeometryPreferencesUI.py

@@ -0,0 +1,46 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.preferences.geometry.GeometryEditorPrefGroupUI import GeometryEditorPrefGroupUI
+from appGUI.preferences.geometry.GeometryAdvOptPrefGroupUI import GeometryAdvOptPrefGroupUI
+from appGUI.preferences.geometry.GeometryOptPrefGroupUI import GeometryOptPrefGroupUI
+from appGUI.preferences.geometry.GeometryGenPrefGroupUI import GeometryGenPrefGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GeometryPreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, decimals, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QHBoxLayout()
+        self.setLayout(self.layout)
+        self.decimals = decimals
+
+        self.geometry_gen_group = GeometryGenPrefGroupUI(decimals=self.decimals)
+        self.geometry_gen_group.setMinimumWidth(220)
+        self.geometry_opt_group = GeometryOptPrefGroupUI(decimals=self.decimals)
+        self.geometry_opt_group.setMinimumWidth(300)
+        self.geometry_adv_opt_group = GeometryAdvOptPrefGroupUI(decimals=self.decimals)
+        self.geometry_adv_opt_group.setMinimumWidth(270)
+        self.geometry_editor_group = GeometryEditorPrefGroupUI(decimals=self.decimals)
+        self.geometry_editor_group.setMinimumWidth(250)
+
+        self.layout.addWidget(self.geometry_gen_group)
+        self.layout.addWidget(self.geometry_opt_group)
+        self.layout.addWidget(self.geometry_adv_opt_group)
+        self.layout.addWidget(self.geometry_editor_group)
+
+        self.layout.addStretch()

+ 0 - 0
appGUI/preferences/geometry/__init__.py


+ 120 - 0
appGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py

@@ -0,0 +1,120 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCCheckBox, RadioSet, FCDoubleSpinner, FCSpinner, OptionalInputSection
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GerberAdvOptPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Gerber Adv. Options Preferences", parent=parent)
+        super(GerberAdvOptPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Gerber Adv. Options")))
+        self.decimals = decimals
+
+        # ## Advanced Gerber Parameters
+        self.adv_param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Advanced Options'))
+        self.adv_param_label.setToolTip(
+            _("A list of advanced parameters.\n"
+              "Those parameters are available only for\n"
+              "Advanced App. Level.")
+        )
+        self.layout.addWidget(self.adv_param_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Follow Attribute
+        self.follow_cb = FCCheckBox(label=_('"Follow"'))
+        self.follow_cb.setToolTip(
+            _("Generate a 'Follow' geometry.\n"
+              "This means that it will cut through\n"
+              "the middle of the trace.")
+        )
+        grid0.addWidget(self.follow_cb, 0, 0, 1, 2)
+
+        # Aperture Table Visibility CB
+        self.aperture_table_visibility_cb = FCCheckBox(label=_('Table Show/Hide'))
+        self.aperture_table_visibility_cb.setToolTip(
+            _("Toggle the display of the Tools Table.")
+        )
+        grid0.addWidget(self.aperture_table_visibility_cb, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 2)
+
+        # Buffering Type
+        buffering_label = QtWidgets.QLabel('%s:' % _('Buffering'))
+        buffering_label.setToolTip(
+            _("Buffering type:\n"
+              "- None --> best performance, fast file loading but no so good display\n"
+              "- Full --> slow file loading but good visuals. This is the default.\n"
+              "<<WARNING>>: Don't change this unless you know what you are doing !!!")
+        )
+        self.buffering_radio = RadioSet([{'label': _('None'), 'value': 'no'},
+                                         {'label': _('Full'), 'value': 'full'}])
+        grid0.addWidget(buffering_label, 9, 0)
+        grid0.addWidget(self.buffering_radio, 9, 1)
+
+        # Delayed Buffering
+        self.delayed_buffer_cb = FCCheckBox(label=_('Delayed Buffering'))
+        self.delayed_buffer_cb.setToolTip(
+            _("When checked it will do the buffering in background.")
+        )
+        grid0.addWidget(self.delayed_buffer_cb, 10, 0, 1, 2)
+
+        # Simplification
+        self.simplify_cb = FCCheckBox(label=_('Simplify'))
+        self.simplify_cb.setToolTip(
+            _("When checked all the Gerber polygons will be\n"
+              "loaded with simplification having a set tolerance.\n"
+              "<<WARNING>>: Don't change this unless you know what you are doing !!!")
+                                    )
+        grid0.addWidget(self.simplify_cb, 11, 0, 1, 2)
+
+        # Simplification tolerance
+        self.simplification_tol_label = QtWidgets.QLabel(_('Tolerance'))
+        self.simplification_tol_label.setToolTip(_("Tolerance for polygon simplification."))
+
+        self.simplification_tol_spinner = FCDoubleSpinner()
+        self.simplification_tol_spinner.set_precision(self.decimals + 1)
+        self.simplification_tol_spinner.setWrapping(True)
+        self.simplification_tol_spinner.setRange(0.00000, 0.01000)
+        self.simplification_tol_spinner.setSingleStep(0.0001)
+
+        grid0.addWidget(self.simplification_tol_label, 12, 0)
+        grid0.addWidget(self.simplification_tol_spinner, 12, 1)
+        self.ois_simplif = OptionalInputSection(
+            self.simplify_cb,
+            [
+                self.simplification_tol_label, self.simplification_tol_spinner
+            ],
+            logic=True)
+
+        self.layout.addStretch()
+
+        # signals
+        self.buffering_radio.activated_custom.connect(self.on_buffering_change)
+
+    def on_buffering_change(self, val):
+        if val == 'no':
+            self.delayed_buffer_cb.setDisabled(False)
+        else:
+            self.delayed_buffer_cb.setDisabled(True)

+ 248 - 0
appGUI/preferences/gerber/GerberEditorPrefGroupUI.py

@@ -0,0 +1,248 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCSpinner, FCDoubleSpinner, FCComboBox, FCEntry, RadioSet, NumericalEvalTupleEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GerberEditorPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Gerber Adv. Options Preferences", parent=parent)
+        super(GerberEditorPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Gerber Editor")))
+        self.decimals = decimals
+
+        # Advanced Gerber Parameters
+        self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.param_label.setToolTip(
+            _("A list of Gerber Editor parameters.")
+        )
+        self.layout.addWidget(self.param_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Selection Limit
+        self.sel_limit_label = QtWidgets.QLabel('%s:' % _("Selection limit"))
+        self.sel_limit_label.setToolTip(
+            _("Set the number of selected Gerber geometry\n"
+              "items above which the utility geometry\n"
+              "becomes just a selection rectangle.\n"
+              "Increases the performance when moving a\n"
+              "large number of geometric elements.")
+        )
+        self.sel_limit_entry = FCSpinner()
+        self.sel_limit_entry.set_range(0, 9999)
+
+        grid0.addWidget(self.sel_limit_label, 0, 0)
+        grid0.addWidget(self.sel_limit_entry, 0, 1)
+
+        # New aperture code
+        self.addcode_entry_lbl = QtWidgets.QLabel('%s:' % _('New Aperture code'))
+        self.addcode_entry_lbl.setToolTip(
+            _("Code for the new aperture")
+        )
+
+        self.addcode_entry = FCSpinner()
+        self.addcode_entry.set_range(10, 99)
+        self.addcode_entry.setWrapping(True)
+
+        grid0.addWidget(self.addcode_entry_lbl, 1, 0)
+        grid0.addWidget(self.addcode_entry, 1, 1)
+
+        # New aperture size
+        self.addsize_entry_lbl = QtWidgets.QLabel('%s:' % _('New Aperture size'))
+        self.addsize_entry_lbl.setToolTip(
+            _("Size for the new aperture")
+        )
+
+        self.addsize_entry = FCDoubleSpinner()
+        self.addsize_entry.set_range(0, 100)
+        self.addsize_entry.set_precision(self.decimals)
+
+        grid0.addWidget(self.addsize_entry_lbl, 2, 0)
+        grid0.addWidget(self.addsize_entry, 2, 1)
+
+        # New aperture type
+        self.addtype_combo_lbl = QtWidgets.QLabel('%s:' % _('New Aperture type'))
+        self.addtype_combo_lbl.setToolTip(
+            _("Type for the new aperture.\n"
+              "Can be 'C', 'R' or 'O'.")
+        )
+
+        self.addtype_combo = FCComboBox()
+        self.addtype_combo.addItems(['C', 'R', 'O'])
+
+        grid0.addWidget(self.addtype_combo_lbl, 3, 0)
+        grid0.addWidget(self.addtype_combo, 3, 1)
+
+        # Number of pads in a pad array
+        self.grb_array_size_label = QtWidgets.QLabel('%s:' % _('Nr of pads'))
+        self.grb_array_size_label.setToolTip(
+            _("Specify how many pads to be in the array.")
+        )
+
+        self.grb_array_size_entry = FCSpinner()
+        self.grb_array_size_entry.set_range(0, 9999)
+
+        grid0.addWidget(self.grb_array_size_label, 4, 0)
+        grid0.addWidget(self.grb_array_size_entry, 4, 1)
+
+        self.adddim_label = QtWidgets.QLabel('%s:' % _('Aperture Dimensions'))
+        self.adddim_label.setToolTip(
+            _("Diameters of the tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
+        )
+        self.adddim_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
+        grid0.addWidget(self.adddim_label, 5, 0)
+        grid0.addWidget(self.adddim_entry, 5, 1)
+
+        self.grb_array_linear_label = QtWidgets.QLabel('<b>%s:</b>' % _('Linear Pad Array'))
+        grid0.addWidget(self.grb_array_linear_label, 6, 0, 1, 2)
+
+        # Linear Pad Array direction
+        self.grb_axis_label = QtWidgets.QLabel('%s:' % _('Linear Direction'))
+        self.grb_axis_label.setToolTip(
+            _("Direction on which the linear array is oriented:\n"
+              "- 'X' - horizontal axis \n"
+              "- 'Y' - vertical axis or \n"
+              "- 'Angle' - a custom angle for the array inclination")
+        )
+
+        self.grb_axis_radio = RadioSet([{'label': _('X'), 'value': 'X'},
+                                        {'label': _('Y'), 'value': 'Y'},
+                                        {'label': _('Angle'), 'value': 'A'}])
+
+        grid0.addWidget(self.grb_axis_label, 7, 0)
+        grid0.addWidget(self.grb_axis_radio, 7, 1)
+
+        # Linear Pad Array pitch distance
+        self.grb_pitch_label = QtWidgets.QLabel('%s:' % _('Pitch'))
+        self.grb_pitch_label.setToolTip(
+            _("Pitch = Distance between elements of the array.")
+        )
+        # self.drill_pitch_label.setMinimumWidth(100)
+        self.grb_pitch_entry = FCDoubleSpinner()
+        self.grb_pitch_entry.set_precision(self.decimals)
+
+        grid0.addWidget(self.grb_pitch_label, 8, 0)
+        grid0.addWidget(self.grb_pitch_entry, 8, 1)
+
+        # Linear Pad Array custom angle
+        self.grb_angle_label = QtWidgets.QLabel('%s:' % _('Angle'))
+        self.grb_angle_label.setToolTip(
+            _("Angle at which each element in circular array is placed.")
+        )
+        self.grb_angle_entry = FCDoubleSpinner()
+        self.grb_angle_entry.set_precision(self.decimals)
+        self.grb_angle_entry.set_range(-360, 360)
+        self.grb_angle_entry.setSingleStep(5)
+
+        grid0.addWidget(self.grb_angle_label, 9, 0)
+        grid0.addWidget(self.grb_angle_entry, 9, 1)
+
+        self.grb_array_circ_label = QtWidgets.QLabel('<b>%s:</b>' % _('Circular Pad Array'))
+        grid0.addWidget(self.grb_array_circ_label, 10, 0, 1, 2)
+
+        # Circular Pad Array direction
+        self.grb_circular_direction_label = QtWidgets.QLabel('%s:' % _('Circular Direction'))
+        self.grb_circular_direction_label.setToolTip(
+            _("Direction for circular array.\n"
+              "Can be CW = clockwise or CCW = counter clockwise.")
+        )
+
+        self.grb_circular_dir_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                                {'label': _('CCW'), 'value': 'CCW'}])
+
+        grid0.addWidget(self.grb_circular_direction_label, 11, 0)
+        grid0.addWidget(self.grb_circular_dir_radio, 11, 1)
+
+        # Circular Pad Array Angle
+        self.grb_circular_angle_label = QtWidgets.QLabel('%s:' % _('Circular Angle'))
+        self.grb_circular_angle_label.setToolTip(
+            _("Angle at which each element in circular array is placed.")
+        )
+        self.grb_circular_angle_entry = FCDoubleSpinner()
+        self.grb_circular_angle_entry.set_precision(self.decimals)
+        self.grb_circular_angle_entry.set_range(-360, 360)
+
+        self.grb_circular_angle_entry.setSingleStep(5)
+
+        grid0.addWidget(self.grb_circular_angle_label, 12, 0)
+        grid0.addWidget(self.grb_circular_angle_entry, 12, 1)
+
+        self.grb_array_tools_b_label = QtWidgets.QLabel('<b>%s:</b>' % _('Buffer Tool'))
+        grid0.addWidget(self.grb_array_tools_b_label, 13, 0, 1, 2)
+
+        # Buffer Distance
+        self.grb_buff_label = QtWidgets.QLabel('%s:' % _('Buffer distance'))
+        self.grb_buff_label.setToolTip(
+            _("Distance at which to buffer the Gerber element.")
+        )
+        self.grb_buff_entry = FCDoubleSpinner()
+        self.grb_buff_entry.set_precision(self.decimals)
+        self.grb_buff_entry.set_range(-9999, 9999)
+
+        grid0.addWidget(self.grb_buff_label, 14, 0)
+        grid0.addWidget(self.grb_buff_entry, 14, 1)
+
+        self.grb_array_tools_s_label = QtWidgets.QLabel('<b>%s:</b>' % _('Scale Tool'))
+        grid0.addWidget(self.grb_array_tools_s_label, 15, 0, 1, 2)
+
+        # Scale Factor
+        self.grb_scale_label = QtWidgets.QLabel('%s:' % _('Scale factor'))
+        self.grb_scale_label.setToolTip(
+            _("Factor to scale the Gerber element.")
+        )
+        self.grb_scale_entry = FCDoubleSpinner()
+        self.grb_scale_entry.set_precision(self.decimals)
+        self.grb_scale_entry.set_range(0, 9999)
+
+        grid0.addWidget(self.grb_scale_label, 16, 0)
+        grid0.addWidget(self.grb_scale_entry, 16, 1)
+
+        self.grb_array_tools_ma_label = QtWidgets.QLabel('<b>%s:</b>' % _('Mark Area Tool'))
+        grid0.addWidget(self.grb_array_tools_ma_label, 17, 0, 1, 2)
+
+        # Mark area Tool low threshold
+        self.grb_ma_low_label = QtWidgets.QLabel('%s:' % _('Threshold low'))
+        self.grb_ma_low_label.setToolTip(
+            _("Threshold value under which the apertures are not marked.")
+        )
+        self.grb_ma_low_entry = FCDoubleSpinner()
+        self.grb_ma_low_entry.set_precision(self.decimals)
+        self.grb_ma_low_entry.set_range(0, 9999)
+
+        grid0.addWidget(self.grb_ma_low_label, 18, 0)
+        grid0.addWidget(self.grb_ma_low_entry, 18, 1)
+
+        # Mark area Tool high threshold
+        self.grb_ma_high_label = QtWidgets.QLabel('%s:' % _('Threshold high'))
+        self.grb_ma_high_label.setToolTip(
+            _("Threshold value over which the apertures are not marked.")
+        )
+        self.grb_ma_high_entry = FCDoubleSpinner()
+        self.grb_ma_high_entry.set_precision(self.decimals)
+        self.grb_ma_high_entry.set_range(0, 9999)
+
+        grid0.addWidget(self.grb_ma_high_label, 19, 0)
+        grid0.addWidget(self.grb_ma_high_entry, 19, 1)
+
+        self.layout.addStretch()

+ 118 - 0
appGUI/preferences/gerber/GerberExpPrefGroupUI.py

@@ -0,0 +1,118 @@
+from PyQt5 import QtWidgets, QtCore
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCSpinner
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GerberExpPrefGroupUI(OptionsGroupUI):
+
+    def __init__(self, decimals=4, parent=None):
+        super(GerberExpPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Gerber Export")))
+        self.decimals = decimals
+
+        # Plot options
+        self.export_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Export Options"))
+        self.export_options_label.setToolTip(
+            _("The parameters set here are used in the file exported\n"
+              "when using the File -> Export -> Export Gerber menu entry.")
+        )
+        self.layout.addWidget(self.export_options_label)
+
+        form = QtWidgets.QFormLayout()
+        self.layout.addLayout(form)
+
+        # Gerber Units
+        self.gerber_units_label = QtWidgets.QLabel('%s:' % _('Units'))
+        self.gerber_units_label.setToolTip(
+            _("The units used in the Gerber file.")
+        )
+
+        self.gerber_units_radio = RadioSet([{'label': _('Inch'), 'value': 'IN'},
+                                            {'label': _('mm'), 'value': 'MM'}])
+        self.gerber_units_radio.setToolTip(
+            _("The units used in the Gerber file.")
+        )
+
+        form.addRow(self.gerber_units_label, self.gerber_units_radio)
+
+        # Gerber format
+        self.digits_label = QtWidgets.QLabel("%s:" % _("Int/Decimals"))
+        self.digits_label.setToolTip(
+            _("The number of digits in the whole part of the number\n"
+              "and in the fractional part of the number.")
+        )
+
+        hlay1 = QtWidgets.QHBoxLayout()
+
+        self.format_whole_entry = FCSpinner()
+        self.format_whole_entry.set_range(0, 9)
+        self.format_whole_entry.set_step(1)
+        self.format_whole_entry.setWrapping(True)
+
+        self.format_whole_entry.setMinimumWidth(30)
+        self.format_whole_entry.setToolTip(
+            _("This numbers signify the number of digits in\n"
+              "the whole part of Gerber coordinates.")
+        )
+        hlay1.addWidget(self.format_whole_entry, QtCore.Qt.AlignLeft)
+
+        gerber_separator_label = QtWidgets.QLabel(':')
+        gerber_separator_label.setFixedWidth(5)
+        hlay1.addWidget(gerber_separator_label, QtCore.Qt.AlignLeft)
+
+        self.format_dec_entry = FCSpinner()
+        self.format_dec_entry.set_range(0, 9)
+        self.format_dec_entry.set_step(1)
+        self.format_dec_entry.setWrapping(True)
+
+        self.format_dec_entry.setMinimumWidth(30)
+        self.format_dec_entry.setToolTip(
+            _("This numbers signify the number of digits in\n"
+              "the decimal part of Gerber coordinates.")
+        )
+        hlay1.addWidget(self.format_dec_entry, QtCore.Qt.AlignLeft)
+        hlay1.addStretch()
+
+        form.addRow(self.digits_label, hlay1)
+
+        # Gerber Zeros
+        self.zeros_label = QtWidgets.QLabel('%s:' % _('Zeros'))
+        self.zeros_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.zeros_label.setToolTip(
+            _("This sets the type of Gerber zeros.\n"
+              "If LZ then Leading Zeros are removed and\n"
+              "Trailing Zeros are kept.\n"
+              "If TZ is checked then Trailing Zeros are removed\n"
+              "and Leading Zeros are kept.")
+        )
+
+        self.zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'L'},
+                                     {'label': _('TZ'), 'value': 'T'}])
+        self.zeros_radio.setToolTip(
+            _("This sets the type of Gerber zeros.\n"
+              "If LZ then Leading Zeros are removed and\n"
+              "Trailing Zeros are kept.\n"
+              "If TZ is checked then Trailing Zeros are removed\n"
+              "and Leading Zeros are kept.")
+        )
+
+        form.addRow(self.zeros_label, self.zeros_radio)
+
+        self.layout.addStretch()

+ 229 - 0
appGUI/preferences/gerber/GerberGenPrefGroupUI.py

@@ -0,0 +1,229 @@
+from PyQt5 import QtWidgets, QtCore, QtGui
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCButton, FCSliderWithSpinner, FCColorEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GerberGenPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Gerber General Preferences", parent=parent)
+        super(GerberGenPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Gerber General")))
+        self.decimals = decimals
+
+        # ## Plot options
+        self.plot_options_label = QtWidgets.QLabel("<b>%s:</b>" % _("Plot Options"))
+        self.layout.addWidget(self.plot_options_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Plot CB
+        self.plot_cb = FCCheckBox(label='%s' % _('Plot'))
+        self.plot_options_label.setToolTip(
+            _("Plot (show) this object.")
+        )
+        grid0.addWidget(self.plot_cb, 0, 0)
+
+        # Solid CB
+        self.solid_cb = FCCheckBox(label='%s' % _('Solid'))
+        self.solid_cb.setToolTip(
+            _("Solid color polygons.")
+        )
+        grid0.addWidget(self.solid_cb, 0, 1)
+
+        # Multicolored CB
+        self.multicolored_cb = FCCheckBox(label='%s' % _('M-Color'))
+        self.multicolored_cb.setToolTip(
+            _("Draw polygons in different colors.")
+        )
+        grid0.addWidget(self.multicolored_cb, 0, 2)
+
+        # Number of circle steps for circular aperture linear approximation
+        self.circle_steps_label = QtWidgets.QLabel('%s:' % _("Circle Steps"))
+        self.circle_steps_label.setToolTip(
+            _("The number of circle steps for Gerber \n"
+              "circular aperture linear approximation.")
+        )
+        self.circle_steps_entry = FCSpinner()
+        self.circle_steps_entry.set_range(0, 9999)
+
+        grid0.addWidget(self.circle_steps_label, 1, 0)
+        grid0.addWidget(self.circle_steps_entry, 1, 1, 1, 2)
+
+        grid0.addWidget(QtWidgets.QLabel(''), 2, 0, 1, 3)
+
+        # Default format for Gerber
+        self.gerber_default_label = QtWidgets.QLabel('<b>%s:</b>' % _('Default Values'))
+        self.gerber_default_label.setToolTip(
+            _("Those values will be used as fallback values\n"
+              "in case that they are not found in the Gerber file.")
+        )
+
+        grid0.addWidget(self.gerber_default_label, 3, 0, 1, 3)
+
+        # Gerber Units
+        self.gerber_units_label = QtWidgets.QLabel('%s:' % _('Units'))
+        self.gerber_units_label.setToolTip(
+            _("The units used in the Gerber file.")
+        )
+
+        self.gerber_units_radio = RadioSet([{'label': _('Inch'), 'value': 'IN'},
+                                            {'label': _('mm'), 'value': 'MM'}])
+        self.gerber_units_radio.setToolTip(
+            _("The units used in the Gerber file.")
+        )
+
+        grid0.addWidget(self.gerber_units_label, 4, 0)
+        grid0.addWidget(self.gerber_units_radio, 4, 1, 1, 2)
+
+        # Gerber Zeros
+        self.gerber_zeros_label = QtWidgets.QLabel('%s:' % _('Zeros'))
+        self.gerber_zeros_label.setAlignment(QtCore.Qt.AlignLeft)
+        self.gerber_zeros_label.setToolTip(
+            _("This sets the type of Gerber zeros.\n"
+              "If LZ then Leading Zeros are removed and\n"
+              "Trailing Zeros are kept.\n"
+              "If TZ is checked then Trailing Zeros are removed\n"
+              "and Leading Zeros are kept.")
+        )
+
+        self.gerber_zeros_radio = RadioSet([{'label': _('LZ'), 'value': 'L'},
+                                            {'label': _('TZ'), 'value': 'T'}])
+        self.gerber_zeros_radio.setToolTip(
+            _("This sets the type of Gerber zeros.\n"
+              "If LZ then Leading Zeros are removed and\n"
+              "Trailing Zeros are kept.\n"
+              "If TZ is checked then Trailing Zeros are removed\n"
+              "and Leading Zeros are kept.")
+        )
+
+        grid0.addWidget(self.gerber_zeros_label, 5, 0)
+        grid0.addWidget(self.gerber_zeros_radio, 5, 1, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 6, 0, 1, 3)
+
+        # Apertures Cleaning
+        self.gerber_clean_cb = FCCheckBox(label='%s' % _('Clean Apertures'))
+        self.gerber_clean_cb.setToolTip(
+            _("Will remove apertures that do not have geometry\n"
+              "thus lowering the number of apertures in the Gerber object.")
+        )
+        grid0.addWidget(self.gerber_clean_cb, 7, 0, 1, 3)
+
+        # Apply Extra Buffering
+        self.gerber_extra_buffering = FCCheckBox(label='%s' % _('Polarity change buffer'))
+        self.gerber_extra_buffering.setToolTip(
+            _("Will apply extra buffering for the\n"
+              "solid geometry when we have polarity changes.\n"
+              "May help loading Gerber files that otherwise\n"
+              "do not load correctly.")
+        )
+        grid0.addWidget(self.gerber_extra_buffering, 8, 0, 1, 3)
+
+        # Store colors
+        self.store_colors_cb = FCCheckBox(label='%s' % _('Store colors'))
+        self.store_colors_cb.setToolTip(
+            _("It will store the set colors for Gerber objects.\n"
+              "Those will be used each time the application is started.")
+        )
+        grid0.addWidget(self.store_colors_cb, 11, 0)
+
+        # Clear stored colors
+        self.clear_colors_button = FCButton('%s' % _('Clear Colors'))
+        self.clear_colors_button.setIcon(QtGui.QIcon(self.app.resource_location + '/trash32.png'))
+        self.clear_colors_button.setToolTip(
+            _("Reset the colors associated with Gerber objects.")
+        )
+        grid0.addWidget(self.clear_colors_button, 11, 1, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 13, 0, 1, 3)
+
+        # Gerber Object Color
+        self.gerber_color_label = QtWidgets.QLabel('<b>%s</b>' % _('Object Color'))
+        grid0.addWidget(self.gerber_color_label, 15, 0, 1, 3)
+
+        # Plot Line Color
+        self.line_color_label = QtWidgets.QLabel('%s:' % _('Outline'))
+        self.line_color_label.setToolTip(
+            _("Set the line color for plotted objects.")
+        )
+        self.line_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.line_color_label, 17, 0)
+        grid0.addWidget(self.line_color_entry, 17, 1, 1, 2)
+
+        # Plot Fill Color
+        self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill'))
+        self.fill_color_label.setToolTip(
+            _("Set the fill color for plotted objects.\n"
+              "First 6 digits are the color and the last 2\n"
+              "digits are for alpha (transparency) level.")
+        )
+        self.fill_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.fill_color_label, 20, 0)
+        grid0.addWidget(self.fill_color_entry, 20, 1, 1, 2)
+
+        # Plot Fill Transparency Level
+        self.gerber_alpha_label = QtWidgets.QLabel('%s:' % _('Alpha'))
+        self.gerber_alpha_label.setToolTip(
+            _("Set the fill transparency for plotted objects.")
+        )
+        self.gerber_alpha_entry = FCSliderWithSpinner(0, 255, 1)
+
+        grid0.addWidget(self.gerber_alpha_label, 22, 0)
+        grid0.addWidget(self.gerber_alpha_entry, 22, 1, 1, 2)
+
+        self.layout.addStretch()
+
+        # Setting plot colors signals
+        self.line_color_entry.editingFinished.connect(self.on_line_color_changed)
+        self.fill_color_entry.editingFinished.connect(self.on_fill_color_changed)
+
+        self.gerber_alpha_entry.valueChanged.connect(self.on_gerber_alpha_changed)     # alpha
+
+        self.clear_colors_button.clicked.connect(self.on_colors_clear_clicked)
+
+    # Setting plot colors handlers
+    def on_fill_color_changed(self):
+        self.app.defaults['gerber_plot_fill'] = self.fill_color_entry.get_value()[:7] + \
+                                                self.app.defaults['gerber_plot_fill'][7:9]
+
+    def on_gerber_alpha_changed(self, spinner_value):
+        self.app.defaults['gerber_plot_fill'] = \
+            self.app.defaults['gerber_plot_fill'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+        self.app.defaults['gerber_plot_line'] = \
+            self.app.defaults['gerber_plot_line'][:7] + \
+            (hex(spinner_value)[2:] if int(hex(spinner_value)[2:], 16) > 0 else '00')
+
+    def on_line_color_changed(self):
+        self.app.defaults['gerber_plot_line'] = self.line_color_entry.get_value()[:7] + \
+                                                self.app.defaults['gerber_plot_line'][7:9]
+
+    def on_colors_clear_clicked(self):
+        self.app.defaults['gerber_color_list'].clear()
+        self.app.inform.emit('[WARNING_NOTCL] %s' % _("Stored colors for Gerber objects are deleted."))

+ 100 - 0
appGUI/preferences/gerber/GerberOptPrefGroupUI.py

@@ -0,0 +1,100 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCSpinner, RadioSet, FCCheckBox, FCComboBox
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GerberOptPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Gerber Options Preferences", parent=parent)
+        super(GerberOptPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.decimals = decimals
+
+        self.setTitle(str(_("Gerber Options")))
+
+        # ## Clear non-copper regions
+        self.clearcopper_label = QtWidgets.QLabel("<b>%s:</b>" % _("Non-copper regions"))
+        self.clearcopper_label.setToolTip(
+            _("Create polygons covering the\n"
+              "areas without copper on the PCB.\n"
+              "Equivalent to the inverse of this\n"
+              "object. Can be used to remove all\n"
+              "copper from a specified region.")
+        )
+        self.layout.addWidget(self.clearcopper_label)
+
+        grid1 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid1)
+
+        # Margin
+        bmlabel = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
+        bmlabel.setToolTip(
+            _("Specify the edge of the PCB\n"
+              "by drawing a box around all\n"
+              "objects with this minimum\n"
+              "distance.")
+        )
+        grid1.addWidget(bmlabel, 0, 0)
+        self.noncopper_margin_entry = FCDoubleSpinner()
+        self.noncopper_margin_entry.set_precision(self.decimals)
+        self.noncopper_margin_entry.setSingleStep(0.1)
+        self.noncopper_margin_entry.set_range(-9999, 9999)
+        grid1.addWidget(self.noncopper_margin_entry, 0, 1)
+
+        # Rounded corners
+        self.noncopper_rounded_cb = FCCheckBox(label=_("Rounded Geo"))
+        self.noncopper_rounded_cb.setToolTip(
+            _("Resulting geometry will have rounded corners.")
+        )
+        grid1.addWidget(self.noncopper_rounded_cb, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid1.addWidget(separator_line, 2, 0, 1, 2)
+
+        # ## Bounding box
+        self.boundingbox_label = QtWidgets.QLabel('<b>%s:</b>' % _('Bounding Box'))
+        self.layout.addWidget(self.boundingbox_label)
+
+        grid2 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid2)
+
+        bbmargin = QtWidgets.QLabel('%s:' % _('Boundary Margin'))
+        bbmargin.setToolTip(
+            _("Distance of the edges of the box\n"
+              "to the nearest polygon.")
+        )
+        self.bbmargin_entry = FCDoubleSpinner()
+        self.bbmargin_entry.set_precision(self.decimals)
+        self.bbmargin_entry.setSingleStep(0.1)
+        self.bbmargin_entry.set_range(-9999, 9999)
+
+        grid2.addWidget(bbmargin, 0, 0)
+        grid2.addWidget(self.bbmargin_entry, 0, 1)
+
+        self.bbrounded_cb = FCCheckBox(label='%s' % _("Rounded Geo"))
+        self.bbrounded_cb.setToolTip(
+            _("If the bounding box is \n"
+              "to have rounded corners\n"
+              "their radius is equal to\n"
+              "the margin.")
+        )
+        grid2.addWidget(self.bbrounded_cb, 1, 0, 1, 2)
+        self.layout.addStretch()

+ 54 - 0
appGUI/preferences/gerber/GerberPreferencesUI.py

@@ -0,0 +1,54 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.preferences.gerber.GerberEditorPrefGroupUI import GerberEditorPrefGroupUI
+from appGUI.preferences.gerber.GerberExpPrefGroupUI import GerberExpPrefGroupUI
+from appGUI.preferences.gerber.GerberAdvOptPrefGroupUI import GerberAdvOptPrefGroupUI
+from appGUI.preferences.gerber.GerberOptPrefGroupUI import GerberOptPrefGroupUI
+from appGUI.preferences.gerber.GerberGenPrefGroupUI import GerberGenPrefGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class GerberPreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, decimals, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QHBoxLayout()
+        self.setLayout(self.layout)
+        self.decimals = decimals
+
+        self.gerber_gen_group = GerberGenPrefGroupUI(decimals=self.decimals)
+        self.gerber_gen_group.setMinimumWidth(250)
+        self.gerber_opt_group = GerberOptPrefGroupUI(decimals=self.decimals)
+        self.gerber_opt_group.setMinimumWidth(250)
+        self.gerber_exp_group = GerberExpPrefGroupUI(decimals=self.decimals)
+        self.gerber_exp_group.setMinimumWidth(230)
+        self.gerber_adv_opt_group = GerberAdvOptPrefGroupUI(decimals=self.decimals)
+        self.gerber_adv_opt_group.setMinimumWidth(200)
+        self.gerber_editor_group = GerberEditorPrefGroupUI(decimals=self.decimals)
+        self.gerber_editor_group.setMinimumWidth(200)
+
+        self.vlay = QtWidgets.QVBoxLayout()
+        self.vlay.addWidget(self.gerber_opt_group)
+        self.vlay.addWidget(self.gerber_adv_opt_group)
+        self.vlay.addWidget(self.gerber_exp_group)
+        self.vlay.addStretch()
+
+        self.layout.addWidget(self.gerber_gen_group)
+        self.layout.addLayout(self.vlay)
+        self.layout.addWidget(self.gerber_editor_group)
+
+        self.layout.addStretch()

+ 0 - 0
appGUI/preferences/gerber/__init__.py


+ 301 - 0
appGUI/preferences/tools/Tools2CThievingPrefGroupUI.py

@@ -0,0 +1,301 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCSpinner, FCDoubleSpinner, RadioSet, FCLabel
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2CThievingPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2CThievingPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Copper Thieving Tool Options")))
+        self.decimals = decimals
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        # ## Parameters
+        self.cflabel = FCLabel('<b>%s</b>' % _('Parameters'))
+        self.cflabel.setToolTip(
+            _("A tool to generate a Copper Thieving that can be added\n"
+              "to a selected Gerber file.")
+        )
+        grid_lay.addWidget(self.cflabel, 0, 0, 1, 2)
+
+        # CIRCLE STEPS - to be used when buffering
+        self.circle_steps_lbl = FCLabel('%s:' % _("Circle Steps"))
+        self.circle_steps_lbl.setToolTip(
+            _("Number of steps (lines) used to interpolate circles.")
+        )
+
+        self.circlesteps_entry = FCSpinner()
+        self.circlesteps_entry.set_range(1, 9999)
+
+        grid_lay.addWidget(self.circle_steps_lbl, 2, 0)
+        grid_lay.addWidget(self.circlesteps_entry, 2, 1)
+
+        # CLEARANCE #
+        self.clearance_label = FCLabel('%s:' % _("Clearance"))
+        self.clearance_label.setToolTip(
+            _("This set the distance between the copper Thieving components\n"
+              "(the polygon fill may be split in multiple polygons)\n"
+              "and the copper traces in the Gerber file.")
+        )
+        self.clearance_entry = FCDoubleSpinner()
+        self.clearance_entry.setMinimum(0.00001)
+        self.clearance_entry.set_precision(self.decimals)
+        self.clearance_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.clearance_label, 4, 0)
+        grid_lay.addWidget(self.clearance_entry, 4, 1)
+
+        # MARGIN #
+        self.margin_label = FCLabel('%s:' % _("Margin"))
+        self.margin_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.margin_entry = FCDoubleSpinner()
+        self.margin_entry.setMinimum(0.0)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.margin_label, 6, 0)
+        grid_lay.addWidget(self.margin_entry, 6, 1)
+
+        # Area #
+        self.area_label = FCLabel('%s:' % _("Area"))
+        self.area_label.setToolTip(
+            _("Thieving areas with area less then this value will not be added.")
+        )
+        self.area_entry = FCDoubleSpinner()
+        self.area_entry.set_range(0.0, 10000.0000)
+        self.area_entry.set_precision(self.decimals)
+        self.area_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.area_label, 8, 0)
+        grid_lay.addWidget(self.area_entry, 8, 1)
+        
+        # Reference #
+        self.reference_radio = RadioSet([
+            {'label': _('Itself'), 'value': 'itself'},
+            {"label": _("Area Selection"), "value": "area"},
+            {'label': _("Reference Object"), 'value': 'box'}
+        ], orientation='vertical', stretch=False)
+        self.reference_label = FCLabel(_("Reference:"))
+        self.reference_label.setToolTip(
+            _("- 'Itself' - the copper thieving extent is based on the object extent.\n"
+              "- 'Area Selection' - left mouse click to start selection of the area to be filled.\n"
+              "- 'Reference Object' - will do copper thieving within the area specified by another object.")
+        )
+        grid_lay.addWidget(self.reference_label, 10, 0)
+        grid_lay.addWidget(self.reference_radio, 10, 1)
+
+        # Bounding Box Type #
+        self.bbox_type_radio = RadioSet([
+            {'label': _('Rectangular'), 'value': 'rect'},
+            {"label": _("Minimal"), "value": "min"}
+        ], stretch=False)
+        self.bbox_type_label = FCLabel('%s:' % _("Box Type"))
+        self.bbox_type_label.setToolTip(
+            _("- 'Rectangular' - the bounding box will be of rectangular shape.\n"
+              "- 'Minimal' - the bounding box will be the convex hull shape.")
+        )
+        grid_lay.addWidget(self.bbox_type_label, 12, 0)
+        grid_lay.addWidget(self.bbox_type_radio, 12, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 14, 0, 1, 2)
+
+        # Fill Type
+        self.fill_type_radio = RadioSet([
+            {'label': _('Solid'), 'value': 'solid'},
+            {"label": _("Dots Grid"), "value": "dot"},
+            {"label": _("Squares Grid"), "value": "square"},
+            {"label": _("Lines Grid"), "value": "line"}
+        ], orientation='vertical', stretch=False)
+        self.fill_type_label = FCLabel(_("Fill Type:"))
+        self.fill_type_label.setToolTip(
+            _("- 'Solid' - copper thieving will be a solid polygon.\n"
+              "- 'Dots Grid' - the empty area will be filled with a pattern of dots.\n"
+              "- 'Squares Grid' - the empty area will be filled with a pattern of squares.\n"
+              "- 'Lines Grid' - the empty area will be filled with a pattern of lines.")
+        )
+        grid_lay.addWidget(self.fill_type_label, 16, 0)
+        grid_lay.addWidget(self.fill_type_radio, 16, 1)
+
+        self.dots_label = FCLabel('<b>%s</b>:' % _("Dots Grid Parameters"))
+        grid_lay.addWidget(self.dots_label, 18, 0, 1, 2)
+
+        # Dot diameter #
+        self.dotdia_label = FCLabel('%s:' % _("Dia"))
+        self.dotdia_label.setToolTip(
+            _("Dot diameter in Dots Grid.")
+        )
+        self.dot_dia_entry = FCDoubleSpinner()
+        self.dot_dia_entry.set_range(0.0, 10000.0000)
+        self.dot_dia_entry.set_precision(self.decimals)
+        self.dot_dia_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.dotdia_label, 20, 0)
+        grid_lay.addWidget(self.dot_dia_entry, 20, 1)
+
+        # Dot spacing #
+        self.dotspacing_label = FCLabel('%s:' % _("Spacing"))
+        self.dotspacing_label.setToolTip(
+            _("Distance between each two dots in Dots Grid.")
+        )
+        self.dot_spacing_entry = FCDoubleSpinner()
+        self.dot_spacing_entry.set_range(0.0, 10000.0000)
+        self.dot_spacing_entry.set_precision(self.decimals)
+        self.dot_spacing_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.dotspacing_label, 22, 0)
+        grid_lay.addWidget(self.dot_spacing_entry, 22, 1)
+
+        self.squares_label = FCLabel('<b>%s</b>:' % _("Squares Grid Parameters"))
+        grid_lay.addWidget(self.squares_label, 24, 0, 1, 2)
+
+        # Square Size #
+        self.square_size_label = FCLabel('%s:' % _("Size"))
+        self.square_size_label.setToolTip(
+            _("Square side size in Squares Grid.")
+        )
+        self.square_size_entry = FCDoubleSpinner()
+        self.square_size_entry.set_range(0.0, 10000.0000)
+        self.square_size_entry.set_precision(self.decimals)
+        self.square_size_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.square_size_label, 26, 0)
+        grid_lay.addWidget(self.square_size_entry, 26, 1)
+
+        # Squares spacing #
+        self.squares_spacing_label = FCLabel('%s:' % _("Spacing"))
+        self.squares_spacing_label.setToolTip(
+            _("Distance between each two squares in Squares Grid.")
+        )
+        self.squares_spacing_entry = FCDoubleSpinner()
+        self.squares_spacing_entry.set_range(0.0, 10000.0000)
+        self.squares_spacing_entry.set_precision(self.decimals)
+        self.squares_spacing_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.squares_spacing_label, 28, 0)
+        grid_lay.addWidget(self.squares_spacing_entry, 28, 1)
+
+        self.lines_label = FCLabel('<b>%s</b>:' % _("Lines Grid Parameters"))
+        grid_lay.addWidget(self.lines_label, 30, 0, 1, 2)
+
+        # Square Size #
+        self.line_size_label = FCLabel('%s:' % _("Size"))
+        self.line_size_label.setToolTip(
+            _("Line thickness size in Lines Grid.")
+        )
+        self.line_size_entry = FCDoubleSpinner()
+        self.line_size_entry.set_range(0.0, 10000.0000)
+        self.line_size_entry.set_precision(self.decimals)
+        self.line_size_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.line_size_label, 32, 0)
+        grid_lay.addWidget(self.line_size_entry, 32, 1)
+
+        # Lines spacing #
+        self.lines_spacing_label = FCLabel('%s:' % _("Spacing"))
+        self.lines_spacing_label.setToolTip(
+            _("Distance between each two lines in Lines Grid.")
+        )
+        self.lines_spacing_entry = FCDoubleSpinner()
+        self.lines_spacing_entry.set_range(0.0, 10000.0000)
+        self.lines_spacing_entry.set_precision(self.decimals)
+        self.lines_spacing_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.lines_spacing_label, 34, 0)
+        grid_lay.addWidget(self.lines_spacing_entry, 34, 1)
+
+        self.robber_bar_label = FCLabel('<b>%s</b>' % _('Robber Bar Parameters'))
+        self.robber_bar_label.setToolTip(
+            _("Parameters used for the robber bar.\n"
+              "Robber bar = copper border to help in pattern hole plating.")
+        )
+        grid_lay.addWidget(self.robber_bar_label, 36, 0, 1, 2)
+
+        # ROBBER BAR MARGIN #
+        self.rb_margin_label = FCLabel('%s:' % _("Margin"))
+        self.rb_margin_label.setToolTip(
+            _("Bounding box margin for robber bar.")
+        )
+        self.rb_margin_entry = FCDoubleSpinner()
+        self.rb_margin_entry.set_range(-10000.0000, 10000.0000)
+        self.rb_margin_entry.set_precision(self.decimals)
+        self.rb_margin_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.rb_margin_label, 38, 0)
+        grid_lay.addWidget(self.rb_margin_entry, 38, 1)
+
+        # THICKNESS #
+        self.rb_thickness_label = FCLabel('%s:' % _("Thickness"))
+        self.rb_thickness_label.setToolTip(
+            _("The robber bar thickness.")
+        )
+        self.rb_thickness_entry = FCDoubleSpinner()
+        self.rb_thickness_entry.set_range(0.0000, 10000.0000)
+        self.rb_thickness_entry.set_precision(self.decimals)
+        self.rb_thickness_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.rb_thickness_label, 40, 0)
+        grid_lay.addWidget(self.rb_thickness_entry, 40, 1)
+
+        self.patern_mask_label = FCLabel('<b>%s</b>' % _('Pattern Plating Mask'))
+        self.patern_mask_label.setToolTip(
+            _("Generate a mask for pattern plating.")
+        )
+        grid_lay.addWidget(self.patern_mask_label, 42, 0, 1, 2)
+
+        # Openings CLEARANCE #
+        self.clearance_ppm_label = FCLabel('%s:' % _("Clearance"))
+        self.clearance_ppm_label.setToolTip(
+            _("The distance between the possible copper thieving elements\n"
+              "and/or robber bar and the actual openings in the mask.")
+        )
+        self.clearance_ppm_entry = FCDoubleSpinner()
+        self.clearance_ppm_entry.set_range(-10000.0000, 10000.0000)
+        self.clearance_ppm_entry.set_precision(self.decimals)
+        self.clearance_ppm_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.clearance_ppm_label, 44, 0)
+        grid_lay.addWidget(self.clearance_ppm_entry, 44, 1)
+
+        # Include geometry
+        self.ppm_choice_label = FCLabel('%s:' % _("Add"))
+        self.ppm_choice_label.setToolTip(
+            _("Choose which additional geometry to include, if available.")
+        )
+        self.ppm_choice_radio = RadioSet([
+            {"label": _("Both"), "value": "b"},
+            {'label': _('Thieving'), 'value': 't'},
+            {"label": _("Robber bar"), "value": "r"},
+            {"label": _("None"), "value": "n"}
+        ], orientation='vertical', stretch=False)
+        grid_lay.addWidget(self.ppm_choice_label, 46, 0)
+        grid_lay.addWidget(self.ppm_choice_radio, 46, 1)
+
+        self.layout.addStretch()

+ 138 - 0
appGUI/preferences/tools/Tools2CalPrefGroupUI.py

@@ -0,0 +1,138 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, NumericalEvalTupleEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2CalPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2CalPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Calibration Tool Options")))
+        self.decimals = decimals
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+
+        # Calibration source
+        self.cal_source_lbl = QtWidgets.QLabel("<b>%s:</b>" % _("Source Type"))
+        self.cal_source_lbl.setToolTip(_("The source of calibration points.\n"
+                                         "It can be:\n"
+                                         "- Object -> click a hole geo for Excellon or a pad for Gerber\n"
+                                         "- Free -> click freely on canvas to acquire the calibration points"))
+        self.cal_source_radio = RadioSet([{'label': _('Object'), 'value': 'object'},
+                                          {'label': _('Free'), 'value': 'free'}],
+                                         stretch=False)
+
+        grid_lay.addWidget(self.cal_source_lbl, 1, 0)
+        grid_lay.addWidget(self.cal_source_radio, 1, 1, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 2, 0, 1, 2)
+
+        # Travel Z entry
+        travelz_lbl = QtWidgets.QLabel('%s:' % _("Travel Z"))
+        travelz_lbl.setToolTip(
+            _("Height (Z) for travelling between the points.")
+        )
+
+        self.travelz_entry = FCDoubleSpinner()
+        self.travelz_entry.set_range(-10000.0000, 10000.0000)
+        self.travelz_entry.set_precision(self.decimals)
+        self.travelz_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(travelz_lbl, 3, 0)
+        grid_lay.addWidget(self.travelz_entry, 3, 1, 1, 2)
+
+        # Verification Z entry
+        verz_lbl = QtWidgets.QLabel('%s:' % _("Verification Z"))
+        verz_lbl.setToolTip(
+            _("Height (Z) for checking the point.")
+        )
+
+        self.verz_entry = FCDoubleSpinner()
+        self.verz_entry.set_range(-10000.0000, 10000.0000)
+        self.verz_entry.set_precision(self.decimals)
+        self.verz_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(verz_lbl, 4, 0)
+        grid_lay.addWidget(self.verz_entry, 4, 1, 1, 2)
+
+        # Zero the Z of the verification tool
+        self.zeroz_cb = FCCheckBox('%s' % _("Zero Z tool"))
+        self.zeroz_cb.setToolTip(
+            _("Include a sequence to zero the height (Z)\n"
+              "of the verification tool.")
+        )
+
+        grid_lay.addWidget(self.zeroz_cb, 5, 0, 1, 3)
+
+        # Toochange Z entry
+        toolchangez_lbl = QtWidgets.QLabel('%s:' % _("Toolchange Z"))
+        toolchangez_lbl.setToolTip(
+            _("Height (Z) for mounting the verification probe.")
+        )
+
+        self.toolchangez_entry = FCDoubleSpinner()
+        self.toolchangez_entry.set_range(0.0000, 10000.0000)
+        self.toolchangez_entry.set_precision(self.decimals)
+        self.toolchangez_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(toolchangez_lbl, 6, 0)
+        grid_lay.addWidget(self.toolchangez_entry, 6, 1, 1, 2)
+
+        # Toolchange X-Y entry
+        toolchangexy_lbl = QtWidgets.QLabel('%s:' % _('Toolchange X-Y'))
+        toolchangexy_lbl.setToolTip(
+            _("Toolchange X,Y position.\n"
+              "If no value is entered then the current\n"
+              "(x, y) point will be used,")
+        )
+
+        self.toolchange_xy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
+        grid_lay.addWidget(toolchangexy_lbl, 7, 0)
+        grid_lay.addWidget(self.toolchange_xy_entry, 7, 1, 1, 2)
+
+        # Second point choice
+        second_point_lbl = QtWidgets.QLabel('%s:' % _("Second point"))
+        second_point_lbl.setToolTip(
+            _("Second point in the Gcode verification can be:\n"
+              "- top-left -> the user will align the PCB vertically\n"
+              "- bottom-right -> the user will align the PCB horizontally")
+        )
+        self.second_point_radio = RadioSet([{'label': _('Top Left'), 'value': 'tl'},
+                                            {'label': _('Bottom Right'), 'value': 'br'}],
+                                           orientation='vertical')
+
+        grid_lay.addWidget(second_point_lbl, 8, 0)
+        grid_lay.addWidget(self.second_point_radio, 8, 1, 1, 2)
+
+        self.layout.addStretch()

+ 231 - 0
appGUI/preferences/tools/Tools2EDrillsPrefGroupUI.py

@@ -0,0 +1,231 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCCheckBox, RadioSet, FCDoubleSpinner
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2EDrillsPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2EDrillsPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Extract Drills Options")))
+        self.decimals = decimals
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+
+        self.padt_label = QtWidgets.QLabel("<b>%s:</b>" % _("Processed Pads Type"))
+        self.padt_label.setToolTip(
+            _("The type of pads shape to be processed.\n"
+              "If the PCB has many SMD pads with rectangular pads,\n"
+              "disable the Rectangular aperture.")
+        )
+
+        grid_lay.addWidget(self.padt_label, 2, 0, 1, 2)
+
+        # Circular Aperture Selection
+        self.circular_cb = FCCheckBox('%s' % _("Circular"))
+        self.circular_cb.setToolTip(
+            _("Process Circular Pads.")
+        )
+
+        grid_lay.addWidget(self.circular_cb, 3, 0, 1, 2)
+
+        # Oblong Aperture Selection
+        self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
+        self.oblong_cb.setToolTip(
+            _("Process Oblong Pads.")
+        )
+
+        grid_lay.addWidget(self.oblong_cb, 4, 0, 1, 2)
+
+        # Square Aperture Selection
+        self.square_cb = FCCheckBox('%s' % _("Square"))
+        self.square_cb.setToolTip(
+            _("Process Square Pads.")
+        )
+
+        grid_lay.addWidget(self.square_cb, 5, 0, 1, 2)
+
+        # Rectangular Aperture Selection
+        self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
+        self.rectangular_cb.setToolTip(
+            _("Process Rectangular Pads.")
+        )
+
+        grid_lay.addWidget(self.rectangular_cb, 6, 0, 1, 2)
+
+        # Others type of Apertures Selection
+        self.other_cb = FCCheckBox('%s' % _("Others"))
+        self.other_cb.setToolTip(
+            _("Process pads not in the categories above.")
+        )
+
+        grid_lay.addWidget(self.other_cb, 7, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 8, 0, 1, 2)
+
+        # ## Axis
+        self.hole_size_radio = RadioSet(
+            [
+                {'label': _("Fixed Diameter"), 'value': 'fixed'},
+                {'label': _("Fixed Annular Ring"), 'value': 'ring'},
+                {'label': _("Proportional"), 'value': 'prop'}
+            ],
+            orientation='vertical',
+            stretch=False)
+        self.hole_size_label = QtWidgets.QLabel('<b>%s:</b>' % _("Method"))
+        self.hole_size_label.setToolTip(
+            _("The method for processing pads. Can be:\n"
+              "- Fixed Diameter -> all holes will have a set size\n"
+              "- Fixed Annular Ring -> all holes will have a set annular ring\n"
+              "- Proportional -> each hole size will be a fraction of the pad size"))
+
+        grid_lay.addWidget(self.hole_size_label, 9, 0)
+        grid_lay.addWidget(self.hole_size_radio, 9, 1)
+
+        # grid_lay1.addWidget(QtWidgets.QLabel(''))
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 10, 0, 1, 2)
+
+        # Annular Ring
+        self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
+        grid_lay.addWidget(self.fixed_label, 11, 0, 1, 2)
+
+        # Diameter value
+        self.dia_entry = FCDoubleSpinner()
+        self.dia_entry.set_precision(self.decimals)
+        self.dia_entry.set_range(0.0000, 10000.0000)
+
+        self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.dia_label.setToolTip(
+            _("Fixed hole diameter.")
+        )
+
+        grid_lay.addWidget(self.dia_label, 12, 0)
+        grid_lay.addWidget(self.dia_entry, 12, 1)
+
+        # Annular Ring value
+        self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
+        self.ring_label.setToolTip(
+            _("The size of annular ring.\n"
+              "The copper sliver between the hole exterior\n"
+              "and the margin of the copper pad.")
+        )
+        grid_lay.addWidget(self.ring_label, 13, 0, 1, 2)
+
+        # Circular Annular Ring Value
+        self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
+        self.circular_ring_label.setToolTip(
+            _("The size of annular ring for circular pads.")
+        )
+
+        self.circular_ring_entry = FCDoubleSpinner()
+        self.circular_ring_entry.set_precision(self.decimals)
+        self.circular_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.circular_ring_label, 14, 0)
+        grid_lay.addWidget(self.circular_ring_entry, 14, 1)
+
+        # Oblong Annular Ring Value
+        self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
+        self.oblong_ring_label.setToolTip(
+            _("The size of annular ring for oblong pads.")
+        )
+
+        self.oblong_ring_entry = FCDoubleSpinner()
+        self.oblong_ring_entry.set_precision(self.decimals)
+        self.oblong_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.oblong_ring_label, 15, 0)
+        grid_lay.addWidget(self.oblong_ring_entry, 15, 1)
+
+        # Square Annular Ring Value
+        self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
+        self.square_ring_label.setToolTip(
+            _("The size of annular ring for square pads.")
+        )
+
+        self.square_ring_entry = FCDoubleSpinner()
+        self.square_ring_entry.set_precision(self.decimals)
+        self.square_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.square_ring_label, 16, 0)
+        grid_lay.addWidget(self.square_ring_entry, 16, 1)
+
+        # Rectangular Annular Ring Value
+        self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
+        self.rectangular_ring_label.setToolTip(
+            _("The size of annular ring for rectangular pads.")
+        )
+
+        self.rectangular_ring_entry = FCDoubleSpinner()
+        self.rectangular_ring_entry.set_precision(self.decimals)
+        self.rectangular_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.rectangular_ring_label, 17, 0)
+        grid_lay.addWidget(self.rectangular_ring_entry, 17, 1)
+
+        # Others Annular Ring Value
+        self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
+        self.other_ring_label.setToolTip(
+            _("The size of annular ring for other pads.")
+        )
+
+        self.other_ring_entry = FCDoubleSpinner()
+        self.other_ring_entry.set_precision(self.decimals)
+        self.other_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.other_ring_label, 18, 0)
+        grid_lay.addWidget(self.other_ring_entry, 18, 1)
+
+        self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
+        grid_lay.addWidget(self.prop_label, 19, 0, 1, 2)
+
+        # Factor value
+        self.factor_entry = FCDoubleSpinner(suffix='%')
+        self.factor_entry.set_precision(self.decimals)
+        self.factor_entry.set_range(0.0000, 100.0000)
+        self.factor_entry.setSingleStep(0.1)
+
+        self.factor_label = QtWidgets.QLabel('%s:' % _("Factor"))
+        self.factor_label.setToolTip(
+            _("Proportional Diameter.\n"
+              "The hole diameter will be a fraction of the pad size.")
+        )
+
+        grid_lay.addWidget(self.factor_label, 20, 0)
+        grid_lay.addWidget(self.factor_entry, 20, 1)
+
+        self.layout.addStretch()

+ 135 - 0
appGUI/preferences/tools/Tools2FiducialsPrefGroupUI.py

@@ -0,0 +1,135 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, RadioSet
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2FiducialsPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2FiducialsPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Fiducials Tool Options")))
+        self.decimals = decimals
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+
+        # DIAMETER #
+        self.dia_label = QtWidgets.QLabel('%s:' % _("Size"))
+        self.dia_label.setToolTip(
+            _("This set the fiducial diameter if fiducial type is circular,\n"
+              "otherwise is the size of the fiducial.\n"
+              "The soldermask opening is double than that.")
+        )
+        self.dia_entry = FCDoubleSpinner()
+        self.dia_entry.set_range(1.0000, 3.0000)
+        self.dia_entry.set_precision(self.decimals)
+        self.dia_entry.setWrapping(True)
+        self.dia_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.dia_label, 1, 0)
+        grid_lay.addWidget(self.dia_entry, 1, 1)
+
+        # MARGIN #
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
+        self.margin_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.margin_entry = FCDoubleSpinner()
+        self.margin_entry.set_range(-10000.0000, 10000.0000)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.margin_label, 2, 0)
+        grid_lay.addWidget(self.margin_entry, 2, 1)
+
+        # Mode #
+        self.mode_radio = RadioSet([
+            {'label': _('Auto'), 'value': 'auto'},
+            {"label": _("Manual"), "value": "manual"}
+        ], stretch=False)
+        self.mode_label = QtWidgets.QLabel('%s:' % _("Mode"))
+        self.mode_label.setToolTip(
+            _("- 'Auto' - automatic placement of fiducials in the corners of the bounding box.\n"
+              "- 'Manual' - manual placement of fiducials.")
+        )
+        grid_lay.addWidget(self.mode_label, 3, 0)
+        grid_lay.addWidget(self.mode_radio, 3, 1)
+
+        # Position for second fiducial #
+        self.pos_radio = RadioSet([
+            {'label': _('Up'), 'value': 'up'},
+            {"label": _("Down"), "value": "down"},
+            {"label": _("None"), "value": "no"}
+        ], stretch=False)
+        self.pos_label = QtWidgets.QLabel('%s:' % _("Second fiducial"))
+        self.pos_label.setToolTip(
+            _("The position for the second fiducial.\n"
+              "- 'Up' - the order is: bottom-left, top-left, top-right.\n"
+              "- 'Down' - the order is: bottom-left, bottom-right, top-right.\n"
+              "- 'None' - there is no second fiducial. The order is: bottom-left, top-right.")
+        )
+        grid_lay.addWidget(self.pos_label, 4, 0)
+        grid_lay.addWidget(self.pos_radio, 4, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 5, 0, 1, 2)
+
+        # Fiducial type #
+        self.fid_type_radio = RadioSet([
+            {'label': _('Circular'), 'value': 'circular'},
+            {"label": _("Cross"), "value": "cross"},
+            {"label": _("Chess"), "value": "chess"}
+        ], stretch=False)
+
+        self.fid_type_label = QtWidgets.QLabel('%s:' % _("Fiducial Type"))
+        self.fid_type_label.setToolTip(
+            _("The type of fiducial.\n"
+              "- 'Circular' - this is the regular fiducial.\n"
+              "- 'Cross' - cross lines fiducial.\n"
+              "- 'Chess' - chess pattern fiducial.")
+        )
+        grid_lay.addWidget(self.fid_type_label, 6, 0)
+        grid_lay.addWidget(self.fid_type_radio, 6, 1)
+
+        # Line Thickness #
+        self.line_thickness_label = QtWidgets.QLabel('%s:' % _("Line thickness"))
+        self.line_thickness_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.line_thickness_entry = FCDoubleSpinner()
+        self.line_thickness_entry.set_range(0.00001, 10000.0000)
+        self.line_thickness_entry.set_precision(self.decimals)
+        self.line_thickness_entry.setSingleStep(0.1)
+
+        grid_lay.addWidget(self.line_thickness_label, 7, 0)
+        grid_lay.addWidget(self.line_thickness_entry, 7, 1)
+
+        self.layout.addStretch()

+ 75 - 0
appGUI/preferences/tools/Tools2InvertPrefGroupUI.py

@@ -0,0 +1,75 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, RadioSet
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2InvertPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2InvertPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Invert Gerber Tool Options")))
+        self.decimals = decimals
+
+        # ## Subtractor Tool Parameters
+        self.sublabel = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.sublabel.setToolTip(
+            _("A tool to invert Gerber geometry from positive to negative\n"
+              "and in revers.")
+        )
+        self.layout.addWidget(self.sublabel)
+
+        # Grid Layout
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
+        # Margin
+        self.margin_label = QtWidgets.QLabel('%s:' % _('Margin'))
+        self.margin_label.setToolTip(
+            _("Distance by which to avoid\n"
+              "the edges of the Gerber object.")
+        )
+        self.margin_entry = FCDoubleSpinner()
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.set_range(0.0000, 10000.0000)
+        self.margin_entry.setObjectName(_("Margin"))
+
+        grid0.addWidget(self.margin_label, 2, 0, 1, 2)
+        grid0.addWidget(self.margin_entry, 3, 0, 1, 2)
+
+        self.join_label = QtWidgets.QLabel('%s:' % _("Lines Join Style"))
+        self.join_label.setToolTip(
+            _("The way that the lines in the object outline will be joined.\n"
+              "Can be:\n"
+              "- rounded -> an arc is added between two joining lines\n"
+              "- square -> the lines meet in 90 degrees angle\n"
+              "- bevel -> the lines are joined by a third line")
+        )
+        self.join_radio = RadioSet([
+            {'label': _('Rounded'), 'value': 'r'},
+            {'label': _('Square'), 'value': 's'},
+            {'label': _('Bevel'), 'value': 'b'}
+        ], orientation='vertical', stretch=False)
+
+        grid0.addWidget(self.join_label, 5, 0, 1, 2)
+        grid0.addWidget(self.join_radio, 7, 0, 1, 2)
+
+        self.layout.addStretch()

+ 56 - 0
appGUI/preferences/tools/Tools2OptimalPrefGroupUI.py

@@ -0,0 +1,56 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCSpinner
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2OptimalPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2OptimalPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Optimal Tool Options")))
+        self.decimals = decimals
+
+        # ## Parameters
+        self.optlabel = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.optlabel.setToolTip(
+            _("A tool to find the minimum distance between\n"
+              "every two Gerber geometric elements")
+        )
+        self.layout.addWidget(self.optlabel)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        self.precision_sp = FCSpinner()
+        self.precision_sp.set_range(2, 10)
+        self.precision_sp.set_step(1)
+        self.precision_sp.setWrapping(True)
+
+        self.precision_lbl = QtWidgets.QLabel('%s:' % _("Precision"))
+        self.precision_lbl.setToolTip(
+            _("Number of decimals for the distances and coordinates in this tool.")
+        )
+
+        grid0.addWidget(self.precision_lbl, 0, 0)
+        grid0.addWidget(self.precision_sp, 0, 1)
+
+        self.layout.addStretch()

+ 89 - 0
appGUI/preferences/tools/Tools2PreferencesUI.py

@@ -0,0 +1,89 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.preferences.tools.Tools2InvertPrefGroupUI import Tools2InvertPrefGroupUI
+from appGUI.preferences.tools.Tools2PunchGerberPrefGroupUI import Tools2PunchGerberPrefGroupUI
+from appGUI.preferences.tools.Tools2EDrillsPrefGroupUI import Tools2EDrillsPrefGroupUI
+from appGUI.preferences.tools.Tools2CalPrefGroupUI import Tools2CalPrefGroupUI
+from appGUI.preferences.tools.Tools2FiducialsPrefGroupUI import Tools2FiducialsPrefGroupUI
+from appGUI.preferences.tools.Tools2CThievingPrefGroupUI import Tools2CThievingPrefGroupUI
+from appGUI.preferences.tools.Tools2QRCodePrefGroupUI import Tools2QRCodePrefGroupUI
+from appGUI.preferences.tools.Tools2OptimalPrefGroupUI import Tools2OptimalPrefGroupUI
+from appGUI.preferences.tools.Tools2RulesCheckPrefGroupUI import Tools2RulesCheckPrefGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2PreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, decimals, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QHBoxLayout()
+        self.setLayout(self.layout)
+        self.decimals = decimals
+
+        self.tools2_checkrules_group = Tools2RulesCheckPrefGroupUI(decimals=self.decimals)
+        self.tools2_checkrules_group.setMinimumWidth(220)
+
+        self.tools2_optimal_group = Tools2OptimalPrefGroupUI(decimals=self.decimals)
+        self.tools2_optimal_group.setMinimumWidth(220)
+
+        self.tools2_qrcode_group = Tools2QRCodePrefGroupUI(decimals=self.decimals)
+        self.tools2_qrcode_group.setMinimumWidth(220)
+
+        self.tools2_cfill_group = Tools2CThievingPrefGroupUI(decimals=self.decimals)
+        self.tools2_cfill_group.setMinimumWidth(220)
+
+        self.tools2_fiducials_group = Tools2FiducialsPrefGroupUI(decimals=self.decimals)
+        self.tools2_fiducials_group.setMinimumWidth(220)
+
+        self.tools2_cal_group = Tools2CalPrefGroupUI(decimals=self.decimals)
+        self.tools2_cal_group.setMinimumWidth(220)
+
+        self.tools2_edrills_group = Tools2EDrillsPrefGroupUI(decimals=self.decimals)
+        self.tools2_edrills_group.setMinimumWidth(220)
+
+        self.tools2_punch_group = Tools2PunchGerberPrefGroupUI(decimals=self.decimals)
+        self.tools2_punch_group.setMinimumWidth(220)
+
+        self.tools2_invert_group = Tools2InvertPrefGroupUI(decimals=self.decimals)
+        self.tools2_invert_group.setMinimumWidth(220)
+
+        self.vlay = QtWidgets.QVBoxLayout()
+        self.vlay.addWidget(self.tools2_checkrules_group)
+        self.vlay.addWidget(self.tools2_optimal_group)
+
+        self.vlay1 = QtWidgets.QVBoxLayout()
+        self.vlay1.addWidget(self.tools2_qrcode_group)
+        self.vlay1.addWidget(self.tools2_fiducials_group)
+
+        self.vlay2 = QtWidgets.QVBoxLayout()
+        self.vlay2.addWidget(self.tools2_cfill_group)
+
+        self.vlay3 = QtWidgets.QVBoxLayout()
+        self.vlay3.addWidget(self.tools2_cal_group)
+        self.vlay3.addWidget(self.tools2_edrills_group)
+
+        self.vlay4 = QtWidgets.QVBoxLayout()
+        self.vlay4.addWidget(self.tools2_punch_group)
+        self.vlay4.addWidget(self.tools2_invert_group)
+
+        self.layout.addLayout(self.vlay)
+        self.layout.addLayout(self.vlay1)
+        self.layout.addLayout(self.vlay2)
+        self.layout.addLayout(self.vlay3)
+        self.layout.addLayout(self.vlay4)
+
+        self.layout.addStretch()

+ 233 - 0
appGUI/preferences/tools/Tools2PunchGerberPrefGroupUI.py

@@ -0,0 +1,233 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCCheckBox, RadioSet, FCDoubleSpinner
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2PunchGerberPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2PunchGerberPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Punch Gerber Options")))
+        self.decimals = decimals
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        self.param_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid_lay.addWidget(self.param_label, 0, 0, 1, 2)
+
+        self.padt_label = QtWidgets.QLabel("<b>%s:</b>" % _("Processed Pads Type"))
+        self.padt_label.setToolTip(
+            _("The type of pads shape to be processed.\n"
+              "If the PCB has many SMD pads with rectangular pads,\n"
+              "disable the Rectangular aperture.")
+        )
+
+        grid_lay.addWidget(self.padt_label, 2, 0, 1, 2)
+
+        # Circular Aperture Selection
+        self.circular_cb = FCCheckBox('%s' % _("Circular"))
+        self.circular_cb.setToolTip(
+            _("Process Circular Pads.")
+        )
+
+        grid_lay.addWidget(self.circular_cb, 3, 0, 1, 2)
+
+        # Oblong Aperture Selection
+        self.oblong_cb = FCCheckBox('%s' % _("Oblong"))
+        self.oblong_cb.setToolTip(
+            _("Process Oblong Pads.")
+        )
+
+        grid_lay.addWidget(self.oblong_cb, 4, 0, 1, 2)
+
+        # Square Aperture Selection
+        self.square_cb = FCCheckBox('%s' % _("Square"))
+        self.square_cb.setToolTip(
+            _("Process Square Pads.")
+        )
+
+        grid_lay.addWidget(self.square_cb, 5, 0, 1, 2)
+
+        # Rectangular Aperture Selection
+        self.rectangular_cb = FCCheckBox('%s' % _("Rectangular"))
+        self.rectangular_cb.setToolTip(
+            _("Process Rectangular Pads.")
+        )
+
+        grid_lay.addWidget(self.rectangular_cb, 6, 0, 1, 2)
+
+        # Others type of Apertures Selection
+        self.other_cb = FCCheckBox('%s' % _("Others"))
+        self.other_cb.setToolTip(
+            _("Process pads not in the categories above.")
+        )
+
+        grid_lay.addWidget(self.other_cb, 7, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 8, 0, 1, 2)
+
+        # ## Axis
+        self.hole_size_radio = RadioSet(
+            [
+                {'label': _("Excellon"), 'value': 'exc'},
+                {'label': _("Fixed Diameter"), 'value': 'fixed'},
+                {'label': _("Fixed Annular Ring"), 'value': 'ring'},
+                {'label': _("Proportional"), 'value': 'prop'}
+            ],
+            orientation='vertical',
+            stretch=False)
+        self.hole_size_label = QtWidgets.QLabel('<b>%s:</b>' % _("Method"))
+        self.hole_size_label.setToolTip(
+            _("The punch hole source can be:\n"
+              "- 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.")
+        )
+        grid_lay.addWidget(self.hole_size_label, 9, 0)
+        grid_lay.addWidget(self.hole_size_radio, 9, 1)
+
+        # grid_lay1.addWidget(QtWidgets.QLabel(''))
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid_lay.addWidget(separator_line, 10, 0, 1, 2)
+
+        # Annular Ring
+        self.fixed_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Diameter"))
+        grid_lay.addWidget(self.fixed_label, 11, 0, 1, 2)
+
+        # Diameter value
+        self.dia_entry = FCDoubleSpinner()
+        self.dia_entry.set_precision(self.decimals)
+        self.dia_entry.set_range(0.0000, 10000.0000)
+
+        self.dia_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.dia_label.setToolTip(
+            _("Fixed hole diameter.")
+        )
+
+        grid_lay.addWidget(self.dia_label, 12, 0)
+        grid_lay.addWidget(self.dia_entry, 12, 1)
+
+        # Annular Ring value
+        self.ring_label = QtWidgets.QLabel('<b>%s</b>' % _("Fixed Annular Ring"))
+        self.ring_label.setToolTip(
+            _("The size of annular ring.\n"
+              "The copper sliver between the hole exterior\n"
+              "and the margin of the copper pad.")
+        )
+        grid_lay.addWidget(self.ring_label, 13, 0, 1, 2)
+
+        # Circular Annular Ring Value
+        self.circular_ring_label = QtWidgets.QLabel('%s:' % _("Circular"))
+        self.circular_ring_label.setToolTip(
+            _("The size of annular ring for circular pads.")
+        )
+
+        self.circular_ring_entry = FCDoubleSpinner()
+        self.circular_ring_entry.set_precision(self.decimals)
+        self.circular_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.circular_ring_label, 14, 0)
+        grid_lay.addWidget(self.circular_ring_entry, 14, 1)
+
+        # Oblong Annular Ring Value
+        self.oblong_ring_label = QtWidgets.QLabel('%s:' % _("Oblong"))
+        self.oblong_ring_label.setToolTip(
+            _("The size of annular ring for oblong pads.")
+        )
+
+        self.oblong_ring_entry = FCDoubleSpinner()
+        self.oblong_ring_entry.set_precision(self.decimals)
+        self.oblong_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.oblong_ring_label, 15, 0)
+        grid_lay.addWidget(self.oblong_ring_entry, 15, 1)
+
+        # Square Annular Ring Value
+        self.square_ring_label = QtWidgets.QLabel('%s:' % _("Square"))
+        self.square_ring_label.setToolTip(
+            _("The size of annular ring for square pads.")
+        )
+
+        self.square_ring_entry = FCDoubleSpinner()
+        self.square_ring_entry.set_precision(self.decimals)
+        self.square_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.square_ring_label, 16, 0)
+        grid_lay.addWidget(self.square_ring_entry, 16, 1)
+
+        # Rectangular Annular Ring Value
+        self.rectangular_ring_label = QtWidgets.QLabel('%s:' % _("Rectangular"))
+        self.rectangular_ring_label.setToolTip(
+            _("The size of annular ring for rectangular pads.")
+        )
+
+        self.rectangular_ring_entry = FCDoubleSpinner()
+        self.rectangular_ring_entry.set_precision(self.decimals)
+        self.rectangular_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.rectangular_ring_label, 17, 0)
+        grid_lay.addWidget(self.rectangular_ring_entry, 17, 1)
+
+        # Others Annular Ring Value
+        self.other_ring_label = QtWidgets.QLabel('%s:' % _("Others"))
+        self.other_ring_label.setToolTip(
+            _("The size of annular ring for other pads.")
+        )
+
+        self.other_ring_entry = FCDoubleSpinner()
+        self.other_ring_entry.set_precision(self.decimals)
+        self.other_ring_entry.set_range(0.0000, 10000.0000)
+
+        grid_lay.addWidget(self.other_ring_label, 18, 0)
+        grid_lay.addWidget(self.other_ring_entry, 18, 1)
+
+        self.prop_label = QtWidgets.QLabel('<b>%s</b>' % _("Proportional Diameter"))
+        grid_lay.addWidget(self.prop_label, 19, 0, 1, 2)
+
+        # Factor value
+        self.factor_entry = FCDoubleSpinner(suffix='%')
+        self.factor_entry.set_precision(self.decimals)
+        self.factor_entry.set_range(0.0000, 100.0000)
+        self.factor_entry.setSingleStep(0.1)
+
+        self.factor_label = QtWidgets.QLabel('%s:' % _("Factor"))
+        self.factor_label.setToolTip(
+            _("Proportional Diameter.\n"
+              "The hole diameter will be a fraction of the pad size.")
+        )
+
+        grid_lay.addWidget(self.factor_label, 20, 0)
+        grid_lay.addWidget(self.factor_entry, 20, 1)
+
+        self.layout.addStretch()

+ 195 - 0
appGUI/preferences/tools/Tools2QRCodePrefGroupUI.py

@@ -0,0 +1,195 @@
+from PyQt5 import QtWidgets, QtCore, QtGui
+from PyQt5.QtCore import Qt, QSettings
+
+from appGUI.GUIElements import FCSpinner, RadioSet, FCTextArea, FCEntry, FCColorEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2QRCodePrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2QRCodePrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("QRCode Tool Options")))
+        self.decimals = decimals
+
+        # ## Parameters
+        self.qrlabel = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.qrlabel.setToolTip(
+            _("A tool to create a QRCode that can be inserted\n"
+              "into a selected Gerber file, or it can be exported as a file.")
+        )
+        self.layout.addWidget(self.qrlabel)
+
+        # ## Grid Layout
+        grid_lay = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid_lay)
+        grid_lay.setColumnStretch(0, 0)
+        grid_lay.setColumnStretch(1, 1)
+
+        # VERSION #
+        self.version_label = QtWidgets.QLabel('%s:' % _("Version"))
+        self.version_label.setToolTip(
+            _("QRCode version can have values from 1 (21x21 boxes)\n"
+              "to 40 (177x177 boxes).")
+        )
+        self.version_entry = FCSpinner()
+        self.version_entry.set_range(1, 40)
+        self.version_entry.setWrapping(True)
+
+        grid_lay.addWidget(self.version_label, 1, 0)
+        grid_lay.addWidget(self.version_entry, 1, 1)
+
+        # ERROR CORRECTION #
+        self.error_label = QtWidgets.QLabel('%s:' % _("Error correction"))
+        self.error_label.setToolTip(
+            _("Parameter that controls the error correction used for the QR Code.\n"
+              "L = maximum 7%% errors can be corrected\n"
+              "M = maximum 15%% errors can be corrected\n"
+              "Q = maximum 25%% errors can be corrected\n"
+              "H = maximum 30%% errors can be corrected.")
+        )
+        self.error_radio = RadioSet([{'label': 'L', 'value': 'L'},
+                                     {'label': 'M', 'value': 'M'},
+                                     {'label': 'Q', 'value': 'Q'},
+                                     {'label': 'H', 'value': 'H'}])
+        self.error_radio.setToolTip(
+            _("Parameter that controls the error correction used for the QR Code.\n"
+              "L = maximum 7%% errors can be corrected\n"
+              "M = maximum 15%% errors can be corrected\n"
+              "Q = maximum 25%% errors can be corrected\n"
+              "H = maximum 30%% errors can be corrected.")
+        )
+        grid_lay.addWidget(self.error_label, 2, 0)
+        grid_lay.addWidget(self.error_radio, 2, 1)
+
+        # BOX SIZE #
+        self.bsize_label = QtWidgets.QLabel('%s:' % _("Box Size"))
+        self.bsize_label.setToolTip(
+            _("Box size control the overall size of the QRcode\n"
+              "by adjusting the size of each box in the code.")
+        )
+        self.bsize_entry = FCSpinner()
+        self.bsize_entry.set_range(1, 9999)
+        self.bsize_entry.setWrapping(True)
+
+        grid_lay.addWidget(self.bsize_label, 3, 0)
+        grid_lay.addWidget(self.bsize_entry, 3, 1)
+
+        # BORDER SIZE #
+        self.border_size_label = QtWidgets.QLabel('%s:' % _("Border Size"))
+        self.border_size_label.setToolTip(
+            _("Size of the QRCode border. How many boxes thick is the border.\n"
+              "Default value is 4. The width of the clearance around the QRCode.")
+        )
+        self.border_size_entry = FCSpinner()
+        self.border_size_entry.set_range(1, 9999)
+        self.border_size_entry.setWrapping(True)
+
+        grid_lay.addWidget(self.border_size_label, 4, 0)
+        grid_lay.addWidget(self.border_size_entry, 4, 1)
+
+        # Text box
+        self.text_label = QtWidgets.QLabel('%s:' % _("QRCode Data"))
+        self.text_label.setToolTip(
+            _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
+        )
+        self.text_data = FCTextArea()
+        self.text_data.setPlaceholderText(
+            _("Add here the text to be included in the QRCode...")
+        )
+        grid_lay.addWidget(self.text_label, 5, 0)
+        grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
+
+        # POLARITY CHOICE #
+        self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
+        self.pol_label.setToolTip(
+            _("Choose the polarity of the QRCode.\n"
+              "It can be drawn in a negative way (squares are clear)\n"
+              "or in a positive way (squares are opaque).")
+        )
+        self.pol_radio = RadioSet([{'label': _('Negative'), 'value': 'neg'},
+                                   {'label': _('Positive'), 'value': 'pos'}])
+        self.pol_radio.setToolTip(
+            _("Choose the type of QRCode to be created.\n"
+              "If added on a Silkscreen Gerber file the QRCode may\n"
+              "be added as positive. If it is added to a Copper Gerber\n"
+              "file then perhaps the QRCode can be added as negative.")
+        )
+        grid_lay.addWidget(self.pol_label, 7, 0)
+        grid_lay.addWidget(self.pol_radio, 7, 1)
+
+        # BOUNDING BOX TYPE #
+        self.bb_label = QtWidgets.QLabel('%s:' % _("Bounding Box"))
+        self.bb_label.setToolTip(
+            _("The bounding box, meaning the empty space that surrounds\n"
+              "the QRCode geometry, can have a rounded or a square shape.")
+        )
+        self.bb_radio = RadioSet([{'label': _('Rounded'), 'value': 'r'},
+                                  {'label': _('Square'), 'value': 's'}])
+        self.bb_radio.setToolTip(
+            _("The bounding box, meaning the empty space that surrounds\n"
+              "the QRCode geometry, can have a rounded or a square shape.")
+        )
+        grid_lay.addWidget(self.bb_label, 8, 0)
+        grid_lay.addWidget(self.bb_radio, 8, 1)
+
+        # FILL COLOR #
+        self.fill_color_label = QtWidgets.QLabel('%s:' % _('Fill Color'))
+        self.fill_color_label.setToolTip(
+            _("Set the QRCode fill color (squares color).")
+        )
+        self.fill_color_entry = FCColorEntry()
+
+        grid_lay.addWidget(self.fill_color_label, 9, 0)
+        grid_lay.addWidget(self.fill_color_entry, 9, 1)
+
+        # BACK COLOR #
+        self.back_color_label = QtWidgets.QLabel('%s:' % _('Back Color'))
+        self.back_color_label.setToolTip(
+            _("Set the QRCode background color.")
+        )
+        self.back_color_entry = FCColorEntry()
+
+        grid_lay.addWidget(self.back_color_label, 10, 0)
+        grid_lay.addWidget(self.back_color_entry, 10, 1)
+
+        # Selection Limit
+        self.sel_limit_label = QtWidgets.QLabel('%s:' % _("Selection limit"))
+        self.sel_limit_label.setToolTip(
+            _("Set the number of selected geometry\n"
+              "items above which the utility geometry\n"
+              "becomes just a selection rectangle.\n"
+              "Increases the performance when moving a\n"
+              "large number of geometric elements.")
+        )
+        self.sel_limit_entry = FCSpinner()
+        self.sel_limit_entry.set_range(0, 9999)
+
+        grid_lay.addWidget(self.sel_limit_label, 11, 0)
+        grid_lay.addWidget(self.sel_limit_entry, 11, 1)
+        # self.layout.addStretch()
+
+        # QRCode Tool
+        self.fill_color_entry.editingFinished.connect(self.on_qrcode_fill_color_entry)
+        self.back_color_entry.editingFinished.connect(self.on_qrcode_back_color_entry)
+
+    def on_qrcode_fill_color_entry(self):
+        self.app.defaults['tools_qrcode_fill_color'] = self.fill_color_entry.get_value()
+
+    def on_qrcode_back_color_entry(self):
+        self.app.defaults['tools_qrcode_back_color'] = self.back_color_entry.get_value()

+ 242 - 0
appGUI/preferences/tools/Tools2RulesCheckPrefGroupUI.py

@@ -0,0 +1,242 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCCheckBox, FCDoubleSpinner
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2RulesCheckPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(Tools2RulesCheckPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Check Rules Tool Options")))
+        self.decimals = decimals
+
+        self.crlabel = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.crlabel.setToolTip(
+            _("A tool to check if Gerber files are within a set\n"
+              "of Manufacturing Rules.")
+        )
+        self.layout.addWidget(self.crlabel)
+
+        # Form Layout
+        self.form_layout_1 = QtWidgets.QFormLayout()
+        self.layout.addLayout(self.form_layout_1)
+
+        # Trace size
+        self.trace_size_cb = FCCheckBox('%s:' % _("Trace Size"))
+        self.trace_size_cb.setToolTip(
+            _("This checks if the minimum size for traces is met.")
+        )
+        self.form_layout_1.addRow(self.trace_size_cb)
+
+        # Trace size value
+        self.trace_size_entry = FCDoubleSpinner()
+        self.trace_size_entry.set_range(0.00001, 999.99999)
+        self.trace_size_entry.set_precision(self.decimals)
+        self.trace_size_entry.setSingleStep(0.1)
+
+        self.trace_size_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.trace_size_lbl.setToolTip(
+            _("Minimum acceptable trace size.")
+        )
+        self.form_layout_1.addRow(self.trace_size_lbl, self.trace_size_entry)
+
+        # Copper2copper clearance
+        self.clearance_copper2copper_cb = FCCheckBox('%s:' % _("Copper to Copper clearance"))
+        self.clearance_copper2copper_cb.setToolTip(
+            _("This checks if the minimum clearance between copper\n"
+              "features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2copper_cb)
+
+        # Copper2copper clearance value
+        self.clearance_copper2copper_entry = FCDoubleSpinner()
+        self.clearance_copper2copper_entry.set_range(0.00001, 999.99999)
+        self.clearance_copper2copper_entry.set_precision(self.decimals)
+        self.clearance_copper2copper_entry.setSingleStep(0.1)
+
+        self.clearance_copper2copper_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_copper2copper_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2copper_lbl, self.clearance_copper2copper_entry)
+
+        # Copper2outline clearance
+        self.clearance_copper2ol_cb = FCCheckBox('%s:' % _("Copper to Outline clearance"))
+        self.clearance_copper2ol_cb.setToolTip(
+            _("This checks if the minimum clearance between copper\n"
+              "features and the outline is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2ol_cb)
+
+        # Copper2outline clearance value
+        self.clearance_copper2ol_entry = FCDoubleSpinner()
+        self.clearance_copper2ol_entry.set_range(0.00001, 999.99999)
+        self.clearance_copper2ol_entry.set_precision(self.decimals)
+        self.clearance_copper2ol_entry.setSingleStep(0.1)
+
+        self.clearance_copper2ol_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_copper2ol_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_copper2ol_lbl, self.clearance_copper2ol_entry)
+
+        # Silkscreen2silkscreen clearance
+        self.clearance_silk2silk_cb = FCCheckBox('%s:' % _("Silk to Silk Clearance"))
+        self.clearance_silk2silk_cb.setToolTip(
+            _("This checks if the minimum clearance between silkscreen\n"
+              "features and silkscreen features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2silk_cb)
+
+        # Copper2silkscreen clearance value
+        self.clearance_silk2silk_entry = FCDoubleSpinner()
+        self.clearance_silk2silk_entry.set_range(0.00001, 999.99999)
+        self.clearance_silk2silk_entry.set_precision(self.decimals)
+        self.clearance_silk2silk_entry.setSingleStep(0.1)
+
+        self.clearance_silk2silk_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_silk2silk_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2silk_lbl, self.clearance_silk2silk_entry)
+
+        # Silkscreen2soldermask clearance
+        self.clearance_silk2sm_cb = FCCheckBox('%s:' % _("Silk to Solder Mask Clearance"))
+        self.clearance_silk2sm_cb.setToolTip(
+            _("This checks if the minimum clearance between silkscreen\n"
+              "features and soldermask features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2sm_cb)
+
+        # Silkscreen2soldermask clearance value
+        self.clearance_silk2sm_entry = FCDoubleSpinner()
+        self.clearance_silk2sm_entry.set_range(0.00001, 999.99999)
+        self.clearance_silk2sm_entry.set_precision(self.decimals)
+        self.clearance_silk2sm_entry.setSingleStep(0.1)
+
+        self.clearance_silk2sm_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_silk2sm_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2sm_lbl, self.clearance_silk2sm_entry)
+
+        # Silk2outline clearance
+        self.clearance_silk2ol_cb = FCCheckBox('%s:' % _("Silk to Outline Clearance"))
+        self.clearance_silk2ol_cb.setToolTip(
+            _("This checks if the minimum clearance between silk\n"
+              "features and the outline is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2ol_cb)
+
+        # Silk2outline clearance value
+        self.clearance_silk2ol_entry = FCDoubleSpinner()
+        self.clearance_silk2ol_entry.set_range(0.00001, 999.99999)
+        self.clearance_silk2ol_entry.set_precision(self.decimals)
+        self.clearance_silk2ol_entry.setSingleStep(0.1)
+
+        self.clearance_silk2ol_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_silk2ol_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_silk2ol_lbl, self.clearance_silk2ol_entry)
+
+        # Soldermask2soldermask clearance
+        self.clearance_sm2sm_cb = FCCheckBox('%s:' % _("Minimum Solder Mask Sliver"))
+        self.clearance_sm2sm_cb.setToolTip(
+            _("This checks if the minimum clearance between soldermask\n"
+              "features and soldermask features is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_sm2sm_cb)
+
+        # Soldermask2soldermask clearance value
+        self.clearance_sm2sm_entry = FCDoubleSpinner()
+        self.clearance_sm2sm_entry.set_range(0.00001, 999.99999)
+        self.clearance_sm2sm_entry.set_precision(self.decimals)
+        self.clearance_sm2sm_entry.setSingleStep(0.1)
+
+        self.clearance_sm2sm_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_sm2sm_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.clearance_sm2sm_lbl, self.clearance_sm2sm_entry)
+
+        # Ring integrity check
+        self.ring_integrity_cb = FCCheckBox('%s:' % _("Minimum Annular Ring"))
+        self.ring_integrity_cb.setToolTip(
+            _("This checks if the minimum copper ring left by drilling\n"
+              "a hole into a pad is met.")
+        )
+        self.form_layout_1.addRow(self.ring_integrity_cb)
+
+        # Ring integrity value
+        self.ring_integrity_entry = FCDoubleSpinner()
+        self.ring_integrity_entry.set_range(0.00001, 999.99999)
+        self.ring_integrity_entry.set_precision(self.decimals)
+        self.ring_integrity_entry.setSingleStep(0.1)
+
+        self.ring_integrity_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.ring_integrity_lbl.setToolTip(
+            _("Minimum acceptable ring value.")
+        )
+        self.form_layout_1.addRow(self.ring_integrity_lbl, self.ring_integrity_entry)
+
+        self.form_layout_1.addRow(QtWidgets.QLabel(""))
+
+        # Hole2Hole clearance
+        self.clearance_d2d_cb = FCCheckBox('%s:' % _("Hole to Hole Clearance"))
+        self.clearance_d2d_cb.setToolTip(
+            _("This checks if the minimum clearance between a drill hole\n"
+              "and another drill hole is met.")
+        )
+        self.form_layout_1.addRow(self.clearance_d2d_cb)
+
+        # Hole2Hole clearance value
+        self.clearance_d2d_entry = FCDoubleSpinner()
+        self.clearance_d2d_entry.set_range(0.00001, 999.99999)
+        self.clearance_d2d_entry.set_precision(self.decimals)
+        self.clearance_d2d_entry.setSingleStep(0.1)
+
+        self.clearance_d2d_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.clearance_d2d_lbl.setToolTip(
+            _("Minimum acceptable drill size.")
+        )
+        self.form_layout_1.addRow(self.clearance_d2d_lbl, self.clearance_d2d_entry)
+
+        # Drill holes size check
+        self.drill_size_cb = FCCheckBox('%s:' % _("Hole Size"))
+        self.drill_size_cb.setToolTip(
+            _("This checks if the drill holes\n"
+              "sizes are above the threshold.")
+        )
+        self.form_layout_1.addRow(self.drill_size_cb)
+
+        # Drile holes value
+        self.drill_size_entry = FCDoubleSpinner()
+        self.drill_size_entry.set_range(0.00001, 999.99999)
+        self.drill_size_entry.set_precision(self.decimals)
+        self.drill_size_entry.setSingleStep(0.1)
+
+        self.drill_size_lbl = QtWidgets.QLabel('%s:' % _("Min value"))
+        self.drill_size_lbl.setToolTip(
+            _("Minimum acceptable clearance value.")
+        )
+        self.form_layout_1.addRow(self.drill_size_lbl, self.drill_size_entry)
+
+        self.layout.addStretch()

+ 103 - 0
appGUI/preferences/tools/Tools2sidedPrefGroupUI.py

@@ -0,0 +1,103 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, RadioSet
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class Tools2sidedPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "2sided Tool Options", parent=parent)
+        super(Tools2sidedPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("2-Sided Tool Options")))
+        self.decimals = decimals
+
+        # ## Board cuttout
+        self.dblsided_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.dblsided_label.setToolTip(
+            _("A tool to help in creating a double sided\n"
+              "PCB using alignment holes.")
+        )
+        self.layout.addWidget(self.dblsided_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # ## Drill diameter for alignment holes
+        self.drill_dia_entry = FCDoubleSpinner()
+        self.drill_dia_entry.set_range(0.000001, 10000.0000)
+        self.drill_dia_entry.set_precision(self.decimals)
+        self.drill_dia_entry.setSingleStep(0.1)
+
+        self.dd_label = QtWidgets.QLabel('%s:' % _("Drill Dia"))
+        self.dd_label.setToolTip(
+            _("Diameter of the drill for the "
+              "alignment holes.")
+        )
+        grid0.addWidget(self.dd_label, 0, 0)
+        grid0.addWidget(self.drill_dia_entry, 0, 1)
+
+        # ## Alignment Axis
+        self.align_ax_label = QtWidgets.QLabel('%s:' % _("Align Axis"))
+        self.align_ax_label.setToolTip(
+            _("Mirror vertically (X) or horizontally (Y).")
+        )
+        self.align_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
+                                          {'label': 'Y', 'value': 'Y'}])
+
+        grid0.addWidget(self.align_ax_label, 1, 0)
+        grid0.addWidget(self.align_axis_radio, 1, 1)
+
+        # ## Axis
+        self.mirror_axis_radio = RadioSet([{'label': 'X', 'value': 'X'},
+                                           {'label': 'Y', 'value': 'Y'}])
+        self.mirax_label = QtWidgets.QLabel('%s:' % _("Mirror Axis"))
+        self.mirax_label.setToolTip(
+            _("Mirror vertically (X) or horizontally (Y).")
+        )
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 2, 0, 1, 2)
+
+        grid0.addWidget(self.mirax_label, 3, 0)
+        grid0.addWidget(self.mirror_axis_radio, 3, 1)
+
+        # ## Axis Location
+        self.axis_location_radio = RadioSet(
+            [
+                {'label': _('Point'), 'value': 'point'},
+                {'label': _('Box'), 'value': 'box'},
+                {'label': _('Hole Snap'), 'value': 'hole'},
+            ]
+        )
+        self.axloc_label = QtWidgets.QLabel('%s:' % _("Axis Ref"))
+        self.axloc_label.setToolTip(
+            _("The coordinates used as reference for the mirror operation.\n"
+              "Can be:\n"
+              "- Point -> a set of coordinates (x,y) around which the object is mirrored\n"
+              "- Box -> a set of coordinates (x, y) obtained from the center of the\n"
+              "bounding box of another object selected below\n"
+              "- Hole Snap-> a point defined by the center of a drill hone in a Excellon object")
+        )
+
+        grid0.addWidget(self.axloc_label, 4, 0)
+        grid0.addWidget(self.axis_location_radio, 4, 1)
+
+        self.layout.addStretch()

+ 153 - 0
appGUI/preferences/tools/ToolsCalculatorsPrefGroupUI.py

@@ -0,0 +1,153 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCLabel
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsCalculatorsPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Calculators Tool Options", parent=parent)
+        super(ToolsCalculatorsPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Calculators Tool Options")))
+        self.decimals = decimals
+
+        # ## V-shape Calculator Tool
+        self.vshape_tool_label = FCLabel("<b>%s:</b>" % _("V-Shape Tool Calculator"))
+        self.vshape_tool_label.setToolTip(
+            _("Calculate the tool diameter for a given V-shape tool,\n"
+              "having the tip diameter, tip angle and\n"
+              "depth-of-cut as parameters.")
+        )
+        self.layout.addWidget(self.vshape_tool_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
+        # ## Tip Diameter
+        self.tip_dia_entry = FCDoubleSpinner()
+        self.tip_dia_entry.set_range(0.000001, 10000.0000)
+        self.tip_dia_entry.set_precision(self.decimals)
+        self.tip_dia_entry.setSingleStep(0.1)
+
+        self.tip_dia_label = FCLabel('%s:' % _("Tip Diameter"))
+        self.tip_dia_label.setToolTip(
+            _("This is the tool tip diameter.\n"
+              "It is specified by manufacturer.")
+        )
+        grid0.addWidget(self.tip_dia_label, 0, 0)
+        grid0.addWidget(self.tip_dia_entry, 0, 1)
+
+        # ## Tip angle
+        self.tip_angle_entry = FCDoubleSpinner()
+        self.tip_angle_entry.set_range(0.0, 180.0)
+        self.tip_angle_entry.set_precision(self.decimals)
+        self.tip_angle_entry.setSingleStep(5)
+
+        self.tip_angle_label = FCLabel('%s:' % _("Tip Angle"))
+        self.tip_angle_label.setToolTip(
+            _("This is the angle on the tip of the tool.\n"
+              "It is specified by manufacturer.")
+        )
+        grid0.addWidget(self.tip_angle_label, 2, 0)
+        grid0.addWidget(self.tip_angle_entry, 2, 1)
+
+        # ## Depth-of-cut Cut Z
+        self.cut_z_entry = FCDoubleSpinner()
+        self.cut_z_entry.set_range(-10000.0000, 0.0000)
+        self.cut_z_entry.set_precision(self.decimals)
+        self.cut_z_entry.setSingleStep(0.01)
+
+        self.cut_z_label = FCLabel('%s:' % _("Cut Z"))
+        self.cut_z_label.setToolTip(
+            _("This is depth to cut into material.\n"
+              "In the CNCJob object it is the CutZ parameter.")
+        )
+        grid0.addWidget(self.cut_z_label, 4, 0)
+        grid0.addWidget(self.cut_z_entry, 4, 1)
+
+        # ## Electroplating Calculator Tool
+        self.plate_title_label = FCLabel("<b>%s:</b>" % _("ElectroPlating Calculator"))
+        self.plate_title_label.setToolTip(
+            _("This calculator is useful for those who plate the via/pad/drill holes,\n"
+              "using a method like graphite ink or calcium hypophosphite ink or palladium chloride.")
+        )
+        grid0.addWidget(self.plate_title_label, 3, 0, 1, 2)
+
+        # ## PCB Length
+        self.pcblength_entry = FCDoubleSpinner()
+        self.pcblength_entry.set_range(0.000001, 10000.0000)
+        self.pcblength_entry.set_precision(self.decimals)
+        self.pcblength_entry.setSingleStep(0.1)
+
+        self.pcblengthlabel = FCLabel('%s:' % _("Board Length"))
+
+        self.pcblengthlabel.setToolTip(_('This is the board length. In centimeters.'))
+        grid0.addWidget(self.pcblengthlabel, 6, 0)
+        grid0.addWidget(self.pcblength_entry, 6, 1)
+
+        # ## PCB Width
+        self.pcbwidth_entry = FCDoubleSpinner()
+        self.pcbwidth_entry.set_range(0.000001, 10000.0000)
+        self.pcbwidth_entry.set_precision(self.decimals)
+        self.pcbwidth_entry.setSingleStep(0.1)
+
+        self.pcbwidthlabel = FCLabel('%s:' % _("Board Width"))
+
+        self.pcbwidthlabel.setToolTip(_('This is the board width.In centimeters.'))
+        grid0.addWidget(self.pcbwidthlabel, 8, 0)
+        grid0.addWidget(self.pcbwidth_entry, 8, 1)
+        
+        # AREA
+        self.area_label = FCLabel('%s:' % _("Area"))
+        self.area_label.setToolTip(_('This is the board area.'))
+        self.area_entry = FCDoubleSpinner()
+        self.area_entry.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)
+        self.area_entry.set_precision(self.decimals)
+        self.area_entry.set_range(0.0, 10000.0000)
+        
+        grid0.addWidget(self.area_label, 10, 0)
+        grid0.addWidget(self.area_entry, 10, 1)
+        
+        # ## Current Density
+        self.cdensity_label = FCLabel('%s:' % _("Current Density"))
+        self.cdensity_entry = FCDoubleSpinner()
+        self.cdensity_entry.set_range(0.000001, 10000.0000)
+        self.cdensity_entry.set_precision(self.decimals)
+        self.cdensity_entry.setSingleStep(0.1)
+
+        self.cdensity_label.setToolTip(_("Current density to pass through the board. \n"
+                                         "In Amps per Square Feet ASF."))
+        grid0.addWidget(self.cdensity_label, 12, 0)
+        grid0.addWidget(self.cdensity_entry, 12, 1)
+
+        # ## PCB Copper Growth
+        self.growth_label = FCLabel('%s:' % _("Copper Growth"))
+        self.growth_entry = FCDoubleSpinner()
+        self.growth_entry.set_range(0.000001, 10000.0000)
+        self.growth_entry.set_precision(self.decimals)
+        self.growth_entry.setSingleStep(0.01)
+
+        self.growth_label.setToolTip(_("How thick the copper growth is intended to be.\n"
+                                       "In microns."))
+        grid0.addWidget(self.growth_label, 14, 0)
+        grid0.addWidget(self.growth_entry, 14, 1)
+
+        self.layout.addStretch()

+ 108 - 0
appGUI/preferences/tools/ToolsCornersPrefGroupUI.py

@@ -0,0 +1,108 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCLabel, RadioSet
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsCornersPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Calculators Tool Options", parent=parent)
+        super(ToolsCornersPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Corner Markers Options")))
+        self.decimals = decimals
+
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
+        self.param_label = FCLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid0.addWidget(self.param_label, 0, 0, 1, 2)
+
+        # Type of Marker
+        self.type_label = FCLabel('%s:' % _("Type"))
+        self.type_label.setToolTip(
+            _("Shape of the marker.")
+        )
+
+        self.type_radio = RadioSet([
+            {"label": _("Semi-Cross"), "value": "s"},
+            {"label": _("Cross"), "value": "c"},
+        ])
+
+        grid0.addWidget(self.type_label, 2, 0)
+        grid0.addWidget(self.type_radio, 2, 1)
+        
+        # Thickness #
+        self.thick_label = FCLabel('%s:' % _("Thickness"))
+        self.thick_label.setToolTip(
+            _("The thickness of the line that makes the corner marker.")
+        )
+        self.thick_entry = FCDoubleSpinner()
+        self.thick_entry.set_range(0.0000, 9.9999)
+        self.thick_entry.set_precision(self.decimals)
+        self.thick_entry.setWrapping(True)
+        self.thick_entry.setSingleStep(10 ** -self.decimals)
+
+        grid0.addWidget(self.thick_label, 4, 0)
+        grid0.addWidget(self.thick_entry, 4, 1)
+
+        # Margin #
+        self.margin_label = FCLabel('%s:' % _("Margin"))
+        self.margin_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.margin_entry = FCDoubleSpinner()
+        self.margin_entry.set_range(-10000.0000, 10000.0000)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.margin_label, 6, 0)
+        grid0.addWidget(self.margin_entry, 6, 1)
+
+        # Length #
+        self.l_label = FCLabel('%s:' % _("Length"))
+        self.l_label.setToolTip(
+            _("The length of the line that makes the corner marker.")
+        )
+        self.l_entry = FCDoubleSpinner()
+        self.l_entry.set_range(-10000.0000, 10000.0000)
+        self.l_entry.set_precision(self.decimals)
+        self.l_entry.setSingleStep(10 ** -self.decimals)
+
+        grid0.addWidget(self.l_label, 8, 0)
+        grid0.addWidget(self.l_entry, 8, 1)
+
+        # Drill Tool Diameter
+        self.drill_dia_label = FCLabel('%s:' % _("Drill Dia"))
+        self.drill_dia_label.setToolTip(
+            '%s.' % _("Drill Diameter")
+        )
+        self.drill_dia_entry = FCDoubleSpinner()
+        self.drill_dia_entry.set_range(0.0000, 100.0000)
+        self.drill_dia_entry.set_precision(self.decimals)
+        self.drill_dia_entry.setWrapping(True)
+
+        grid0.addWidget(self.drill_dia_label, 10, 0)
+        grid0.addWidget(self.drill_dia_entry, 10, 1)
+
+        self.layout.addStretch()

+ 245 - 0
appGUI/preferences/tools/ToolsCutoutPrefGroupUI.py

@@ -0,0 +1,245 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox, FCLabel
+from appGUI.preferences import machinist_setting
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsCutoutPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Cutout Tool Options", parent=parent)
+        super(ToolsCutoutPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Cutout Tool Options")))
+        self.decimals = decimals
+
+        # ## Board cutout
+        self.board_cutout_label = FCLabel("<b>%s:</b>" % _("Parameters"))
+        self.board_cutout_label.setToolTip(
+            _("Create toolpaths to cut around\n"
+              "the PCB and separate it from\n"
+              "the original board.")
+        )
+        self.layout.addWidget(self.board_cutout_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        tdclabel = FCLabel('%s:' % _('Tool Diameter'))
+        tdclabel.setToolTip(
+            _("Diameter of the tool used to cutout\n"
+              "the PCB shape out of the surrounding material.")
+        )
+
+        self.cutout_tooldia_entry = FCDoubleSpinner()
+        self.cutout_tooldia_entry.set_range(0.000001, 10000.0000)
+        self.cutout_tooldia_entry.set_precision(self.decimals)
+        self.cutout_tooldia_entry.setSingleStep(0.1)
+
+        grid0.addWidget(tdclabel, 0, 0)
+        grid0.addWidget(self.cutout_tooldia_entry, 0, 1)
+
+        # Cut Z
+        cutzlabel = FCLabel('%s:' % _('Cut Z'))
+        cutzlabel.setToolTip(
+            _(
+                "Cutting depth (negative)\n"
+                "below the copper surface."
+            )
+        )
+        self.cutz_entry = FCDoubleSpinner()
+        self.cutz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.cutz_entry.setRange(-10000.0000, 0.0000)
+        else:
+            self.cutz_entry.setRange(-10000.0000, 10000.0000)
+
+        self.cutz_entry.setSingleStep(0.1)
+
+        grid0.addWidget(cutzlabel, 1, 0)
+        grid0.addWidget(self.cutz_entry, 1, 1)
+
+        # Multi-pass
+        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
+        self.mpass_cb.setToolTip(
+            _(
+                "Use multiple passes to limit\n"
+                "the cut depth in each pass. Will\n"
+                "cut multiple times until Cut Z is\n"
+                "reached."
+            )
+        )
+
+        self.maxdepth_entry = FCDoubleSpinner()
+        self.maxdepth_entry.set_precision(self.decimals)
+        self.maxdepth_entry.setRange(0, 10000.0000)
+        self.maxdepth_entry.setSingleStep(0.1)
+
+        self.maxdepth_entry.setToolTip(_("Depth of each pass (positive)."))
+
+        grid0.addWidget(self.mpass_cb, 2, 0)
+        grid0.addWidget(self.maxdepth_entry, 2, 1)
+
+        # Object kind
+        kindlabel = FCLabel('%s:' % _('Kind'))
+        kindlabel.setToolTip(
+            _("Choice of what kind the object we want to cutout is.\n"
+              "- Single: contain a single PCB Gerber outline object.\n"
+              "- Panel: a panel PCB Gerber object, which is made\n"
+              "out of many individual PCB outlines.")
+        )
+
+        self.obj_kind_combo = RadioSet([
+            {"label": _("Single"), "value": "single"},
+            {"label": _("Panel"), "value": "panel"},
+        ])
+        grid0.addWidget(kindlabel, 3, 0)
+        grid0.addWidget(self.obj_kind_combo, 3, 1)
+
+        marginlabel = FCLabel('%s:' % _('Margin'))
+        marginlabel.setToolTip(
+            _("Margin over bounds. A positive value here\n"
+              "will make the cutout of the PCB further from\n"
+              "the actual PCB border")
+        )
+
+        self.cutout_margin_entry = FCDoubleSpinner()
+        self.cutout_margin_entry.set_range(-10000.0000, 10000.0000)
+        self.cutout_margin_entry.set_precision(self.decimals)
+        self.cutout_margin_entry.setSingleStep(0.1)
+
+        grid0.addWidget(marginlabel, 4, 0)
+        grid0.addWidget(self.cutout_margin_entry, 4, 1)
+        
+        # Gap Size
+        gaplabel = FCLabel('%s:' % _('Gap size'))
+        gaplabel.setToolTip(
+            _("The size of the bridge gaps in the cutout\n"
+              "used to keep the board connected to\n"
+              "the surrounding material (the one \n"
+              "from which the PCB is cutout).")
+        )
+
+        self.cutout_gap_entry = FCDoubleSpinner()
+        self.cutout_gap_entry.set_range(0.000001, 10000.0000)
+        self.cutout_gap_entry.set_precision(self.decimals)
+        self.cutout_gap_entry.setSingleStep(0.1)
+
+        grid0.addWidget(gaplabel, 5, 0)
+        grid0.addWidget(self.cutout_gap_entry, 5, 1)
+        
+        # Gap Type
+        self.gaptype_label = FCLabel('%s:' % _("Gap type"))
+        self.gaptype_label.setToolTip(
+            _("The type of gap:\n"
+              "- Bridge -> the cutout will be interrupted by bridges\n"
+              "- Thin -> same as 'bridge' but it will be thinner by partially milling the gap\n"
+              "- M-Bites -> 'Mouse Bites' - same as 'bridge' but covered with drill holes")
+        )
+
+        self.gaptype_radio = RadioSet(
+            [
+                {'label': _('Bridge'),      'value': 'b'},
+                {'label': _('Thin'),        'value': 'bt'},
+                {'label': "M-Bites",        'value': 'mb'}
+            ],
+            stretch=True
+        )
+
+        grid0.addWidget(self.gaptype_label, 7, 0)
+        grid0.addWidget(self.gaptype_radio, 7, 1)
+
+        # Thin gaps Depth
+        self.thin_depth_label = FCLabel('%s:' % _("Depth"))
+        self.thin_depth_label.setToolTip(
+            _("The depth until the milling is done\n"
+              "in order to thin the gaps.")
+        )
+        self.thin_depth_entry = FCDoubleSpinner()
+        self.thin_depth_entry.set_precision(self.decimals)
+        if machinist_setting == 0:
+            self.thin_depth_entry.setRange(-10000.0000, -0.00001)
+        else:
+            self.thin_depth_entry.setRange(-10000.0000, 10000.0000)
+        self.thin_depth_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.thin_depth_label, 9, 0)
+        grid0.addWidget(self.thin_depth_entry, 9, 1)
+
+        # Mouse Bites Tool Diameter
+        self.mb_dia_label = FCLabel('%s:' % _("Tool Diameter"))
+        self.mb_dia_label.setToolTip(
+            _("The drill hole diameter when doing mouse bites.")
+        )
+        self.mb_dia_entry = FCDoubleSpinner()
+        self.mb_dia_entry.set_precision(self.decimals)
+        self.mb_dia_entry.setRange(0, 100.0000)
+
+        grid0.addWidget(self.mb_dia_label, 11, 0)
+        grid0.addWidget(self.mb_dia_entry, 11, 1)
+
+        # Mouse Bites Holes Spacing
+        self.mb_spacing_label = FCLabel('%s:' % _("Spacing"))
+        self.mb_spacing_label.setToolTip(
+            _("The spacing between drill holes when doing mouse bites.")
+        )
+        self.mb_spacing_entry = FCDoubleSpinner()
+        self.mb_spacing_entry.set_precision(self.decimals)
+        self.mb_spacing_entry.setRange(0, 100.0000)
+
+        grid0.addWidget(self.mb_spacing_label, 13, 0)
+        grid0.addWidget(self.mb_spacing_entry, 13, 1)
+        
+        gaps_label = FCLabel('%s:' % _('Gaps'))
+        gaps_label.setToolTip(
+            _("Number of gaps used for the cutout.\n"
+              "There can be maximum 8 bridges/gaps.\n"
+              "The choices are:\n"
+              "- None  - no gaps\n"
+              "- lr    - left + right\n"
+              "- tb    - top + bottom\n"
+              "- 4     - left + right +top + bottom\n"
+              "- 2lr   - 2*left + 2*right\n"
+              "- 2tb  - 2*top + 2*bottom\n"
+              "- 8     - 2*left + 2*right +2*top + 2*bottom")
+        )
+
+        self.gaps_combo = FCComboBox()
+        grid0.addWidget(gaps_label, 15, 0)
+        grid0.addWidget(self.gaps_combo, 15, 1)
+
+        gaps_items = ['None', 'LR', 'TB', '4', '2LR', '2TB', '8']
+        for it in gaps_items:
+            self.gaps_combo.addItem(it)
+            # self.gaps_combo.setStyleSheet('background-color: rgb(255,255,255)')
+
+        # Surrounding convex box shape
+        self.convex_box = FCCheckBox('%s' % _("Convex Shape"))
+        self.convex_box.setToolTip(
+            _("Create a convex shape surrounding the entire PCB.\n"
+              "Used only if the source object type is Gerber.")
+        )
+        grid0.addWidget(self.convex_box, 17, 0, 1, 2)
+
+        self.big_cursor_cb = FCCheckBox('%s' % _("Big cursor"))
+        self.big_cursor_cb.setToolTip(
+            _("Use a big cursor when adding manual gaps."))
+        grid0.addWidget(self.big_cursor_cb, 19, 0, 1, 2)
+
+        self.layout.addStretch()

+ 453 - 0
appGUI/preferences/tools/ToolsDrillPrefGroupUI.py

@@ -0,0 +1,453 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings, Qt
+
+from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, NumericalEvalTupleEntry, \
+    OptionalInputSection, NumericalEvalEntry, FCLabel
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsDrillPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        super(ToolsDrillPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Drilling Tool Options")))
+        self.decimals = decimals
+
+        # ## Clear non-copper regions
+        self.drill_label = FCLabel("<b>%s:</b>" % _("Parameters"))
+        self.drill_label.setToolTip(
+            _("Create CNCJob with toolpaths for drilling or milling holes.")
+        )
+        self.layout.addWidget(self.drill_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Tool order Radio Button
+        self.order_label = FCLabel('%s:' % _('Tool order'))
+        self.order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
+                                      "'No' --> means that the used order is the one in the tool table\n"
+                                      "'Forward' --> means that the tools will be ordered from small to big\n"
+                                      "'Reverse' --> means that the tools will ordered from big to small\n\n"
+                                      "WARNING: using rest machining will automatically set the order\n"
+                                      "in reverse and disable this control."))
+
+        self.order_radio = RadioSet([{'label': _('No'), 'value': 'no'},
+                                     {'label': _('Forward'), 'value': 'fwd'},
+                                     {'label': _('Reverse'), 'value': 'rev'}])
+
+        grid0.addWidget(self.order_label, 1, 0)
+        grid0.addWidget(self.order_radio, 1, 1, 1, 2)
+
+        # Cut Z
+        cutzlabel = FCLabel('%s:' % _('Cut Z'))
+        cutzlabel.setToolTip(
+            _("Drill depth (negative)\n"
+              "below the copper surface.")
+        )
+
+        self.cutz_entry = FCDoubleSpinner()
+
+        if machinist_setting == 0:
+            self.cutz_entry.set_range(-10000.0000, 0.0000)
+        else:
+            self.cutz_entry.set_range(-10000.0000, 10000.0000)
+
+        self.cutz_entry.setSingleStep(0.1)
+        self.cutz_entry.set_precision(self.decimals)
+
+        grid0.addWidget(cutzlabel, 3, 0)
+        grid0.addWidget(self.cutz_entry, 3, 1, 1, 2)
+
+        # Multi-Depth
+        self.mpass_cb = FCCheckBox('%s:' % _("Multi-Depth"))
+        self.mpass_cb.setToolTip(
+            _(
+                "Use multiple passes to limit\n"
+                "the cut depth in each pass. Will\n"
+                "cut multiple times until Cut Z is\n"
+                "reached."
+            )
+        )
+
+        self.maxdepth_entry = FCDoubleSpinner()
+        self.maxdepth_entry.set_precision(self.decimals)
+        self.maxdepth_entry.set_range(0, 10000.0000)
+        self.maxdepth_entry.setSingleStep(0.1)
+
+        self.maxdepth_entry.setToolTip(_("Depth of each pass (positive)."))
+
+        grid0.addWidget(self.mpass_cb, 4, 0)
+        grid0.addWidget(self.maxdepth_entry, 4, 1, 1, 2)
+
+        # Travel Z
+        travelzlabel = FCLabel('%s:' % _('Travel Z'))
+        travelzlabel.setToolTip(
+            _("Tool height when travelling\n"
+              "across the XY plane.")
+        )
+
+        self.travelz_entry = FCDoubleSpinner()
+        self.travelz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.travelz_entry.set_range(0.0001, 10000.0000)
+        else:
+            self.travelz_entry.set_range(-10000.0000, 10000.0000)
+
+        grid0.addWidget(travelzlabel, 5, 0)
+        grid0.addWidget(self.travelz_entry, 5, 1, 1, 2)
+
+        # Tool change:
+        self.toolchange_cb = FCCheckBox('%s' % _("Tool change"))
+        self.toolchange_cb.setToolTip(
+            _("Include tool-change sequence\n"
+              "in G-Code (Pause for tool change).")
+        )
+        grid0.addWidget(self.toolchange_cb, 6, 0, 1, 3)
+
+        # Tool Change Z
+        toolchangezlabel = FCLabel('%s:' % _('Toolchange Z'))
+        toolchangezlabel.setToolTip(
+            _("Z-axis position (height) for\n"
+              "tool change.")
+        )
+
+        self.toolchangez_entry = FCDoubleSpinner()
+        self.toolchangez_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.toolchangez_entry.set_range(0.0001, 10000.0000)
+        else:
+            self.toolchangez_entry.set_range(-10000.0000, 10000.0000)
+
+        grid0.addWidget(toolchangezlabel, 7, 0)
+        grid0.addWidget(self.toolchangez_entry, 7, 1, 1, 2)
+
+        # End Move Z
+        endz_label = FCLabel('%s:' % _('End move Z'))
+        endz_label.setToolTip(
+            _("Height of the tool after\n"
+              "the last move at the end of the job.")
+        )
+        self.endz_entry = FCDoubleSpinner()
+        self.endz_entry.set_precision(self.decimals)
+
+        if machinist_setting == 0:
+            self.endz_entry.set_range(0.0000, 10000.0000)
+        else:
+            self.endz_entry.set_range(-10000.0000, 10000.0000)
+
+        grid0.addWidget(endz_label, 8, 0)
+        grid0.addWidget(self.endz_entry, 8, 1, 1, 2)
+
+        # End Move X,Y
+        endmove_xy_label = FCLabel('%s:' % _('End move X,Y'))
+        endmove_xy_label.setToolTip(
+            _("End move X,Y position. In format (x,y).\n"
+              "If no value is entered then there is no move\n"
+              "on X,Y plane at the end of the job.")
+        )
+        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
+        grid0.addWidget(endmove_xy_label, 9, 0)
+        grid0.addWidget(self.endxy_entry, 9, 1, 1, 2)
+
+        # Feedrate Z
+        frlabel = FCLabel('%s:' % _('Feedrate Z'))
+        frlabel.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "So called 'Plunge' feedrate.\n"
+              "This is for linear move G01.")
+        )
+        self.feedrate_z_entry = FCDoubleSpinner()
+        self.feedrate_z_entry.set_precision(self.decimals)
+        self.feedrate_z_entry.set_range(0, 910000.0000)
+
+        grid0.addWidget(frlabel, 10, 0)
+        grid0.addWidget(self.feedrate_z_entry, 10, 1, 1, 2)
+
+        # Spindle speed
+        spdlabel = FCLabel('%s:' % _('Spindle Speed'))
+        spdlabel.setToolTip(
+            _("Speed of the spindle\n"
+              "in RPM (optional)")
+        )
+
+        self.spindlespeed_entry = FCSpinner()
+        self.spindlespeed_entry.set_range(0, 1000000)
+        self.spindlespeed_entry.set_step(100)
+
+        grid0.addWidget(spdlabel, 11, 0)
+        grid0.addWidget(self.spindlespeed_entry, 11, 1, 1, 2)
+
+        # Dwell
+        self.dwell_cb = FCCheckBox('%s' % _('Enable Dwell'))
+        self.dwell_cb.setToolTip(
+            _("Pause to allow the spindle to reach its\n"
+              "speed before cutting.")
+        )
+
+        grid0.addWidget(self.dwell_cb, 12, 0, 1, 3)
+
+        # Dwell Time
+        dwelltime = FCLabel('%s:' % _('Duration'))
+        dwelltime.setToolTip(_("Number of time units for spindle to dwell."))
+        self.dwelltime_entry = FCDoubleSpinner()
+        self.dwelltime_entry.set_precision(self.decimals)
+        self.dwelltime_entry.set_range(0, 910000.0000)
+
+        grid0.addWidget(dwelltime, 13, 0)
+        grid0.addWidget(self.dwelltime_entry, 13, 1, 1, 2)
+
+        self.ois_dwell_exc = OptionalInputSection(self.dwell_cb, [self.dwelltime_entry])
+
+        # preprocessor selection
+        pp_excellon_label = FCLabel('%s:' % _("Preprocessor"))
+        pp_excellon_label.setToolTip(
+            _("The preprocessor JSON file that dictates\n"
+              "Gcode output.")
+        )
+
+        self.pp_excellon_name_cb = FCComboBox()
+        self.pp_excellon_name_cb.setFocusPolicy(Qt.StrongFocus)
+        self.pp_excellon_name_cb.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred)
+
+        grid0.addWidget(pp_excellon_label, 14, 0)
+        grid0.addWidget(self.pp_excellon_name_cb, 14, 1, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 16, 0, 1, 3)
+
+        # DRILL SLOTS LABEL
+        self.dslots_label = FCLabel('<b>%s:</b>' % _('Drilling Slots'))
+        grid0.addWidget(self.dslots_label, 18, 0, 1, 3)
+
+        # Drill slots
+        self.drill_slots_cb = FCCheckBox('%s' % _('Drill slots'))
+        self.drill_slots_cb.setToolTip(
+            _("If the selected tool has slots then they will be drilled.")
+        )
+        grid0.addWidget(self.drill_slots_cb, 20, 0, 1, 3)
+
+        # Drill Overlap
+        self.drill_overlap_label = FCLabel('%s:' % _('Overlap'))
+        self.drill_overlap_label.setToolTip(
+            _("How much (percentage) of the tool diameter to overlap previous drill hole.")
+        )
+
+        self.drill_overlap_entry = FCDoubleSpinner()
+        self.drill_overlap_entry.set_precision(self.decimals)
+        self.drill_overlap_entry.set_range(0.0, 10000.0000)
+        self.drill_overlap_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.drill_overlap_label, 22, 0)
+        grid0.addWidget(self.drill_overlap_entry, 22, 1, 1, 2)
+
+        # Last drill in slot
+        self.last_drill_cb = FCCheckBox('%s' % _('Last drill'))
+        self.last_drill_cb.setToolTip(
+            _("If the slot length is not completely covered by drill holes,\n"
+              "add a drill hole on the slot end point.")
+        )
+        grid0.addWidget(self.last_drill_cb, 24, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 26, 0, 1, 3)
+
+        self.exc_label = FCLabel('<b>%s:</b>' % _('Advanced Options'))
+        self.exc_label.setToolTip(
+            _("A list of advanced parameters.")
+        )
+        grid0.addWidget(self.exc_label, 28, 0, 1, 3)
+
+        # Offset Z
+        offsetlabel = FCLabel('%s:' % _('Offset Z'))
+        offsetlabel.setToolTip(
+            _("Some drill bits (the larger ones) need to drill deeper\n"
+              "to create the desired exit hole diameter due of the tip shape.\n"
+              "The value here can compensate the Cut Z parameter."))
+        self.offset_entry = FCDoubleSpinner()
+        self.offset_entry.set_precision(self.decimals)
+        self.offset_entry.set_range(-999.9999, 999.9999)
+
+        grid0.addWidget(offsetlabel, 29, 0)
+        grid0.addWidget(self.offset_entry, 29, 1, 1, 2)
+
+        # ToolChange X,Y
+        toolchange_xy_label = FCLabel('%s:' % _('Toolchange X,Y'))
+        toolchange_xy_label.setToolTip(
+            _("Toolchange X,Y position.")
+        )
+        self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
+        grid0.addWidget(toolchange_xy_label, 31, 0)
+        grid0.addWidget(self.toolchangexy_entry, 31, 1, 1, 2)
+
+        # Start Z
+        startzlabel = FCLabel('%s:' % _('Start Z'))
+        startzlabel.setToolTip(
+            _("Height of the tool just after starting the work.\n"
+              "Delete the value if you don't need this feature.")
+        )
+        self.estartz_entry = NumericalEvalEntry(border_color='#0069A9')
+
+        grid0.addWidget(startzlabel, 33, 0)
+        grid0.addWidget(self.estartz_entry, 33, 1, 1, 2)
+
+        # Feedrate Rapids
+        fr_rapid_label = FCLabel('%s:' % _('Feedrate Rapids'))
+        fr_rapid_label.setToolTip(
+            _("Tool speed while drilling\n"
+              "(in units per minute).\n"
+              "This is for the rapid move G00.\n"
+              "It is useful only for Marlin,\n"
+              "ignore for any other cases.")
+        )
+        self.feedrate_rapid_entry = FCDoubleSpinner()
+        self.feedrate_rapid_entry.set_precision(self.decimals)
+        self.feedrate_rapid_entry.set_range(0, 910000.0000)
+
+        grid0.addWidget(fr_rapid_label, 35, 0)
+        grid0.addWidget(self.feedrate_rapid_entry, 35, 1, 1, 2)
+
+        # Probe depth
+        self.pdepth_label = FCLabel('%s:' % _("Probe Z depth"))
+        self.pdepth_label.setToolTip(
+            _("The maximum depth that the probe is allowed\n"
+              "to probe. Negative value, in current units.")
+        )
+        self.pdepth_entry = FCDoubleSpinner()
+        self.pdepth_entry.set_precision(self.decimals)
+        self.pdepth_entry.set_range(-910000.0000, 0.0000)
+
+        grid0.addWidget(self.pdepth_label, 37, 0)
+        grid0.addWidget(self.pdepth_entry, 37, 1, 1, 2)
+
+        # Probe feedrate
+        self.feedrate_probe_label = FCLabel('%s:' % _("Feedrate Probe"))
+        self.feedrate_probe_label.setToolTip(
+           _("The feedrate used while the probe is probing.")
+        )
+        self.feedrate_probe_entry = FCDoubleSpinner()
+        self.feedrate_probe_entry.set_precision(self.decimals)
+        self.feedrate_probe_entry.set_range(0, 910000.0000)
+
+        grid0.addWidget(self.feedrate_probe_label, 38, 0)
+        grid0.addWidget(self.feedrate_probe_entry, 38, 1, 1, 2)
+
+        # Spindle direction
+        spindle_dir_label = FCLabel('%s:' % _('Spindle direction'))
+        spindle_dir_label.setToolTip(
+            _("This sets the direction that the spindle is rotating.\n"
+              "It can be either:\n"
+              "- CW = clockwise or\n"
+              "- CCW = counter clockwise")
+        )
+
+        self.spindledir_radio = RadioSet([{'label': _('CW'), 'value': 'CW'},
+                                          {'label': _('CCW'), 'value': 'CCW'}])
+        grid0.addWidget(spindle_dir_label, 40, 0)
+        grid0.addWidget(self.spindledir_radio, 40, 1, 1, 2)
+
+        self.fplunge_cb = FCCheckBox('%s' % _('Fast Plunge'))
+        self.fplunge_cb.setToolTip(
+            _("By checking this, the vertical move from\n"
+              "Z_Toolchange to Z_move is done with G0,\n"
+              "meaning the fastest speed available.\n"
+              "WARNING: the move is done at Toolchange X,Y coords.")
+        )
+        grid0.addWidget(self.fplunge_cb, 42, 0, 1, 3)
+
+        self.fretract_cb = FCCheckBox('%s' % _('Fast Retract'))
+        self.fretract_cb.setToolTip(
+            _("Exit hole strategy.\n"
+              " - When uncheked, while exiting the drilled hole the drill bit\n"
+              "will travel slow, with set feedrate (G1), up to zero depth and then\n"
+              "travel as fast as possible (G0) to the Z Move (travel height).\n"
+              " - When checked the travel from Z cut (cut depth) to Z_move\n"
+              "(travel height) is done as fast as possible (G0) in one move.")
+        )
+
+        grid0.addWidget(self.fretract_cb, 45, 0, 1, 3)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 46, 0, 1, 3)
+
+        # -----------------------------
+        # --- Area Exclusion ----------
+        # -----------------------------
+        self.area_exc_label = FCLabel('<b>%s:</b>' % _('Area Exclusion'))
+        self.area_exc_label.setToolTip(
+            _("Area exclusion parameters.")
+        )
+        grid0.addWidget(self.area_exc_label, 47, 0, 1, 2)
+
+        # Exclusion Area CB
+        self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas"))
+        self.exclusion_cb.setToolTip(
+            _(
+                "Include exclusion areas.\n"
+                "In those areas the travel of the tools\n"
+                "is forbidden."
+            )
+        )
+        grid0.addWidget(self.exclusion_cb, 49, 0, 1, 2)
+
+        # Area Selection shape
+        self.area_shape_label = FCLabel('%s:' % _("Shape"))
+        self.area_shape_label.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+
+        grid0.addWidget(self.area_shape_label, 51, 0)
+        grid0.addWidget(self.area_shape_radio, 51, 1)
+
+        # Chose Strategy
+        self.strategy_label = FCLabel('%s:' % _("Strategy"))
+        self.strategy_label.setToolTip(_("The strategy followed when encountering an exclusion area.\n"
+                                         "Can be:\n"
+                                         "- Over -> when encountering the area, the tool will go to a set height\n"
+                                         "- Around -> will avoid the exclusion area by going around the area"))
+        self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'},
+                                        {'label': _('Around'), 'value': 'around'}])
+
+        grid0.addWidget(self.strategy_label, 53, 0)
+        grid0.addWidget(self.strategy_radio, 53, 1)
+
+        # Over Z
+        self.over_z_label = FCLabel('%s:' % _("Over Z"))
+        self.over_z_label.setToolTip(_("The height Z to which the tool will rise in order to avoid\n"
+                                       "an interdiction area."))
+        self.over_z_entry = FCDoubleSpinner()
+        self.over_z_entry.set_range(0.000, 10000.0000)
+        self.over_z_entry.set_precision(self.decimals)
+
+        grid0.addWidget(self.over_z_label, 55, 0)
+        grid0.addWidget(self.over_z_entry, 55, 1)
+
+        self.layout.addStretch()

+ 322 - 0
appGUI/preferences/tools/ToolsFilmPrefGroupUI.py

@@ -0,0 +1,322 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCComboBox, FCColorEntry, FCLabel, FCSpinner
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsFilmPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Cutout Tool Options", parent=parent)
+        super(ToolsFilmPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Film Tool Options")))
+        self.decimals = decimals
+
+        # ## Parameters
+        self.film_label = FCLabel("<b>%s:</b>" % _("Parameters"))
+        self.film_label.setToolTip(
+            _("Create a PCB film from a Gerber or Geometry object.\n"
+              "The file is saved in SVG format.")
+        )
+        self.layout.addWidget(self.film_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        self.film_type_radio = RadioSet([{'label': 'Pos', 'value': 'pos'},
+                                         {'label': 'Neg', 'value': 'neg'}])
+        ftypelbl = FCLabel('%s:' % _('Film Type'))
+        ftypelbl.setToolTip(
+            _("Generate a Positive black film or a Negative film.\n"
+              "Positive means that it will print the features\n"
+              "with black on a white canvas.\n"
+              "Negative means that it will print the features\n"
+              "with white on a black canvas.\n"
+              "The Film format is SVG.")
+        )
+        grid0.addWidget(ftypelbl, 0, 0)
+        grid0.addWidget(self.film_type_radio, 0, 1)
+
+        # Film Color
+        self.film_color_label = FCLabel('%s:' % _('Film Color'))
+        self.film_color_label.setToolTip(
+            _("Set the film color when positive film is selected.")
+        )
+        self.film_color_entry = FCColorEntry()
+
+        grid0.addWidget(self.film_color_label, 1, 0)
+        grid0.addWidget(self.film_color_entry, 1, 1)
+
+        # Film Border
+        self.film_boundary_entry = FCDoubleSpinner()
+        self.film_boundary_entry.set_precision(self.decimals)
+        self.film_boundary_entry.set_range(0, 10000.0000)
+        self.film_boundary_entry.setSingleStep(0.1)
+
+        self.film_boundary_label = FCLabel('%s:' % _("Border"))
+        self.film_boundary_label.setToolTip(
+            _("Specify a border around the object.\n"
+              "Only for negative film.\n"
+              "It helps if we use as a Box Object the same \n"
+              "object as in Film Object. It will create a thick\n"
+              "black bar around the actual print allowing for a\n"
+              "better delimitation of the outline features which are of\n"
+              "white color like the rest and which may confound with the\n"
+              "surroundings if not for this border.")
+        )
+        grid0.addWidget(self.film_boundary_label, 2, 0)
+        grid0.addWidget(self.film_boundary_entry, 2, 1)
+
+        self.film_scale_stroke_entry = FCDoubleSpinner()
+        self.film_scale_stroke_entry.set_precision(self.decimals)
+        self.film_scale_stroke_entry.set_range(0, 10000.0000)
+        self.film_scale_stroke_entry.setSingleStep(0.1)
+
+        self.film_scale_stroke_label = FCLabel('%s:' % _("Scale Stroke"))
+        self.film_scale_stroke_label.setToolTip(
+            _("Scale the line stroke thickness of each feature in the SVG file.\n"
+              "It means that the line that envelope each SVG feature will be thicker or thinner,\n"
+              "therefore the fine features may be more affected by this parameter.")
+        )
+        grid0.addWidget(self.film_scale_stroke_label, 3, 0)
+        grid0.addWidget(self.film_scale_stroke_entry, 3, 1)
+
+        self.film_adj_label = FCLabel('<b>%s</b>' % _("Film Adjustments"))
+        self.film_adj_label.setToolTip(
+            _("Sometime the printers will distort the print shape, especially the Laser types.\n"
+              "This section provide the tools to compensate for the print distortions.")
+        )
+
+        grid0.addWidget(self.film_adj_label, 4, 0, 1, 2)
+
+        # Scale Geometry
+        self.film_scale_cb = FCCheckBox('%s' % _("Scale Film geometry"))
+        self.film_scale_cb.setToolTip(
+            _("A value greater than 1 will stretch the film\n"
+              "while a value less than 1 will jolt it.")
+        )
+        self.film_scale_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        grid0.addWidget(self.film_scale_cb, 5, 0, 1, 2)
+
+        self.film_scalex_label = FCLabel('%s:' % _("X factor"))
+        self.film_scalex_entry = FCDoubleSpinner()
+        self.film_scalex_entry.set_range(-999.9999, 999.9999)
+        self.film_scalex_entry.set_precision(self.decimals)
+        self.film_scalex_entry.setSingleStep(0.01)
+
+        grid0.addWidget(self.film_scalex_label, 6, 0)
+        grid0.addWidget(self.film_scalex_entry, 6, 1)
+
+        self.film_scaley_label = FCLabel('%s:' % _("Y factor"))
+        self.film_scaley_entry = FCDoubleSpinner()
+        self.film_scaley_entry.set_range(-999.9999, 999.9999)
+        self.film_scaley_entry.set_precision(self.decimals)
+        self.film_scaley_entry.setSingleStep(0.01)
+
+        grid0.addWidget(self.film_scaley_label, 7, 0)
+        grid0.addWidget(self.film_scaley_entry, 7, 1)
+
+        # Skew Geometry
+        self.film_skew_cb = FCCheckBox('%s' % _("Skew Film geometry"))
+        self.film_skew_cb.setToolTip(
+            _("Positive values will skew to the right\n"
+              "while negative values will skew to the left.")
+        )
+        self.film_skew_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        grid0.addWidget(self.film_skew_cb, 8, 0, 1, 2)
+
+        self.film_skewx_label = FCLabel('%s:' % _("X angle"))
+        self.film_skewx_entry = FCDoubleSpinner()
+        self.film_skewx_entry.set_range(-999.9999, 999.9999)
+        self.film_skewx_entry.set_precision(self.decimals)
+        self.film_skewx_entry.setSingleStep(0.01)
+
+        grid0.addWidget(self.film_skewx_label, 9, 0)
+        grid0.addWidget(self.film_skewx_entry, 9, 1)
+
+        self.film_skewy_label = FCLabel('%s:' % _("Y angle"))
+        self.film_skewy_entry = FCDoubleSpinner()
+        self.film_skewy_entry.set_range(-999.9999, 999.9999)
+        self.film_skewy_entry.set_precision(self.decimals)
+        self.film_skewy_entry.setSingleStep(0.01)
+
+        grid0.addWidget(self.film_skewy_label, 10, 0)
+        grid0.addWidget(self.film_skewy_entry, 10, 1)
+
+        self.film_skew_ref_label = FCLabel('%s:' % _("Reference"))
+        self.film_skew_ref_label.setToolTip(
+            _("The reference point to be used as origin for the skew.\n"
+              "It can be one of the four points of the geometry bounding box.")
+        )
+        self.film_skew_reference = RadioSet([{'label': _('Bottom Left'), 'value': 'bottomleft'},
+                                             {'label': _('Top Left'), 'value': 'topleft'},
+                                             {'label': _('Bottom Right'), 'value': 'bottomright'},
+                                             {'label': _('Top right'), 'value': 'topright'}],
+                                            orientation='vertical',
+                                            stretch=False)
+
+        grid0.addWidget(self.film_skew_ref_label, 11, 0)
+        grid0.addWidget(self.film_skew_reference, 11, 1)
+
+        # Mirror Geometry
+        self.film_mirror_cb = FCCheckBox('%s' % _("Mirror Film geometry"))
+        self.film_mirror_cb.setToolTip(
+            _("Mirror the film geometry on the selected axis or on both.")
+        )
+        self.film_mirror_cb.setStyleSheet(
+            """
+            QCheckBox {font-weight: bold; color: black}
+            """
+        )
+        grid0.addWidget(self.film_mirror_cb, 12, 0, 1, 2)
+
+        self.film_mirror_axis = RadioSet([{'label': _('None'), 'value': 'none'},
+                                          {'label': _('X'), 'value': 'x'},
+                                          {'label': _('Y'), 'value': 'y'},
+                                          {'label': _('Both'), 'value': 'both'}],
+                                         stretch=False)
+        self.film_mirror_axis_label = FCLabel('%s:' % _("Mirror Axis"))
+
+        grid0.addWidget(self.film_mirror_axis_label, 13, 0)
+        grid0.addWidget(self.film_mirror_axis, 13, 1)
+
+        separator_line3 = QtWidgets.QFrame()
+        separator_line3.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line3.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line3, 14, 0, 1, 2)
+
+        self.file_type_radio = RadioSet([{'label': _('SVG'), 'value': 'svg'},
+                                         {'label': _('PNG'), 'value': 'png'},
+                                         {'label': _('PDF'), 'value': 'pdf'}
+                                         ], stretch=False)
+
+        self.file_type_label = FCLabel('%s:' % _("Film Type"))
+        self.file_type_label.setToolTip(
+            _("The file type of the saved film. Can be:\n"
+              "- 'SVG' -> open-source vectorial format\n"
+              "- 'PNG' -> raster image\n"
+              "- 'PDF' -> portable document format")
+        )
+        grid0.addWidget(self.file_type_label, 15, 0)
+        grid0.addWidget(self.file_type_radio, 15, 1)
+
+        # Page orientation
+        self.orientation_label = FCLabel('%s:' % _("Page Orientation"))
+        self.orientation_label.setToolTip(_("Can be:\n"
+                                            "- Portrait\n"
+                                            "- Landscape"))
+
+        self.orientation_radio = RadioSet([{'label': _('Portrait'), 'value': 'p'},
+                                           {'label': _('Landscape'), 'value': 'l'},
+                                           ], stretch=False)
+
+        grid0.addWidget(self.orientation_label, 16, 0)
+        grid0.addWidget(self.orientation_radio, 16, 1)
+
+        # Page Size
+        self.pagesize_label = FCLabel('%s:' % _("Page Size"))
+        self.pagesize_label.setToolTip(_("A selection of standard ISO 216 page sizes."))
+
+        self.pagesize_combo = FCComboBox()
+
+        self.pagesize = {}
+        self.pagesize.update(
+            {
+                'Bounds': None,
+                'A0': (841, 1189),
+                'A1': (594, 841),
+                'A2': (420, 594),
+                'A3': (297, 420),
+                'A4': (210, 297),
+                'A5': (148, 210),
+                'A6': (105, 148),
+                'A7': (74, 105),
+                'A8': (52, 74),
+                'A9': (37, 52),
+                'A10': (26, 37),
+
+                'B0': (1000, 1414),
+                'B1': (707, 1000),
+                'B2': (500, 707),
+                'B3': (353, 500),
+                'B4': (250, 353),
+                'B5': (176, 250),
+                'B6': (125, 176),
+                'B7': (88, 125),
+                'B8': (62, 88),
+                'B9': (44, 62),
+                'B10': (31, 44),
+
+                'C0': (917, 1297),
+                'C1': (648, 917),
+                'C2': (458, 648),
+                'C3': (324, 458),
+                'C4': (229, 324),
+                'C5': (162, 229),
+                'C6': (114, 162),
+                'C7': (81, 114),
+                'C8': (57, 81),
+                'C9': (40, 57),
+                'C10': (28, 40),
+
+                # American paper sizes
+                'LETTER': (8.5, 11),
+                'LEGAL': (8.5, 14),
+                'ELEVENSEVENTEEN': (11, 17),
+
+                # From https://en.wikipedia.org/wiki/Paper_size
+                'JUNIOR_LEGAL': (5, 8),
+                'HALF_LETTER': (5.5, 8),
+                'GOV_LETTER': (8, 10.5),
+                'GOV_LEGAL': (8.5, 13),
+                'LEDGER': (17, 11),
+            }
+        )
+
+        page_size_list = list(self.pagesize.keys())
+        self.pagesize_combo.addItems(page_size_list)
+
+        grid0.addWidget(self.pagesize_label, 17, 0)
+        grid0.addWidget(self.pagesize_combo, 17, 1)
+
+        # PNG DPI
+        self.png_dpi_label = FCLabel('%s:' % "PNG DPI")
+        self.png_dpi_label.setToolTip(
+            _("Default value is 96 DPI. Change this value to scale the PNG file.")
+        )
+        self.png_dpi_spinner = FCSpinner()
+        self.png_dpi_spinner.set_range(0, 100000)
+
+        grid0.addWidget(self.png_dpi_label, 19, 0)
+        grid0.addWidget(self.png_dpi_spinner, 19, 1)
+
+        self.layout.addStretch()
+
+        # Film Tool
+        self.film_color_entry.editingFinished.connect(self.on_film_color_entry)
+
+    def on_film_color_entry(self):
+        self.app.defaults['tools_film_color'] = self.film_color_entry.get_value()

+ 347 - 0
appGUI/preferences/tools/ToolsISOPrefGroupUI.py

@@ -0,0 +1,347 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox2, FCCheckBox, FCSpinner, NumericalEvalTupleEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsISOPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        super(ToolsISOPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Isolation Tool Options")))
+        self.decimals = decimals
+
+        # ## Clear non-copper regions
+        self.iso_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.iso_label.setToolTip(
+            _("Create a Geometry object with\n"
+              "toolpaths to cut around polygons.")
+        )
+        self.layout.addWidget(self.iso_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Tool Dias
+        isotdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
+        isotdlabel.setToolTip(
+            _("Diameters of the tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
+        )
+        self.tool_dia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+        self.tool_dia_entry.setPlaceholderText(_("Comma separated values"))
+
+        grid0.addWidget(isotdlabel, 0, 0)
+        grid0.addWidget(self.tool_dia_entry, 0, 1, 1, 2)
+
+        # Tool order Radio Button
+        self.order_label = QtWidgets.QLabel('%s:' % _('Tool order'))
+        self.order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
+                                      "'No' --> means that the used order is the one in the tool table\n"
+                                      "'Forward' --> means that the tools will be ordered from small to big\n"
+                                      "'Reverse' --> means that the tools will ordered from big to small\n\n"
+                                      "WARNING: using rest machining will automatically set the order\n"
+                                      "in reverse and disable this control."))
+
+        self.order_radio = RadioSet([{'label': _('No'), 'value': 'no'},
+                                     {'label': _('Forward'), 'value': 'fwd'},
+                                     {'label': _('Reverse'), 'value': 'rev'}])
+
+        grid0.addWidget(self.order_label, 1, 0)
+        grid0.addWidget(self.order_radio, 1, 1, 1, 2)
+
+        # Tool Type Radio Button
+        self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
+        self.tool_type_label.setToolTip(
+            _("Default tool type:\n"
+              "- 'V-shape'\n"
+              "- Circular")
+        )
+
+        self.tool_type_radio = RadioSet([{'label': _('V-shape'), 'value': 'V'},
+                                         {'label': _('Circular'), 'value': 'C1'}])
+        self.tool_type_radio.setToolTip(
+            _("Default tool type:\n"
+              "- 'V-shape'\n"
+              "- Circular")
+        )
+
+        grid0.addWidget(self.tool_type_label, 2, 0)
+        grid0.addWidget(self.tool_type_radio, 2, 1, 1, 2)
+
+        # Tip Dia
+        self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
+        self.tipdialabel.setToolTip(
+            _("The tip diameter for V-Shape Tool"))
+        self.tipdia_entry = FCDoubleSpinner()
+        self.tipdia_entry.set_precision(self.decimals)
+        self.tipdia_entry.set_range(0, 1000)
+        self.tipdia_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.tipdialabel, 3, 0)
+        grid0.addWidget(self.tipdia_entry, 3, 1, 1, 2)
+
+        # Tip Angle
+        self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
+        self.tipanglelabel.setToolTip(
+            _("The tip angle for V-Shape Tool.\n"
+              "In degrees."))
+        self.tipangle_entry = FCDoubleSpinner()
+        self.tipangle_entry.set_precision(self.decimals)
+        self.tipangle_entry.set_range(1, 180)
+        self.tipangle_entry.setSingleStep(5)
+        self.tipangle_entry.setWrapping(True)
+
+        grid0.addWidget(self.tipanglelabel, 4, 0)
+        grid0.addWidget(self.tipangle_entry, 4, 1, 1, 2)
+
+        # Cut Z entry
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel.setToolTip(
+           _("Depth of cut into material. Negative value.\n"
+             "In application units.")
+        )
+        self.cutz_entry = FCDoubleSpinner()
+        self.cutz_entry.set_precision(self.decimals)
+        self.cutz_entry.set_range(-10000.0000, 0.0000)
+        self.cutz_entry.setSingleStep(0.1)
+
+        self.cutz_entry.setToolTip(
+           _("Depth of cut into material. Negative value.\n"
+             "In application units.")
+        )
+
+        grid0.addWidget(cutzlabel, 5, 0)
+        grid0.addWidget(self.cutz_entry, 5, 1, 1, 2)
+
+        # New Diameter
+        self.newdialabel = QtWidgets.QLabel('%s:' % _('New Dia'))
+        self.newdialabel.setToolTip(
+            _("Diameter for the new tool to add in the Tool Table.\n"
+              "If the tool is V-shape type then this value is automatically\n"
+              "calculated from the other parameters.")
+        )
+        self.newdia_entry = FCDoubleSpinner()
+        self.newdia_entry.set_precision(self.decimals)
+        self.newdia_entry.set_range(0.0001, 10000.0000)
+        self.newdia_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.newdialabel, 6, 0)
+        grid0.addWidget(self.newdia_entry, 6, 1, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 7, 0, 1, 3)
+
+        # Passes
+        passlabel = QtWidgets.QLabel('%s:' % _('Passes'))
+        passlabel.setToolTip(
+            _("Width of the isolation gap in\n"
+              "number (integer) of tool widths.")
+        )
+        self.passes_entry = FCSpinner()
+        self.passes_entry.set_range(1, 999)
+        self.passes_entry.setObjectName("i_passes")
+
+        grid0.addWidget(passlabel, 8, 0)
+        grid0.addWidget(self.passes_entry, 8, 1, 1, 2)
+
+        # Overlap Entry
+        overlabel = QtWidgets.QLabel('%s:' % _('Overlap'))
+        overlabel.setToolTip(
+            _("How much (percentage) of the tool width to overlap each tool pass.")
+        )
+        self.overlap_entry = FCDoubleSpinner(suffix='%')
+        self.overlap_entry.set_precision(self.decimals)
+        self.overlap_entry.setWrapping(True)
+        self.overlap_entry.set_range(0.0000, 99.9999)
+        self.overlap_entry.setSingleStep(0.1)
+        self.overlap_entry.setObjectName("i_overlap")
+
+        grid0.addWidget(overlabel, 9, 0)
+        grid0.addWidget(self.overlap_entry, 9, 1, 1, 2)
+
+        # Milling Type Radio Button
+        self.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
+        self.milling_type_label.setToolTip(
+            _("Milling type:\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+
+        self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
+                                            {'label': _('Conventional'), 'value': 'cv'}])
+        self.milling_type_radio.setToolTip(
+            _("Milling type:\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+
+        grid0.addWidget(self.milling_type_label, 10, 0)
+        grid0.addWidget(self.milling_type_radio, 10, 1, 1, 2)
+
+        # Follow
+        self.follow_label = QtWidgets.QLabel('%s:' % _('Follow'))
+        self.follow_label.setToolTip(
+            _("Generate a 'Follow' geometry.\n"
+              "This means that it will cut through\n"
+              "the middle of the trace.")
+        )
+
+        self.follow_cb = FCCheckBox()
+        self.follow_cb.setToolTip(_("Generate a 'Follow' geometry.\n"
+                                    "This means that it will cut through\n"
+                                    "the middle of the trace."))
+        self.follow_cb.setObjectName("i_follow")
+
+        grid0.addWidget(self.follow_label, 11, 0)
+        grid0.addWidget(self.follow_cb, 11, 1, 1, 2)
+
+        # Isolation Type
+        self.iso_type_label = QtWidgets.QLabel('%s:' % _('Isolation Type'))
+        self.iso_type_label.setToolTip(
+            _("Choose how the isolation will be executed:\n"
+              "- 'Full' -> complete isolation of polygons\n"
+              "- 'Ext' -> will isolate only on the outside\n"
+              "- 'Int' -> will isolate only on the inside\n"
+              "'Exterior' isolation is almost always possible\n"
+              "(with the right tool) but 'Interior'\n"
+              "isolation can be done only when there is an opening\n"
+              "inside of the polygon (e.g polygon is a 'doughnut' shape).")
+        )
+        self.iso_type_radio = RadioSet([{'label': _('Full'), 'value': 'full'},
+                                        {'label': _('Ext'), 'value': 'ext'},
+                                        {'label': _('Int'), 'value': 'int'}])
+        self.iso_type_radio.setObjectName("i_type")
+
+        grid0.addWidget(self.iso_type_label, 12, 0)
+        grid0.addWidget(self.iso_type_radio, 12, 1, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 13, 0, 1, 3)
+
+        # Rest machining CheckBox
+        self.rest_cb = FCCheckBox('%s' % _("Rest"))
+        self.rest_cb.setObjectName("i_rest_machining")
+        self.rest_cb.setToolTip(
+            _("If checked, use 'rest machining'.\n"
+              "Basically it will process copper outside PCB features,\n"
+              "using the biggest tool and continue with the next tools,\n"
+              "from bigger to smaller, to process the copper features that\n"
+              "could not be processed by previous tool, until there is\n"
+              "nothing left to process or there are no more tools.\n\n"
+              "If not checked, use the standard algorithm.")
+        )
+
+        grid0.addWidget(self.rest_cb, 17, 0)
+
+        # Combine All Passes
+        self.combine_passes_cb = FCCheckBox(label=_('Combine'))
+        self.combine_passes_cb.setToolTip(
+            _("Combine all passes into one object")
+        )
+        self.combine_passes_cb.setObjectName("i_combine")
+
+        grid0.addWidget(self.combine_passes_cb, 17, 1)
+
+        # Exception Areas
+        self.except_cb = FCCheckBox(label=_('Except'))
+        self.except_cb.setToolTip(_("When the isolation geometry is generated,\n"
+                                    "by checking this, the area of the object below\n"
+                                    "will be subtracted from the isolation geometry."))
+        self.except_cb.setObjectName("i_except")
+        grid0.addWidget(self.except_cb, 17, 2)
+
+        # Check Tool validity
+        self.valid_cb = FCCheckBox(label=_('Check validity'))
+        self.valid_cb.setToolTip(
+            _("If checked then the tools diameters are verified\n"
+              "if they will provide a complete isolation.")
+        )
+        self.valid_cb.setObjectName("i_check")
+
+        grid0.addWidget(self.valid_cb, 18, 0, 1, 3)
+
+        # Isolation Scope
+        self.select_label = QtWidgets.QLabel('%s:' % _("Selection"))
+        self.select_label.setToolTip(
+            _("Isolation scope. Choose what to isolate:\n"
+              "- 'All' -> Isolate all the polygons in the object\n"
+              "- 'Area Selection' -> Isolate polygons within a selection area.\n"
+              "- 'Polygon Selection' -> Isolate a selection of polygons.\n"
+              "- 'Reference Object' - will process the area specified by another object.")
+        )
+        self.select_combo = FCComboBox2()
+        self.select_combo.addItems(
+            [_("All"), _("Area Selection"), _("Polygon Selection"), _("Reference Object")]
+        )
+        self.select_combo.setObjectName("i_selection")
+
+        grid0.addWidget(self.select_label, 20, 0)
+        grid0.addWidget(self.select_combo, 20, 1, 1, 2)
+
+        # Area Shape
+        self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape"))
+        self.area_shape_label.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+
+        grid0.addWidget(self.area_shape_label, 21, 0)
+        grid0.addWidget(self.area_shape_radio, 21, 1, 1, 2)
+
+        # Polygon interiors selection
+        self.poly_int_cb = FCCheckBox(_("Interiors"))
+        self.poly_int_cb.setToolTip(
+            _("When checked the user can select interiors of a polygon.\n"
+              "(holes in the polygon).")
+        )
+
+        # Force isolation even if the interiors are not isolated
+        self.force_iso_cb = FCCheckBox(_("Forced Rest"))
+        self.force_iso_cb.setToolTip(
+            _("When checked the isolation will be done with the current tool even if\n"
+              "interiors of a polygon (holes in the polygon) could not be isolated.\n"
+              "Works when 'rest machining' is used.")
+        )
+        grid0.addWidget(self.poly_int_cb, 22, 0)
+        grid0.addWidget(self.force_iso_cb, 22, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 24, 0, 1, 3)
+
+        # ## Plotting type
+        self.plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
+                                        {"label": _("Progressive"), "value": "progressive"}])
+        plotting_label = QtWidgets.QLabel('%s:' % _("Plotting"))
+        plotting_label.setToolTip(
+            _("- 'Normal' - normal plotting, done at the end of the job\n"
+              "- 'Progressive' - each shape is plotted after it is generated")
+        )
+        grid0.addWidget(plotting_label, 25, 0)
+        grid0.addWidget(self.plotting_radio, 25, 1, 1, 2)
+
+        self.layout.addStretch()

+ 357 - 0
appGUI/preferences/tools/ToolsNCCPrefGroupUI.py

@@ -0,0 +1,357 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, NumericalEvalTupleEntry, FCComboBox2
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsNCCPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "NCC Tool Options", parent=parent)
+        super(ToolsNCCPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("NCC Tool Options")))
+        self.decimals = decimals
+
+        # ## Clear non-copper regions
+        self.clearcopper_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.clearcopper_label.setToolTip(
+            _("Create a Geometry object with\n"
+              "toolpaths to cut all non-copper regions.")
+        )
+        self.layout.addWidget(self.clearcopper_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        ncctdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
+        ncctdlabel.setToolTip(
+            _("Diameters of the tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
+        )
+        grid0.addWidget(ncctdlabel, 0, 0)
+        self.ncc_tool_dia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+        self.ncc_tool_dia_entry.setPlaceholderText(_("Comma separated values"))
+        grid0.addWidget(self.ncc_tool_dia_entry, 0, 1)
+
+        # Tool Type Radio Button
+        self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
+        self.tool_type_label.setToolTip(
+            _("Default tool type:\n"
+              "- 'V-shape'\n"
+              "- Circular")
+        )
+
+        self.tool_type_radio = RadioSet([{'label': _('V-shape'), 'value': 'V'},
+                                         {'label': _('Circular'), 'value': 'C1'}])
+        self.tool_type_radio.setToolTip(
+            _("Default tool type:\n"
+              "- 'V-shape'\n"
+              "- Circular")
+        )
+
+        grid0.addWidget(self.tool_type_label, 1, 0)
+        grid0.addWidget(self.tool_type_radio, 1, 1)
+
+        # Tip Dia
+        self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
+        self.tipdialabel.setToolTip(
+            _("The tip diameter for V-Shape Tool"))
+        self.tipdia_entry = FCDoubleSpinner()
+        self.tipdia_entry.set_precision(self.decimals)
+        self.tipdia_entry.set_range(0, 1000)
+        self.tipdia_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.tipdialabel, 2, 0)
+        grid0.addWidget(self.tipdia_entry, 2, 1)
+
+        # Tip Angle
+        self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
+        self.tipanglelabel.setToolTip(
+            _("The tip angle for V-Shape Tool.\n"
+              "In degree."))
+        self.tipangle_entry = FCDoubleSpinner()
+        self.tipangle_entry.set_precision(self.decimals)
+        self.tipangle_entry.set_range(1, 180)
+        self.tipangle_entry.setSingleStep(5)
+        self.tipangle_entry.setWrapping(True)
+
+        grid0.addWidget(self.tipanglelabel, 3, 0)
+        grid0.addWidget(self.tipangle_entry, 3, 1)
+
+        # Cut Z entry
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel.setToolTip(
+           _("Depth of cut into material. Negative value.\n"
+             "In application units.")
+        )
+        self.cutz_entry = FCDoubleSpinner()
+        self.cutz_entry.set_precision(self.decimals)
+        self.cutz_entry.set_range(-10000.0000, 0.0000)
+        self.cutz_entry.setSingleStep(0.1)
+
+        self.cutz_entry.setToolTip(
+           _("Depth of cut into material. Negative value.\n"
+             "In application units.")
+        )
+
+        grid0.addWidget(cutzlabel, 4, 0)
+        grid0.addWidget(self.cutz_entry, 4, 1)
+
+        # New Diameter
+        self.newdialabel = QtWidgets.QLabel('%s:' % _('New Dia'))
+        self.newdialabel.setToolTip(
+            _("Diameter for the new tool to add in the Tool Table.\n"
+              "If the tool is V-shape type then this value is automatically\n"
+              "calculated from the other parameters.")
+        )
+        self.newdia_entry = FCDoubleSpinner()
+        self.newdia_entry.set_precision(self.decimals)
+        self.newdia_entry.set_range(0.0001, 10000.0000)
+        self.newdia_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.newdialabel, 5, 0)
+        grid0.addWidget(self.newdia_entry, 5, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 6, 0, 1, 2)
+
+        # Milling Type Radio Button
+        self.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
+        self.milling_type_label.setToolTip(
+            _("Milling type:\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+
+        self.milling_type_radio = RadioSet([{'label': _('Climb'), 'value': 'cl'},
+                                            {'label': _('Conventional'), 'value': 'cv'}])
+        self.milling_type_radio.setToolTip(
+            _("Milling type:\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+
+        grid0.addWidget(self.milling_type_label, 7, 0)
+        grid0.addWidget(self.milling_type_radio, 7, 1)
+
+        # Tool order Radio Button
+        self.ncc_order_label = QtWidgets.QLabel('%s:' % _('Tool order'))
+        self.ncc_order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
+                                          "'No' --> means that the used order is the one in the tool table\n"
+                                          "'Forward' --> means that the tools will be ordered from small to big\n"
+                                          "'Reverse' --> means that the tools will ordered from big to small\n\n"
+                                          "WARNING: using rest machining will automatically set the order\n"
+                                          "in reverse and disable this control."))
+
+        self.ncc_order_radio = RadioSet([{'label': _('No'), 'value': 'no'},
+                                         {'label': _('Forward'), 'value': 'fwd'},
+                                         {'label': _('Reverse'), 'value': 'rev'}])
+        self.ncc_order_radio.setToolTip(_("This set the way that the tools in the tools table are used.\n"
+                                          "'No' --> means that the used order is the one in the tool table\n"
+                                          "'Forward' --> means that the tools will be ordered from small to big\n"
+                                          "'Reverse' --> means that the tools will ordered from big to small\n\n"
+                                          "WARNING: using rest machining will automatically set the order\n"
+                                          "in reverse and disable this control."))
+        grid0.addWidget(self.ncc_order_label, 8, 0)
+        grid0.addWidget(self.ncc_order_radio, 8, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
+
+        # Overlap Entry
+        nccoverlabel = QtWidgets.QLabel('%s:' % _('Overlap'))
+        nccoverlabel.setToolTip(
+           _("How much (percentage) of the tool width to overlap each tool pass.\n"
+             "Adjust the value starting with lower values\n"
+             "and increasing it if areas that should be processed are still \n"
+             "not processed.\n"
+             "Lower values = faster processing, faster execution on CNC.\n"
+             "Higher values = slow processing and slow execution on CNC\n"
+             "due of too many paths.")
+        )
+        self.ncc_overlap_entry = FCDoubleSpinner(suffix='%')
+        self.ncc_overlap_entry.set_precision(self.decimals)
+        self.ncc_overlap_entry.setWrapping(True)
+        self.ncc_overlap_entry.setRange(0.0000, 99.9999)
+        self.ncc_overlap_entry.setSingleStep(0.1)
+
+        grid0.addWidget(nccoverlabel, 10, 0)
+        grid0.addWidget(self.ncc_overlap_entry, 10, 1)
+
+        # Margin entry
+        nccmarginlabel = QtWidgets.QLabel('%s:' % _('Margin'))
+        nccmarginlabel.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.ncc_margin_entry = FCDoubleSpinner()
+        self.ncc_margin_entry.set_precision(self.decimals)
+        self.ncc_margin_entry.set_range(-10000, 10000)
+        self.ncc_margin_entry.setSingleStep(0.1)
+
+        grid0.addWidget(nccmarginlabel, 11, 0)
+        grid0.addWidget(self.ncc_margin_entry, 11, 1)
+
+        # Method
+        methodlabel = QtWidgets.QLabel('%s:' % _('Method'))
+        methodlabel.setToolTip(
+            _("Algorithm for copper clearing:\n"
+              "- Standard: Fixed step inwards.\n"
+              "- Seed-based: Outwards from seed.\n"
+              "- Line-based: Parallel lines.")
+        )
+
+        # self.ncc_method_radio = RadioSet([
+        #     {"label": _("Standard"), "value": "standard"},
+        #     {"label": _("Seed-based"), "value": "seed"},
+        #     {"label": _("Straight lines"), "value": "lines"}
+        # ], orientation='vertical', stretch=False)
+        self.ncc_method_combo = FCComboBox2()
+        self.ncc_method_combo.addItems(
+            [_("Standard"), _("Seed"), _("Lines"), _("Combo")]
+        )
+
+        grid0.addWidget(methodlabel, 12, 0)
+        grid0.addWidget(self.ncc_method_combo, 12, 1)
+
+        # Connect lines
+        self.ncc_connect_cb = FCCheckBox('%s' % _("Connect"))
+        self.ncc_connect_cb.setToolTip(
+            _("Draw lines between resulting\n"
+              "segments to minimize tool lifts.")
+        )
+
+        grid0.addWidget(self.ncc_connect_cb, 13, 0)
+
+        # Contour Checkbox
+        self.ncc_contour_cb = FCCheckBox('%s' % _("Contour"))
+        self.ncc_contour_cb.setToolTip(
+           _("Cut around the perimeter of the polygon\n"
+             "to trim rough edges.")
+        )
+
+        grid0.addWidget(self.ncc_contour_cb, 13, 1)
+
+        # ## NCC Offset choice
+        self.ncc_choice_offset_cb = FCCheckBox('%s' % _("Offset"))
+        self.ncc_choice_offset_cb.setToolTip(
+            _("If used, it will add an offset to the copper features.\n"
+              "The copper clearing will finish to a distance\n"
+              "from the copper features.")
+        )
+
+        grid0.addWidget(self.ncc_choice_offset_cb, 14, 0, 1, 2)
+
+        # ## NCC Offset value
+        self.ncc_offset_label = QtWidgets.QLabel('%s:' % _("Offset value"))
+        self.ncc_offset_label.setToolTip(
+            _("If used, it will add an offset to the copper features.\n"
+              "The copper clearing will finish to a distance\n"
+              "from the copper features.")
+        )
+        self.ncc_offset_spinner = FCDoubleSpinner()
+        self.ncc_offset_spinner.set_range(0.00, 10000.0000)
+        self.ncc_offset_spinner.set_precision(self.decimals)
+        self.ncc_offset_spinner.setWrapping(True)
+        self.ncc_offset_spinner.setSingleStep(0.1)
+
+        grid0.addWidget(self.ncc_offset_label, 15, 0)
+        grid0.addWidget(self.ncc_offset_spinner, 15, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 16, 0, 1, 2)
+
+        # Rest machining CheckBox
+        self.ncc_rest_cb = FCCheckBox('%s' % _("Rest"))
+        self.ncc_rest_cb.setToolTip(
+            _("If checked, use 'rest machining'.\n"
+              "Basically it will process copper outside PCB features,\n"
+              "using the biggest tool and continue with the next tools,\n"
+              "from bigger to smaller, to process the copper features that\n"
+              "could not be processed by previous tool, until there is\n"
+              "nothing left to process or there are no more tools.\n\n"
+              "If not checked, use the standard algorithm.")
+        )
+
+        grid0.addWidget(self.ncc_rest_cb, 17, 0, 1, 2)
+
+        # ## Reference
+        # self.reference_radio = RadioSet([{'label': _('Itself'), 'value': 'itself'},
+        #                                  {"label": _("Area Selection"), "value": "area"},
+        #                                  {'label': _('Reference Object'), 'value': 'box'}],
+        #                                 orientation='vertical',
+        #                                 stretch=None)
+        self.select_combo = FCComboBox2()
+        self.select_combo.addItems(
+            [_("Itself"), _("Area Selection"), _("Reference Object")]
+        )
+        select_label = QtWidgets.QLabel('%s:' % _("Selection"))
+        select_label.setToolTip(
+            _("Selection of area to be processed.\n"
+              "- 'Itself' - the processing extent is based on the object that is processed.\n "
+              "- 'Area Selection' - left mouse click to start selection of the area to be processed.\n"
+              "- 'Reference Object' - will process the area specified by another object.")
+        )
+
+        grid0.addWidget(select_label, 18, 0)
+        grid0.addWidget(self.select_combo, 18, 1)
+
+        self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape"))
+        self.area_shape_label.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+
+        grid0.addWidget(self.area_shape_label, 19, 0)
+        grid0.addWidget(self.area_shape_radio, 19, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 20, 0, 1, 2)
+
+        # ## Plotting type
+        self.plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
+                                        {"label": _("Progressive"), "value": "progressive"}])
+        plotting_label = QtWidgets.QLabel('%s:' % _("Plotting"))
+        plotting_label.setToolTip(
+            _("- 'Normal' - normal plotting, done at the end of the job\n"
+              "- 'Progressive' - each shape is plotted after it is generated")
+        )
+        grid0.addWidget(plotting_label, 21, 0)
+        grid0.addWidget(self.plotting_radio, 21, 1)
+
+        # Check Tool validity
+        self.valid_cb = FCCheckBox(label=_('Check validity'))
+        self.valid_cb.setToolTip(
+            _("If checked then the tools diameters are verified\n"
+              "if they will provide a complete isolation.")
+        )
+        self.valid_cb.setObjectName("n_check")
+
+        grid0.addWidget(self.valid_cb, 23, 0, 1, 2)
+
+        self.layout.addStretch()

+ 311 - 0
appGUI/preferences/tools/ToolsPaintPrefGroupUI.py

@@ -0,0 +1,311 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox2, FCCheckBox, NumericalEvalTupleEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsPaintPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Paint Area Tool Options", parent=parent)
+        super(ToolsPaintPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Paint Tool Options")))
+        self.decimals = decimals
+
+        # ------------------------------
+        # ## Paint area
+        # ------------------------------
+        self.paint_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.paint_label.setToolTip(
+            _("Creates tool paths to cover the\n"
+              "whole area of a polygon.")
+        )
+        self.layout.addWidget(self.paint_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+        self.layout.addLayout(grid0)
+
+        # Tool dia
+        ptdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
+        ptdlabel.setToolTip(
+            _("Diameters of the tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
+        )
+        grid0.addWidget(ptdlabel, 0, 0)
+
+        self.painttooldia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+        self.painttooldia_entry.setPlaceholderText(_("Comma separated values"))
+
+        grid0.addWidget(self.painttooldia_entry, 0, 1)
+
+        # Tool Type Radio Button
+        self.tool_type_label = QtWidgets.QLabel('%s:' % _('Tool Type'))
+        self.tool_type_label.setToolTip(
+            _("Default tool type:\n"
+              "- 'V-shape'\n"
+              "- Circular")
+        )
+
+        self.tool_type_radio = RadioSet([{'label': _('V-shape'), 'value': 'V'},
+                                         {'label': _('Circular'), 'value': 'C1'}])
+
+        self.tool_type_radio.setObjectName(_("Tool Type"))
+
+        grid0.addWidget(self.tool_type_label, 1, 0)
+        grid0.addWidget(self.tool_type_radio, 1, 1)
+
+        # Tip Dia
+        self.tipdialabel = QtWidgets.QLabel('%s:' % _('V-Tip Dia'))
+        self.tipdialabel.setToolTip(
+            _("The tip diameter for V-Shape Tool"))
+        self.tipdia_entry = FCDoubleSpinner()
+        self.tipdia_entry.set_precision(self.decimals)
+        self.tipdia_entry.set_range(0.0000, 10000.0000)
+        self.tipdia_entry.setSingleStep(0.1)
+        self.tipdia_entry.setObjectName(_("V-Tip Dia"))
+
+        grid0.addWidget(self.tipdialabel, 2, 0)
+        grid0.addWidget(self.tipdia_entry, 2, 1)
+
+        # Tip Angle
+        self.tipanglelabel = QtWidgets.QLabel('%s:' % _('V-Tip Angle'))
+        self.tipanglelabel.setToolTip(
+            _("The tip angle for V-Shape Tool.\n"
+              "In degree."))
+        self.tipangle_entry = FCDoubleSpinner()
+        self.tipangle_entry.set_precision(self.decimals)
+        self.tipangle_entry.set_range(1.0000, 180.0000)
+        self.tipangle_entry.setSingleStep(5)
+        self.tipangle_entry.setObjectName(_("V-Tip Angle"))
+
+        grid0.addWidget(self.tipanglelabel, 3, 0)
+        grid0.addWidget(self.tipangle_entry, 3, 1)
+
+        # Cut Z entry
+        cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
+        cutzlabel.setToolTip(
+            _("Depth of cut into material. Negative value.\n"
+              "In application units.")
+        )
+        self.cutz_entry = FCDoubleSpinner()
+        self.cutz_entry.set_precision(self.decimals)
+        self.cutz_entry.set_range(-910000.0000, 0.0000)
+        self.cutz_entry.setObjectName(_("Cut Z"))
+
+        self.cutz_entry.setToolTip(
+            _("Depth of cut into material. Negative value.\n"
+              "In application units.")
+        )
+        grid0.addWidget(cutzlabel, 4, 0)
+        grid0.addWidget(self.cutz_entry, 4, 1)
+
+        # ### Tool Diameter ####
+        self.newdialabel = QtWidgets.QLabel('%s:' % _('New Dia'))
+        self.newdialabel.setToolTip(
+            _("Diameter for the new tool to add in the Tool Table.\n"
+              "If the tool is V-shape type then this value is automatically\n"
+              "calculated from the other parameters.")
+        )
+        self.newdia_entry = FCDoubleSpinner()
+        self.newdia_entry.set_precision(self.decimals)
+        self.newdia_entry.set_range(0.000, 10000.0000)
+        self.newdia_entry.setObjectName(_("Tool Dia"))
+
+        grid0.addWidget(self.newdialabel, 5, 0)
+        grid0.addWidget(self.newdia_entry, 5, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 6, 0, 1, 2)
+
+        self.paint_order_label = QtWidgets.QLabel('%s:' % _('Tool order'))
+        self.paint_order_label.setToolTip(_("This set the way that the tools in the tools table are used.\n"
+                                            "'No' --> means that the used order is the one in the tool table\n"
+                                            "'Forward' --> means that the tools will be ordered from small to big\n"
+                                            "'Reverse' --> means that the tools will ordered from big to small\n\n"
+                                            "WARNING: using rest machining will automatically set the order\n"
+                                            "in reverse and disable this control."))
+
+        self.paint_order_radio = RadioSet([{'label': _('No'), 'value': 'no'},
+                                           {'label': _('Forward'), 'value': 'fwd'},
+                                           {'label': _('Reverse'), 'value': 'rev'}])
+
+        grid0.addWidget(self.paint_order_label, 7, 0)
+        grid0.addWidget(self.paint_order_radio, 7, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 8, 0, 1, 2)
+
+        # Overlap
+        ovlabel = QtWidgets.QLabel('%s:' % _('Overlap'))
+        ovlabel.setToolTip(
+            _("How much (percentage) of the tool width to overlap each tool pass.\n"
+              "Adjust the value starting with lower values\n"
+              "and increasing it if areas that should be processed are still \n"
+              "not processed.\n"
+              "Lower values = faster processing, faster execution on CNC.\n"
+              "Higher values = slow processing and slow execution on CNC\n"
+              "due of too many paths.")
+        )
+        self.paintoverlap_entry = FCDoubleSpinner(suffix='%')
+        self.paintoverlap_entry.set_precision(self.decimals)
+        self.paintoverlap_entry.setWrapping(True)
+        self.paintoverlap_entry.setRange(0.0000, 99.9999)
+        self.paintoverlap_entry.setSingleStep(0.1)
+
+        grid0.addWidget(ovlabel, 9, 0)
+        grid0.addWidget(self.paintoverlap_entry, 9, 1)
+
+        # Margin
+        marginlabel = QtWidgets.QLabel('%s:' % _('Margin'))
+        marginlabel.setToolTip(
+            _("Distance by which to avoid\n"
+              "the edges of the polygon to\n"
+              "be painted.")
+        )
+        self.paintmargin_entry = FCDoubleSpinner()
+        self.paintmargin_entry.set_range(-10000.0000, 10000.0000)
+        self.paintmargin_entry.set_precision(self.decimals)
+        self.paintmargin_entry.setSingleStep(0.1)
+
+        grid0.addWidget(marginlabel, 10, 0)
+        grid0.addWidget(self.paintmargin_entry, 10, 1)
+
+        # Method
+        methodlabel = QtWidgets.QLabel('%s:' % _('Method'))
+        methodlabel.setToolTip(
+            _("Algorithm for painting:\n"
+              "- Standard: Fixed step inwards.\n"
+              "- Seed-based: Outwards from seed.\n"
+              "- Line-based: Parallel lines.\n"
+              "- Laser-lines: Active only for Gerber objects.\n"
+              "Will create lines that follow the traces.\n"
+              "- Combo: In case of failure a new method will be picked from the above\n"
+              "in the order specified.")
+        )
+
+        # self.paintmethod_combo = RadioSet([
+        #     {"label": _("Standard"), "value": "standard"},
+        #     {"label": _("Seed-based"), "value": "seed"},
+        #     {"label": _("Straight lines"), "value": "lines"}
+        # ], orientation='vertical', stretch=False)
+        self.paintmethod_combo = FCComboBox2()
+        self.paintmethod_combo.addItems(
+            [_("Standard"), _("Seed"), _("Lines"), _("Laser_lines"), _("Combo")]
+        )
+
+        grid0.addWidget(methodlabel, 11, 0)
+        grid0.addWidget(self.paintmethod_combo, 11, 1)
+
+        # Connect lines
+        self.pathconnect_cb = FCCheckBox('%s' % _("Connect"))
+        self.pathconnect_cb.setToolTip(
+            _("Draw lines between resulting\n"
+              "segments to minimize tool lifts.")
+        )
+        grid0.addWidget(self.pathconnect_cb, 12, 0)
+
+        # Paint contour
+        self.contour_cb = FCCheckBox('%s' % _("Contour"))
+        self.contour_cb.setToolTip(
+            _("Cut around the perimeter of the polygon\n"
+              "to trim rough edges.")
+        )
+        grid0.addWidget(self.contour_cb, 12, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 13, 0, 1, 2)
+
+        self.rest_cb = FCCheckBox('%s' % _("Rest"))
+        self.rest_cb.setObjectName(_("Rest"))
+        self.rest_cb.setToolTip(
+            _("If checked, use 'rest machining'.\n"
+              "Basically it will process copper outside PCB features,\n"
+              "using the biggest tool and continue with the next tools,\n"
+              "from bigger to smaller, to process the copper features that\n"
+              "could not be processed by previous tool, until there is\n"
+              "nothing left to process or there are no more tools.\n\n"
+              "If not checked, use the standard algorithm.")
+        )
+        grid0.addWidget(self.rest_cb, 14, 0, 1, 2)
+
+        # Polygon selection
+        selectlabel = QtWidgets.QLabel('%s:' % _('Selection'))
+        selectlabel.setToolTip(
+            _("Selection of area to be processed.\n"
+              "- 'Polygon Selection' - left mouse click to add/remove polygons to be processed.\n"
+              "- 'Area Selection' - left mouse click to start selection of the area to be processed.\n"
+              "Keeping a modifier key pressed (CTRL or SHIFT) will allow to add multiple areas.\n"
+              "- 'All Polygons' - the process will start after click.\n"
+              "- 'Reference Object' - will process the area specified by another object.")
+        )
+
+        # self.selectmethod_combo = RadioSet(
+        #     [
+        #         {"label": _("Polygon Selection"), "value": "single"},
+        #         {"label": _("Area Selection"), "value": "area"},
+        #         {"label": _("All Polygons"), "value": "all"},
+        #         {"label": _("Reference Object"), "value": "ref"}
+        #     ],
+        #     orientation='vertical',
+        #     stretch=None
+        # )
+        self.selectmethod_combo = FCComboBox2()
+        self.selectmethod_combo.addItems(
+            [_("All"), _("Polygon Selection"), _("Area Selection"), _("Reference Object")]
+        )
+
+        grid0.addWidget(selectlabel, 15, 0)
+        grid0.addWidget(self.selectmethod_combo, 15, 1)
+
+        self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape"))
+        self.area_shape_label.setToolTip(
+            _("The kind of selection shape used for area selection.")
+        )
+
+        self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
+                                          {'label': _("Polygon"), 'value': 'polygon'}])
+
+        grid0.addWidget(self.area_shape_label, 18, 0)
+        grid0.addWidget(self.area_shape_radio, 18, 1)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 19, 0, 1, 2)
+
+        # ## Plotting type
+        self.paint_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
+                                              {"label": _("Progressive"), "value": "progressive"}])
+        plotting_label = QtWidgets.QLabel('%s:' % _("Plotting"))
+        plotting_label.setToolTip(
+            _("- 'Normal' - normal plotting, done at the end of the job\n"
+              "- 'Progressive' - each shape is plotted after it is generated")
+        )
+        grid0.addWidget(plotting_label, 20, 0)
+        grid0.addWidget(self.paint_plotting_radio, 20, 1)
+
+        self.layout.addStretch()

+ 156 - 0
appGUI/preferences/tools/ToolsPanelizePrefGroupUI.py

@@ -0,0 +1,156 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCSpinner, RadioSet, FCCheckBox
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsPanelizePrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Cutout Tool Options", parent=parent)
+        super(ToolsPanelizePrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Panelize Tool Options")))
+        self.decimals = decimals
+
+        # ## Board cuttout
+        self.panelize_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.panelize_label.setToolTip(
+            _("Create an object that contains an array of (x, y) elements,\n"
+              "each element is a copy of the source object spaced\n"
+              "at a X distance, Y distance of each other.")
+        )
+        self.layout.addWidget(self.panelize_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        # ## Spacing Columns
+        self.pspacing_columns = FCDoubleSpinner()
+        self.pspacing_columns.set_range(0.000001, 10000.0000)
+        self.pspacing_columns.set_precision(self.decimals)
+        self.pspacing_columns.setSingleStep(0.1)
+
+        self.spacing_columns_label = QtWidgets.QLabel('%s:' % _("Spacing cols"))
+        self.spacing_columns_label.setToolTip(
+            _("Spacing between columns of the desired panel.\n"
+              "In current units.")
+        )
+        grid0.addWidget(self.spacing_columns_label, 0, 0)
+        grid0.addWidget(self.pspacing_columns, 0, 1)
+
+        # ## Spacing Rows
+        self.pspacing_rows = FCDoubleSpinner()
+        self.pspacing_rows.set_range(0.000001, 10000.0000)
+        self.pspacing_rows.set_precision(self.decimals)
+        self.pspacing_rows.setSingleStep(0.1)
+
+        self.spacing_rows_label = QtWidgets.QLabel('%s:' % _("Spacing rows"))
+        self.spacing_rows_label.setToolTip(
+            _("Spacing between rows of the desired panel.\n"
+              "In current units.")
+        )
+        grid0.addWidget(self.spacing_rows_label, 1, 0)
+        grid0.addWidget(self.pspacing_rows, 1, 1)
+
+        # ## Columns
+        self.pcolumns = FCSpinner()
+        self.pcolumns.set_range(1, 1000)
+        self.pcolumns.set_step(1)
+
+        self.columns_label = QtWidgets.QLabel('%s:' % _("Columns"))
+        self.columns_label.setToolTip(
+            _("Number of columns of the desired panel")
+        )
+        grid0.addWidget(self.columns_label, 2, 0)
+        grid0.addWidget(self.pcolumns, 2, 1)
+
+        # ## Rows
+        self.prows = FCSpinner()
+        self.prows.set_range(1, 1000)
+        self.prows.set_step(1)
+
+        self.rows_label = QtWidgets.QLabel('%s:' % _("Rows"))
+        self.rows_label.setToolTip(
+            _("Number of rows of the desired panel")
+        )
+        grid0.addWidget(self.rows_label, 3, 0)
+        grid0.addWidget(self.prows, 3, 1)
+
+        # ## Type of resulting Panel object
+        self.panel_type_radio = RadioSet([{'label': _('Gerber'), 'value': 'gerber'},
+                                          {'label': _('Geo'), 'value': 'geometry'}])
+        self.panel_type_label = QtWidgets.QLabel('%s:' % _("Panel Type"))
+        self.panel_type_label.setToolTip(
+           _("Choose the type of object for the panel object:\n"
+             "- Gerber\n"
+             "- Geometry")
+        )
+
+        grid0.addWidget(self.panel_type_label, 4, 0)
+        grid0.addWidget(self.panel_type_radio, 4, 1)
+
+        # Path optimization
+        self.poptimization_cb = FCCheckBox('%s' % _("Path Optimization"))
+        self.poptimization_cb.setToolTip(
+            _("Active only for Geometry panel type.\n"
+              "When checked the application will find\n"
+              "any two overlapping Line elements in the panel\n"
+              "and will remove the overlapping parts, keeping only one of them.")
+        )
+        grid0.addWidget(self.poptimization_cb, 5, 0, 1, 2)
+
+        # ## Constrains
+        self.pconstrain_cb = FCCheckBox('%s:' % _("Constrain within"))
+        self.pconstrain_cb.setToolTip(
+            _("Area define by DX and DY within to constrain the panel.\n"
+              "DX and DY values are in current units.\n"
+              "Regardless of how many columns and rows are desired,\n"
+              "the final panel will have as many columns and rows as\n"
+              "they fit completely within selected area.")
+        )
+        grid0.addWidget(self.pconstrain_cb, 10, 0, 1, 2)
+
+        self.px_width_entry = FCDoubleSpinner()
+        self.px_width_entry.set_range(0.000001, 10000.0000)
+        self.px_width_entry.set_precision(self.decimals)
+        self.px_width_entry.setSingleStep(0.1)
+
+        self.x_width_lbl = QtWidgets.QLabel('%s:' % _("Width (DX)"))
+        self.x_width_lbl.setToolTip(
+            _("The width (DX) within which the panel must fit.\n"
+              "In current units.")
+        )
+        grid0.addWidget(self.x_width_lbl, 12, 0)
+        grid0.addWidget(self.px_width_entry, 12, 1)
+
+        self.py_height_entry = FCDoubleSpinner()
+        self.py_height_entry.set_range(0.000001, 10000.0000)
+        self.py_height_entry.set_precision(self.decimals)
+        self.py_height_entry.setSingleStep(0.1)
+
+        self.y_height_lbl = QtWidgets.QLabel('%s:' % _("Height (DY)"))
+        self.y_height_lbl.setToolTip(
+            _("The height (DY)within which the panel must fit.\n"
+              "In current units.")
+        )
+        grid0.addWidget(self.y_height_lbl, 17, 0)
+        grid0.addWidget(self.py_height_entry, 17, 1)
+
+        self.layout.addStretch()

+ 111 - 0
appGUI/preferences/tools/ToolsPreferencesUI.py

@@ -0,0 +1,111 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.preferences.tools.ToolsSubPrefGroupUI import ToolsSubPrefGroupUI
+from appGUI.preferences.tools.ToolsSolderpastePrefGroupUI import ToolsSolderpastePrefGroupUI
+from appGUI.preferences.tools.ToolsCornersPrefGroupUI import ToolsCornersPrefGroupUI
+from appGUI.preferences.tools.ToolsTransformPrefGroupUI import ToolsTransformPrefGroupUI
+from appGUI.preferences.tools.ToolsCalculatorsPrefGroupUI import ToolsCalculatorsPrefGroupUI
+from appGUI.preferences.tools.ToolsPanelizePrefGroupUI import ToolsPanelizePrefGroupUI
+from appGUI.preferences.tools.ToolsFilmPrefGroupUI import ToolsFilmPrefGroupUI
+from appGUI.preferences.tools.Tools2sidedPrefGroupUI import Tools2sidedPrefGroupUI
+
+from appGUI.preferences.tools.ToolsCutoutPrefGroupUI import ToolsCutoutPrefGroupUI
+from appGUI.preferences.tools.ToolsNCCPrefGroupUI import ToolsNCCPrefGroupUI
+from appGUI.preferences.tools.ToolsPaintPrefGroupUI import ToolsPaintPrefGroupUI
+from appGUI.preferences.tools.ToolsISOPrefGroupUI import ToolsISOPrefGroupUI
+from appGUI.preferences.tools.ToolsDrillPrefGroupUI import ToolsDrillPrefGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsPreferencesUI(QtWidgets.QWidget):
+
+    def __init__(self, decimals, parent=None):
+        QtWidgets.QWidget.__init__(self, parent=parent)
+        self.layout = QtWidgets.QHBoxLayout()
+        self.setLayout(self.layout)
+        self.decimals = decimals
+
+        self.tools_iso_group = ToolsISOPrefGroupUI(decimals=self.decimals)
+        self.tools_iso_group.setMinimumWidth(220)
+
+        self.tools_drill_group = ToolsDrillPrefGroupUI(decimals=self.decimals)
+        self.tools_drill_group.setMinimumWidth(220)
+
+        self.tools_ncc_group = ToolsNCCPrefGroupUI(decimals=self.decimals)
+        self.tools_ncc_group.setMinimumWidth(220)
+
+        self.tools_paint_group = ToolsPaintPrefGroupUI(decimals=self.decimals)
+        self.tools_paint_group.setMinimumWidth(220)
+
+        self.tools_cutout_group = ToolsCutoutPrefGroupUI(decimals=self.decimals)
+        self.tools_cutout_group.setMinimumWidth(220)
+
+        self.tools_2sided_group = Tools2sidedPrefGroupUI(decimals=self.decimals)
+        self.tools_2sided_group.setMinimumWidth(220)
+
+        self.tools_film_group = ToolsFilmPrefGroupUI(decimals=self.decimals)
+        self.tools_film_group.setMinimumWidth(220)
+
+        self.tools_panelize_group = ToolsPanelizePrefGroupUI(decimals=self.decimals)
+        self.tools_panelize_group.setMinimumWidth(220)
+
+        self.tools_calculators_group = ToolsCalculatorsPrefGroupUI(decimals=self.decimals)
+        self.tools_calculators_group.setMinimumWidth(220)
+
+        self.tools_transform_group = ToolsTransformPrefGroupUI(decimals=self.decimals)
+        self.tools_transform_group.setMinimumWidth(200)
+
+        self.tools_solderpaste_group = ToolsSolderpastePrefGroupUI(decimals=self.decimals)
+        self.tools_solderpaste_group.setMinimumWidth(200)
+
+        self.tools_corners_group = ToolsCornersPrefGroupUI(decimals=self.decimals)
+        self.tools_corners_group.setMinimumWidth(200)
+
+        self.tools_sub_group = ToolsSubPrefGroupUI(decimals=self.decimals)
+        self.tools_sub_group.setMinimumWidth(200)
+
+        self.vlay = QtWidgets.QVBoxLayout()
+
+        self.vlay.addWidget(self.tools_iso_group)
+        self.vlay.addWidget(self.tools_2sided_group)
+        self.vlay.addWidget(self.tools_cutout_group)
+
+        self.vlay1 = QtWidgets.QVBoxLayout()
+        self.vlay1.addWidget(self.tools_drill_group)
+        self.vlay1.addWidget(self.tools_panelize_group)
+
+        self.vlay2 = QtWidgets.QVBoxLayout()
+        self.vlay2.addWidget(self.tools_ncc_group)
+        self.vlay2.addWidget(self.tools_paint_group)
+
+        self.vlay3 = QtWidgets.QVBoxLayout()
+        self.vlay3.addWidget(self.tools_film_group)
+        self.vlay3.addWidget(self.tools_transform_group)
+
+        self.vlay4 = QtWidgets.QVBoxLayout()
+        self.vlay4.addWidget(self.tools_solderpaste_group)
+        self.vlay4.addWidget(self.tools_corners_group)
+        self.vlay4.addWidget(self.tools_calculators_group)
+        self.vlay4.addWidget(self.tools_sub_group)
+
+        self.layout.addLayout(self.vlay)
+        self.layout.addLayout(self.vlay1)
+        self.layout.addLayout(self.vlay2)
+        self.layout.addLayout(self.vlay3)
+        self.layout.addLayout(self.vlay4)
+
+        self.layout.addStretch()

+ 246 - 0
appGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py

@@ -0,0 +1,246 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCSpinner, FCComboBox, NumericalEvalTupleEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsSolderpastePrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(ToolsSolderpastePrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("SolderPaste Tool Options")))
+        self.decimals = decimals
+
+        # ## Solder Paste Dispensing
+        self.solderpastelabel = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.solderpastelabel.setToolTip(
+            _("A tool to create GCode for dispensing\n"
+              "solder paste onto a PCB.")
+        )
+        self.layout.addWidget(self.solderpastelabel)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+
+        # Nozzle Tool Diameters
+        nozzletdlabel = QtWidgets.QLabel('<b><font color="green">%s:</font></b>' % _('Tools Dia'))
+        nozzletdlabel.setToolTip(
+            _("Diameters of the tools, separated by comma.\n"
+              "The value of the diameter has to use the dot decimals separator.\n"
+              "Valid values: 0.3, 1.0")
+        )
+        self.nozzle_tool_dia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
+        grid0.addWidget(nozzletdlabel, 0, 0)
+        grid0.addWidget(self.nozzle_tool_dia_entry, 0, 1)
+
+        # New Nozzle Tool Dia
+        self.addtool_entry_lbl = QtWidgets.QLabel('<b>%s:</b>' % _('New Nozzle Dia'))
+        self.addtool_entry_lbl.setToolTip(
+            _("Diameter for the new tool to add in the Tool Table")
+        )
+        self.addtool_entry = FCDoubleSpinner()
+        self.addtool_entry.set_precision(self.decimals)
+        self.addtool_entry.set_range(0.0000001, 10000.0000)
+        self.addtool_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.addtool_entry_lbl, 1, 0)
+        grid0.addWidget(self.addtool_entry, 1, 1)
+
+        # Z dispense start
+        self.z_start_entry = FCDoubleSpinner()
+        self.z_start_entry.set_precision(self.decimals)
+        self.z_start_entry.set_range(0.0000001, 10000.0000)
+        self.z_start_entry.setSingleStep(0.1)
+
+        self.z_start_label = QtWidgets.QLabel('%s:' % _("Z Dispense Start"))
+        self.z_start_label.setToolTip(
+            _("The height (Z) when solder paste dispensing starts.")
+        )
+        grid0.addWidget(self.z_start_label, 2, 0)
+        grid0.addWidget(self.z_start_entry, 2, 1)
+
+        # Z dispense
+        self.z_dispense_entry = FCDoubleSpinner()
+        self.z_dispense_entry.set_precision(self.decimals)
+        self.z_dispense_entry.set_range(0.0000001, 10000.0000)
+        self.z_dispense_entry.setSingleStep(0.1)
+
+        self.z_dispense_label = QtWidgets.QLabel('%s:' % _("Z Dispense"))
+        self.z_dispense_label.setToolTip(
+            _("The height (Z) when doing solder paste dispensing.")
+        )
+        grid0.addWidget(self.z_dispense_label, 3, 0)
+        grid0.addWidget(self.z_dispense_entry, 3, 1)
+
+        # Z dispense stop
+        self.z_stop_entry = FCDoubleSpinner()
+        self.z_stop_entry.set_precision(self.decimals)
+        self.z_stop_entry.set_range(0.0000001, 10000.0000)
+        self.z_stop_entry.setSingleStep(0.1)
+
+        self.z_stop_label = QtWidgets.QLabel('%s:' % _("Z Dispense Stop"))
+        self.z_stop_label.setToolTip(
+            _("The height (Z) when solder paste dispensing stops.")
+        )
+        grid0.addWidget(self.z_stop_label, 4, 0)
+        grid0.addWidget(self.z_stop_entry, 4, 1)
+
+        # Z travel
+        self.z_travel_entry = FCDoubleSpinner()
+        self.z_travel_entry.set_precision(self.decimals)
+        self.z_travel_entry.set_range(0.0000001, 10000.0000)
+        self.z_travel_entry.setSingleStep(0.1)
+
+        self.z_travel_label = QtWidgets.QLabel('%s:' % _("Z Travel"))
+        self.z_travel_label.setToolTip(
+            _("The height (Z) for travel between pads\n"
+              "(without dispensing solder paste).")
+        )
+        grid0.addWidget(self.z_travel_label, 5, 0)
+        grid0.addWidget(self.z_travel_entry, 5, 1)
+
+        # Z toolchange location
+        self.z_toolchange_entry = FCDoubleSpinner()
+        self.z_toolchange_entry.set_precision(self.decimals)
+        self.z_toolchange_entry.set_range(0.0000001, 10000.0000)
+        self.z_toolchange_entry.setSingleStep(0.1)
+
+        self.z_toolchange_label = QtWidgets.QLabel('%s:' % _("Z Toolchange"))
+        self.z_toolchange_label.setToolTip(
+            _("The height (Z) for tool (nozzle) change.")
+        )
+        grid0.addWidget(self.z_toolchange_label, 6, 0)
+        grid0.addWidget(self.z_toolchange_entry, 6, 1)
+
+        # X,Y Toolchange location
+        self.xy_toolchange_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+        self.xy_toolchange_label = QtWidgets.QLabel('%s:' % _("Toolchange X-Y"))
+        self.xy_toolchange_label.setToolTip(
+            _("The X,Y location for tool (nozzle) change.\n"
+              "The format is (x, y) where x and y are real numbers.")
+        )
+        grid0.addWidget(self.xy_toolchange_label, 7, 0)
+        grid0.addWidget(self.xy_toolchange_entry, 7, 1)
+
+        # Feedrate X-Y
+        self.frxy_entry = FCDoubleSpinner()
+        self.frxy_entry.set_precision(self.decimals)
+        self.frxy_entry.set_range(0.0000001, 910000.0000)
+        self.frxy_entry.setSingleStep(0.1)
+
+        self.frxy_label = QtWidgets.QLabel('%s:' % _("Feedrate X-Y"))
+        self.frxy_label.setToolTip(
+            _("Feedrate (speed) while moving on the X-Y plane.")
+        )
+        grid0.addWidget(self.frxy_label, 8, 0)
+        grid0.addWidget(self.frxy_entry, 8, 1)
+
+        # Feedrate Z
+        self.frz_entry = FCDoubleSpinner()
+        self.frz_entry.set_precision(self.decimals)
+        self.frz_entry.set_range(0.0000001, 910000.0000)
+        self.frz_entry.setSingleStep(0.1)
+
+        self.frz_label = QtWidgets.QLabel('%s:' % _("Feedrate Z"))
+        self.frz_label.setToolTip(
+            _("Feedrate (speed) while moving vertically\n"
+              "(on Z plane).")
+        )
+        grid0.addWidget(self.frz_label, 9, 0)
+        grid0.addWidget(self.frz_entry, 9, 1)
+
+        # Feedrate Z Dispense
+        self.frz_dispense_entry = FCDoubleSpinner()
+        self.frz_dispense_entry.set_precision(self.decimals)
+        self.frz_dispense_entry.set_range(0.0000001, 910000.0000)
+        self.frz_dispense_entry.setSingleStep(0.1)
+
+        self.frz_dispense_label = QtWidgets.QLabel('%s:' % _("Feedrate Z Dispense"))
+        self.frz_dispense_label.setToolTip(
+            _("Feedrate (speed) while moving up vertically\n"
+              "to Dispense position (on Z plane).")
+        )
+        grid0.addWidget(self.frz_dispense_label, 10, 0)
+        grid0.addWidget(self.frz_dispense_entry, 10, 1)
+
+        # Spindle Speed Forward
+        self.speedfwd_entry = FCSpinner()
+        self.speedfwd_entry.set_range(0, 99999)
+        self.speedfwd_entry.set_step(1000)
+
+        self.speedfwd_label = QtWidgets.QLabel('%s:' % _("Spindle Speed FWD"))
+        self.speedfwd_label.setToolTip(
+            _("The dispenser speed while pushing solder paste\n"
+              "through the dispenser nozzle.")
+        )
+        grid0.addWidget(self.speedfwd_label, 11, 0)
+        grid0.addWidget(self.speedfwd_entry, 11, 1)
+
+        # Dwell Forward
+        self.dwellfwd_entry = FCDoubleSpinner()
+        self.dwellfwd_entry.set_precision(self.decimals)
+        self.dwellfwd_entry.set_range(0.0000001, 10000.0000)
+        self.dwellfwd_entry.setSingleStep(0.1)
+
+        self.dwellfwd_label = QtWidgets.QLabel('%s:' % _("Dwell FWD"))
+        self.dwellfwd_label.setToolTip(
+            _("Pause after solder dispensing.")
+        )
+        grid0.addWidget(self.dwellfwd_label, 12, 0)
+        grid0.addWidget(self.dwellfwd_entry, 12, 1)
+
+        # Spindle Speed Reverse
+        self.speedrev_entry = FCSpinner()
+        self.speedrev_entry.set_range(0, 999999)
+        self.speedrev_entry.set_step(1000)
+
+        self.speedrev_label = QtWidgets.QLabel('%s:' % _("Spindle Speed REV"))
+        self.speedrev_label.setToolTip(
+            _("The dispenser speed while retracting solder paste\n"
+              "through the dispenser nozzle.")
+        )
+        grid0.addWidget(self.speedrev_label, 13, 0)
+        grid0.addWidget(self.speedrev_entry, 13, 1)
+
+        # Dwell Reverse
+        self.dwellrev_entry = FCDoubleSpinner()
+        self.dwellrev_entry.set_precision(self.decimals)
+        self.dwellrev_entry.set_range(0.0000001, 10000.0000)
+        self.dwellrev_entry.setSingleStep(0.1)
+
+        self.dwellrev_label = QtWidgets.QLabel('%s:' % _("Dwell REV"))
+        self.dwellrev_label.setToolTip(
+            _("Pause after solder paste dispenser retracted,\n"
+              "to allow pressure equilibrium.")
+        )
+        grid0.addWidget(self.dwellrev_label, 14, 0)
+        grid0.addWidget(self.dwellrev_entry, 14, 1)
+
+        # Preprocessors
+        pp_label = QtWidgets.QLabel('%s:' % _('Preprocessor'))
+        pp_label.setToolTip(
+            _("Files that control the GCode generation.")
+        )
+
+        self.pp_combo = FCComboBox()
+        grid0.addWidget(pp_label, 15, 0)
+        grid0.addWidget(self.pp_combo, 15, 1)
+
+        self.layout.addStretch()

+ 48 - 0
appGUI/preferences/tools/ToolsSubPrefGroupUI.py

@@ -0,0 +1,48 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCCheckBox
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsSubPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(ToolsSubPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Substractor Tool Options")))
+        self.decimals = decimals
+
+        # ## Subtractor Tool Parameters
+        self.sublabel = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.sublabel.setToolTip(
+            _("A tool to substract one Gerber or Geometry object\n"
+              "from another of the same type.")
+        )
+        self.layout.addWidget(self.sublabel)
+
+        self.close_paths_cb = FCCheckBox(_("Close paths"))
+        self.close_paths_cb.setToolTip(_("Checking this will close the paths cut by the subtractor object."))
+        self.layout.addWidget(self.close_paths_cb)
+
+        self.delete_sources_cb = FCCheckBox(_("Delete source"))
+        self.delete_sources_cb.setToolTip(
+            _("When checked will delete the source objects\n"
+              "after a successful operation.")
+        )
+        self.layout.addWidget(self.delete_sources_cb)
+        self.layout.addStretch()

+ 261 - 0
appGUI/preferences/tools/ToolsTransformPrefGroupUI.py

@@ -0,0 +1,261 @@
+from PyQt5 import QtWidgets, QtGui
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCDoubleSpinner, FCCheckBox, NumericalEvalTupleEntry, FCComboBox
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class ToolsTransformPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+
+        super(ToolsTransformPrefGroupUI, self).__init__(self, parent=parent)
+
+        self.setTitle(str(_("Transform Tool Options")))
+        self.decimals = decimals
+
+        # ## Transformations
+        self.transform_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
+        self.transform_label.setToolTip(
+            _("Various transformations that can be applied\n"
+              "on a application object.")
+        )
+        self.layout.addWidget(self.transform_label)
+
+        grid0 = QtWidgets.QGridLayout()
+        self.layout.addLayout(grid0)
+        grid0.setColumnStretch(0, 0)
+        grid0.setColumnStretch(1, 1)
+
+        # Reference Type
+        ref_label = QtWidgets.QLabel('%s:' % _("Reference"))
+        ref_label.setToolTip(
+            _("The reference point for Rotate, Skew, Scale, Mirror.\n"
+              "Can be:\n"
+              "- Origin -> it is the 0, 0 point\n"
+              "- Selection -> the center of the bounding box of the selected objects\n"
+              "- Point -> a custom point defined by X,Y coordinates\n"
+              "- Object -> the center of the bounding box of a specific object")
+        )
+        self.ref_combo = FCComboBox()
+        self.ref_items = [_("Origin"), _("Selection"), _("Point"), _("Object")]
+        self.ref_combo.addItems(self.ref_items)
+
+        grid0.addWidget(ref_label, 0, 0)
+        grid0.addWidget(self.ref_combo, 0, 1)
+
+        self.point_label = QtWidgets.QLabel('%s:' % _("Point"))
+        self.point_label.setToolTip(
+            _("A point of reference in format X,Y.")
+        )
+        self.point_entry = NumericalEvalTupleEntry()
+
+        grid0.addWidget(self.point_label, 1, 0)
+        grid0.addWidget(self.point_entry, 1, 1)
+
+        # Type of object to be used as reference
+        self.type_object_label = QtWidgets.QLabel('%s:' % _("Object"))
+        self.type_object_label.setToolTip(
+            _("The type of object used as reference.")
+        )
+
+        self.type_obj_combo = FCComboBox()
+        self.type_obj_combo.addItem(_("Gerber"))
+        self.type_obj_combo.addItem(_("Excellon"))
+        self.type_obj_combo.addItem(_("Geometry"))
+
+        self.type_obj_combo.setItemIcon(0, QtGui.QIcon(self.app.resource_location + "/flatcam_icon16.png"))
+        self.type_obj_combo.setItemIcon(1, QtGui.QIcon(self.app.resource_location + "/drill16.png"))
+        self.type_obj_combo.setItemIcon(2, QtGui.QIcon(self.app.resource_location + "/geometry16.png"))
+
+        grid0.addWidget(self.type_object_label, 3, 0)
+        grid0.addWidget(self.type_obj_combo, 3, 1)
+
+        # ## Rotate Angle
+        rotate_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Rotate"))
+        grid0.addWidget(rotate_title_lbl, 4, 0, 1, 2)
+
+        self.rotate_entry = FCDoubleSpinner()
+        self.rotate_entry.set_range(-360.0, 360.0)
+        self.rotate_entry.set_precision(self.decimals)
+        self.rotate_entry.setSingleStep(15)
+
+        self.rotate_label = QtWidgets.QLabel('%s:' % _("Angle"))
+        self.rotate_label.setToolTip(
+            _("Angle, in degrees.\n"
+              "Float number between -360 and 359.\n"
+              "Positive numbers for CW motion.\n"
+              "Negative numbers for CCW motion.")
+        )
+        grid0.addWidget(self.rotate_label, 6, 0)
+        grid0.addWidget(self.rotate_entry, 6, 1)
+
+        # ## Skew/Shear Angle on X axis
+        skew_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Skew"))
+        grid0.addWidget(skew_title_lbl, 8, 0)
+
+        # ## Link Skew factors
+        self.skew_link_cb = FCCheckBox()
+        self.skew_link_cb.setText(_("Link"))
+        self.skew_link_cb.setToolTip(
+            _("Link the Y entry to X entry and copy its content.")
+        )
+
+        grid0.addWidget(self.skew_link_cb, 8, 1)
+
+        self.skewx_entry = FCDoubleSpinner()
+        self.skewx_entry.set_range(-360.0, 360.0)
+        self.skewx_entry.set_precision(self.decimals)
+        self.skewx_entry.setSingleStep(0.1)
+
+        self.skewx_label = QtWidgets.QLabel('%s:' % _("X angle"))
+        self.skewx_label.setToolTip(
+            _("Angle, in degrees.\n"
+              "Float number between -360 and 359.")
+        )
+        grid0.addWidget(self.skewx_label, 9, 0)
+        grid0.addWidget(self.skewx_entry, 9, 1)
+
+        # ## Skew/Shear Angle on Y axis
+        self.skewy_entry = FCDoubleSpinner()
+        self.skewy_entry.set_range(-360.0, 360.0)
+        self.skewy_entry.set_precision(self.decimals)
+        self.skewy_entry.setSingleStep(0.1)
+
+        self.skewy_label = QtWidgets.QLabel('%s:' % _("Y angle"))
+        self.skewy_label.setToolTip(
+            _("Angle, in degrees.\n"
+              "Float number between -360 and 359.")
+        )
+        grid0.addWidget(self.skewy_label, 10, 0)
+        grid0.addWidget(self.skewy_entry, 10, 1)
+
+        # ## Scale
+        scale_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Scale"))
+        grid0.addWidget(scale_title_lbl, 12, 0)
+
+        # ## Link Scale factors
+        self.scale_link_cb = FCCheckBox(_("Link"))
+        self.scale_link_cb.setToolTip(
+            _("Link the Y entry to X entry and copy its content.")
+        )
+        grid0.addWidget(self.scale_link_cb, 12, 1)
+
+        self.scalex_entry = FCDoubleSpinner()
+        self.scalex_entry.set_range(0, 10000.0000)
+        self.scalex_entry.set_precision(self.decimals)
+        self.scalex_entry.setSingleStep(0.1)
+
+        self.scalex_label = QtWidgets.QLabel('%s:' % _("X factor"))
+        self.scalex_label.setToolTip(
+            _("Factor for scaling on X axis.")
+        )
+        grid0.addWidget(self.scalex_label, 14, 0)
+        grid0.addWidget(self.scalex_entry, 14, 1)
+
+        # ## Scale factor on X axis
+        self.scaley_entry = FCDoubleSpinner()
+        self.scaley_entry.set_range(0, 10000.0000)
+        self.scaley_entry.set_precision(self.decimals)
+        self.scaley_entry.setSingleStep(0.1)
+
+        self.scaley_label = QtWidgets.QLabel('%s:' % _("Y factor"))
+        self.scaley_label.setToolTip(
+            _("Factor for scaling on Y axis.")
+        )
+        grid0.addWidget(self.scaley_label, 16, 0)
+        grid0.addWidget(self.scaley_entry, 16, 1)
+
+        # ## Offset
+        offset_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Offset"))
+        grid0.addWidget(offset_title_lbl, 20, 0, 1, 2)
+
+        self.offx_entry = FCDoubleSpinner()
+        self.offx_entry.set_range(-10000.0000, 10000.0000)
+        self.offx_entry.set_precision(self.decimals)
+        self.offx_entry.setSingleStep(0.1)
+
+        self.offx_label = QtWidgets.QLabel('%s:' % _("X val"))
+        self.offx_label.setToolTip(
+           _("Distance to offset on X axis. In current units.")
+        )
+        grid0.addWidget(self.offx_label, 22, 0)
+        grid0.addWidget(self.offx_entry, 22, 1)
+
+        # ## Offset distance on Y axis
+        self.offy_entry = FCDoubleSpinner()
+        self.offy_entry.set_range(-10000.0000, 10000.0000)
+        self.offy_entry.set_precision(self.decimals)
+        self.offy_entry.setSingleStep(0.1)
+
+        self.offy_label = QtWidgets.QLabel('%s:' % _("Y val"))
+        self.offy_label.setToolTip(
+            _("Distance to offset on Y axis. In current units.")
+        )
+        grid0.addWidget(self.offy_label, 24, 0)
+        grid0.addWidget(self.offy_entry, 24, 1)
+
+        # ## Buffer
+        buffer_title_lbl = QtWidgets.QLabel('<b>%s</b>' % _("Buffer"))
+        grid0.addWidget(buffer_title_lbl, 26, 0)
+
+        self.buffer_rounded_cb = FCCheckBox()
+        self.buffer_rounded_cb.setText('%s' % _("Rounded"))
+        self.buffer_rounded_cb.setToolTip(
+            _("If checked then the buffer will surround the buffered shape,\n"
+              "every corner will be rounded.\n"
+              "If not checked then the buffer will follow the exact geometry\n"
+              "of the buffered shape.")
+        )
+
+        grid0.addWidget(self.buffer_rounded_cb, 26, 1)
+
+        self.buffer_label = QtWidgets.QLabel('%s:' % _("Distance"))
+        self.buffer_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased with the 'distance'.")
+        )
+
+        self.buffer_entry = FCDoubleSpinner()
+        self.buffer_entry.set_precision(self.decimals)
+        self.buffer_entry.setSingleStep(0.1)
+        self.buffer_entry.setWrapping(True)
+        self.buffer_entry.set_range(-10000.0000, 10000.0000)
+
+        grid0.addWidget(self.buffer_label, 28, 0)
+        grid0.addWidget(self.buffer_entry, 28, 1)
+
+        self.buffer_factor_label = QtWidgets.QLabel('%s:' % _("Value"))
+        self.buffer_factor_label.setToolTip(
+            _("A positive value will create the effect of dilation,\n"
+              "while a negative value will create the effect of erosion.\n"
+              "Each geometry element of the object will be increased\n"
+              "or decreased to fit the 'Value'. Value is a percentage\n"
+              "of the initial dimension.")
+        )
+
+        self.buffer_factor_entry = FCDoubleSpinner(suffix='%')
+        self.buffer_factor_entry.set_range(-100.0000, 1000.0000)
+        self.buffer_factor_entry.set_precision(self.decimals)
+        self.buffer_factor_entry.setWrapping(True)
+        self.buffer_factor_entry.setSingleStep(1)
+
+        grid0.addWidget(self.buffer_factor_label, 30, 0)
+        grid0.addWidget(self.buffer_factor_entry, 30, 1)
+
+        self.layout.addStretch()

+ 0 - 0
appGUI/preferences/tools/__init__.py


+ 83 - 0
appGUI/preferences/utilities/AutoCompletePrefGroupUI.py

@@ -0,0 +1,83 @@
+from PyQt5 import QtWidgets, QtGui
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import FCButton, FCTextArea, FCEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class AutoCompletePrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Gerber File associations Preferences", parent=None)
+        super().__init__(self, parent=parent)
+
+        self.setTitle(str(_("Autocompleter Keywords")))
+        self.decimals = decimals
+
+        self.restore_btn = FCButton(_("Restore"))
+        self.restore_btn.setToolTip(_("Restore the autocompleter keywords list to the default state."))
+        self.del_all_btn = FCButton(_("Delete All"))
+        self.del_all_btn.setToolTip(_("Delete all autocompleter keywords from the list."))
+
+        hlay0 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay0)
+        hlay0.addWidget(self.restore_btn)
+        hlay0.addWidget(self.del_all_btn)
+
+        # ## Gerber associations
+        self.grb_list_label = QtWidgets.QLabel("<b>%s:</b>" % _("Keywords list"))
+        self.grb_list_label.setToolTip(
+            _("List of keywords used by\n"
+              "the autocompleter in FlatCAM.\n"
+              "The autocompleter is installed\n"
+              "in the Code Editor and for the Tcl Shell.")
+        )
+        self.layout.addWidget(self.grb_list_label)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("textbox_font_size"):
+            tb_fsize = qsettings.value('textbox_font_size', type=int)
+        else:
+            tb_fsize = 10
+
+        self.kw_list_text = FCTextArea()
+        self.kw_list_text.setReadOnly(True)
+        # self.grb_list_text.sizeHint(custom_sizehint=150)
+        self.layout.addWidget(self.kw_list_text)
+        font = QtGui.QFont()
+        font.setPointSize(tb_fsize)
+        self.kw_list_text.setFont(font)
+
+        self.kw_label = QtWidgets.QLabel('%s:' % _("Extension"))
+        self.kw_label.setToolTip(_("A keyword to be added or deleted to the list."))
+        self.kw_entry = FCEntry()
+
+        hlay1 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay1)
+        hlay1.addWidget(self.kw_label)
+        hlay1.addWidget(self.kw_entry)
+
+        self.add_btn = FCButton(_("Add keyword"))
+        self.add_btn.setToolTip(_("Add a keyword to the list"))
+        self.del_btn = FCButton(_("Delete keyword"))
+        self.del_btn.setToolTip(_("Delete a keyword from the list"))
+
+        hlay2 = QtWidgets.QHBoxLayout()
+        self.layout.addLayout(hlay2)
+        hlay2.addWidget(self.add_btn)
+        hlay2.addWidget(self.del_btn)
+
+        # self.layout.addStretch()

+ 102 - 0
appGUI/preferences/utilities/FAExcPrefGroupUI.py

@@ -0,0 +1,102 @@
+from PyQt5 import QtWidgets, QtGui
+from PyQt5.QtCore import QSettings
+
+from appGUI.GUIElements import VerticalScrollArea, FCButton, FCTextArea, FCEntry
+from appGUI.preferences.OptionsGroupUI import OptionsGroupUI
+
+import gettext
+import appTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+settings = QSettings("Open Source", "FlatCAM")
+if settings.contains("machinist"):
+    machinist_setting = settings.value('machinist', type=int)
+else:
+    machinist_setting = 0
+
+
+class FAExcPrefGroupUI(OptionsGroupUI):
+    def __init__(self, decimals=4, parent=None):
+        # OptionsGroupUI.__init__(self, "Excellon File associations Preferences", parent=None)
+        super().__init__(self, parent=parent)
+
+        self.setTitle(str(_("Excellon File associations")))
+        self.decimals = decimals
+
+        self.layout.setContentsMargins(2, 2, 2, 2)
+
+        self.vertical_lay = QtWidgets.QVBoxLayout()
+        scroll_widget = QtWidgets.QWidget()
+
+        scroll = VerticalScrollArea()
+        scroll.setWidget(scroll_widget)
+        scroll.setWidgetResizable(True)
+        scroll.setFrameShape(QtWidgets.QFrame.NoFrame)
+
+        self.restore_btn = FCButton(_("Restore"))
+        self.restore_btn.setToolTip(_("Restore the extension list to the default state."))
+        self.del_all_btn = FCButton(_("Delete All"))
+        self.del_all_btn.setToolTip(_("Delete all extensions from the list."))
+
+        hlay0 = QtWidgets.QHBoxLayout()
+        hlay0.addWidget(self.restore_btn)
+        hlay0.addWidget(self.del_all_btn)
+        self.vertical_lay.addLayout(hlay0)
+
+        # # ## Excellon associations
+        list_label = QtWidgets.QLabel("<b>%s:</b>" % _("Extensions list"))
+        list_label.setToolTip(
+            _("List of file extensions to be\n"
+              "associated with FlatCAM.")
+        )
+        self.vertical_lay.addWidget(list_label)
+
+        qsettings = QSettings("Open Source", "FlatCAM")
+        if qsettings.contains("textbox_font_size"):
+            tb_fsize = qsettings.value('textbox_font_size', type=int)
+        else:
+            tb_fsize = 10
+
+        self.exc_list_text = FCTextArea()
+        self.exc_list_text.setReadOnly(True)
+        # self.exc_list_text.sizeHint(custom_sizehint=150)
+        font = QtGui.QFont()
+        font.setPointSize(tb_fsize)
+        self.exc_list_text.setFont(font)
+
+        self.vertical_lay.addWidget(self.exc_list_text)
+
+        self.ext_label = QtWidgets.QLabel('%s:' % _("Extension"))
+        self.ext_label.setToolTip(_("A file extension to be added or deleted to the list."))
+        self.ext_entry = FCEntry()
+
+        hlay1 = QtWidgets.QHBoxLayout()
+        self.vertical_lay.addLayout(hlay1)
+        hlay1.addWidget(self.ext_label)
+        hlay1.addWidget(self.ext_entry)
+
+        self.add_btn = FCButton(_("Add Extension"))
+        self.add_btn.setToolTip(_("Add a file extension to the list"))
+        self.del_btn = FCButton(_("Delete Extension"))
+        self.del_btn.setToolTip(_("Delete a file extension from the list"))
+
+        hlay2 = QtWidgets.QHBoxLayout()
+        self.vertical_lay.addLayout(hlay2)
+        hlay2.addWidget(self.add_btn)
+        hlay2.addWidget(self.del_btn)
+
+        self.exc_list_btn = FCButton(_("Apply Association"))
+        self.exc_list_btn.setToolTip(_("Apply the file associations between\n"
+                                       "FlatCAM and the files with above extensions.\n"
+                                       "They will be active after next logon.\n"
+                                       "This work only in Windows."))
+        self.vertical_lay.addWidget(self.exc_list_btn)
+
+        scroll_widget.setLayout(self.vertical_lay)
+        self.layout.addWidget(scroll)
+
+        # self.vertical_lay.addStretch()

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä