ソースを参照

Merged in isolation_tool (pull request #11)

Isolation tool
Marius Stanciu 5 年 前
コミット
58707031a7
100 ファイル変更4128 行追加3531 行削除
  1. 196 7
      AppDatabase.py
  2. 29 48
      AppEditors/FlatCAMExcEditor.py
  3. 69 73
      AppEditors/FlatCAMGeoEditor.py
  4. 120 126
      AppEditors/FlatCAMGrbEditor.py
  5. 4 4
      AppEditors/FlatCAMTextEditor.py
  6. 0 0
      AppEditors/__init__.py
  7. 298 19
      AppGUI/GUIElements.py
  8. 122 899
      AppGUI/MainGUI.py
  9. 274 383
      AppGUI/ObjectUI.py
  10. 153 14
      AppGUI/PlotCanvas.py
  11. 235 11
      AppGUI/PlotCanvasLegacy.py
  12. 1 0
      AppGUI/VisPyCanvas.py
  13. 0 0
      AppGUI/VisPyData/data/fonts/opensans-regular.ttf
  14. 0 0
      AppGUI/VisPyData/data/freetype/freetype253.dll
  15. 0 0
      AppGUI/VisPyData/data/freetype/freetype253_x64.dll
  16. 0 0
      AppGUI/VisPyPatches.py
  17. 0 0
      AppGUI/VisPyTesselators.py
  18. 1 1
      AppGUI/VisPyVisuals.py
  19. 0 0
      AppGUI/__init__.py
  20. 0 0
      AppGUI/preferences/OptionsGroupUI.py
  21. 183 112
      AppGUI/preferences/PreferencesUIManager.py
  22. 2 2
      AppGUI/preferences/__init__.py
  23. 3 3
      AppGUI/preferences/cncjob/CNCJobAdvOptPrefGroupUI.py
  24. 3 3
      AppGUI/preferences/cncjob/CNCJobGenPrefGroupUI.py
  25. 3 3
      AppGUI/preferences/cncjob/CNCJobOptPrefGroupUI.py
  26. 3 3
      AppGUI/preferences/cncjob/CNCJobPreferencesUI.py
  27. 0 0
      AppGUI/preferences/cncjob/__init__.py
  28. 5 5
      AppGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py
  29. 3 3
      AppGUI/preferences/excellon/ExcellonEditorPrefGroupUI.py
  30. 3 3
      AppGUI/preferences/excellon/ExcellonExpPrefGroupUI.py
  31. 121 4
      AppGUI/preferences/excellon/ExcellonGenPrefGroupUI.py
  32. 6 6
      AppGUI/preferences/excellon/ExcellonOptPrefGroupUI.py
  33. 7 7
      AppGUI/preferences/excellon/ExcellonPreferencesUI.py
  34. 0 0
      AppGUI/preferences/excellon/__init__.py
  35. 26 15
      AppGUI/preferences/general/GeneralAPPSetGroupUI.py
  36. 5 6
      AppGUI/preferences/general/GeneralAppPrefGroupUI.py
  37. 20 47
      AppGUI/preferences/general/GeneralGUIPrefGroupUI.py
  38. 4 4
      AppGUI/preferences/general/GeneralPreferencesUI.py
  39. 0 0
      AppGUI/preferences/general/__init__.py
  40. 20 12
      AppGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py
  41. 3 3
      AppGUI/preferences/geometry/GeometryEditorPrefGroupUI.py
  42. 14 4
      AppGUI/preferences/geometry/GeometryGenPrefGroupUI.py
  43. 6 5
      AppGUI/preferences/geometry/GeometryOptPrefGroupUI.py
  44. 5 5
      AppGUI/preferences/geometry/GeometryPreferencesUI.py
  45. 0 0
      AppGUI/preferences/geometry/__init__.py
  46. 3 82
      AppGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py
  47. 5 4
      AppGUI/preferences/gerber/GerberEditorPrefGroupUI.py
  48. 3 3
      AppGUI/preferences/gerber/GerberExpPrefGroupUI.py
  49. 12 12
      AppGUI/preferences/gerber/GerberGenPrefGroupUI.py
  50. 100 0
      AppGUI/preferences/gerber/GerberOptPrefGroupUI.py
  51. 7 6
      AppGUI/preferences/gerber/GerberPreferencesUI.py
  52. 0 0
      AppGUI/preferences/gerber/__init__.py
  53. 3 3
      AppGUI/preferences/tools/Tools2CThievingPrefGroupUI.py
  54. 4 4
      AppGUI/preferences/tools/Tools2CalPrefGroupUI.py
  55. 3 3
      AppGUI/preferences/tools/Tools2EDrillsPrefGroupUI.py
  56. 3 3
      AppGUI/preferences/tools/Tools2FiducialsPrefGroupUI.py
  57. 3 3
      AppGUI/preferences/tools/Tools2InvertPrefGroupUI.py
  58. 3 3
      AppGUI/preferences/tools/Tools2OptimalPrefGroupUI.py
  59. 10 10
      AppGUI/preferences/tools/Tools2PreferencesUI.py
  60. 3 3
      AppGUI/preferences/tools/Tools2PunchGerberPrefGroupUI.py
  61. 68 4
      AppGUI/preferences/tools/Tools2QRCodePrefGroupUI.py
  62. 3 3
      AppGUI/preferences/tools/Tools2RulesCheckPrefGroupUI.py
  63. 3 3
      AppGUI/preferences/tools/Tools2sidedPrefGroupUI.py
  64. 3 3
      AppGUI/preferences/tools/ToolsCalculatorsPrefGroupUI.py
  65. 81 0
      AppGUI/preferences/tools/ToolsCornersPrefGroupUI.py
  66. 4 4
      AppGUI/preferences/tools/ToolsCutoutPrefGroupUI.py
  67. 36 4
      AppGUI/preferences/tools/ToolsFilmPrefGroupUI.py
  68. 319 0
      AppGUI/preferences/tools/ToolsISOPrefGroupUI.py
  69. 12 12
      AppGUI/preferences/tools/ToolsNCCPrefGroupUI.py
  70. 10 10
      AppGUI/preferences/tools/ToolsPaintPrefGroupUI.py
  71. 3 3
      AppGUI/preferences/tools/ToolsPanelizePrefGroupUI.py
  72. 23 12
      AppGUI/preferences/tools/ToolsPreferencesUI.py
  73. 5 5
      AppGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py
  74. 3 3
      AppGUI/preferences/tools/ToolsSubPrefGroupUI.py
  75. 4 4
      AppGUI/preferences/tools/ToolsTransformPrefGroupUI.py
  76. 0 0
      AppGUI/preferences/tools/__init__.py
  77. 3 3
      AppGUI/preferences/utilities/AutoCompletePrefGroupUI.py
  78. 3 3
      AppGUI/preferences/utilities/FAExcPrefGroupUI.py
  79. 3 3
      AppGUI/preferences/utilities/FAGcoPrefGroupUI.py
  80. 3 3
      AppGUI/preferences/utilities/FAGrbPrefGroupUI.py
  81. 4 4
      AppGUI/preferences/utilities/UtilPreferencesUI.py
  82. 0 0
      AppGUI/preferences/utilities/__init__.py
  83. 394 0
      AppObjects/AppObject.py
  84. 17 14
      AppObjects/FlatCAMCNCJob.py
  85. 24 3
      AppObjects/FlatCAMDocument.py
  86. 367 42
      AppObjects/FlatCAMExcellon.py
  87. 269 283
      AppObjects/FlatCAMGeometry.py
  88. 27 686
      AppObjects/FlatCAMGerber.py
  89. 27 29
      AppObjects/FlatCAMObj.py
  90. 33 7
      AppObjects/FlatCAMScript.py
  91. 216 16
      AppObjects/ObjectCollection.py
  92. 0 0
      AppObjects/__init__.py
  93. 2 2
      AppParsers/ParseDXF.py
  94. 0 0
      AppParsers/ParseDXF_Spline.py
  95. 15 16
      AppParsers/ParseExcellon.py
  96. 1 1
      AppParsers/ParseFont.py
  97. 6 7
      AppParsers/ParseGerber.py
  98. 2 3
      AppParsers/ParseHPGL2.py
  99. 25 356
      AppParsers/ParsePDF.py
  100. 1 1
      AppParsers/ParseSVG.py

+ 196 - 7
FlatCAMDB.py → AppDatabase.py

@@ -1,5 +1,5 @@
 from PyQt5 import QtGui, QtCore, QtWidgets
-from flatcamGUI.GUIElements import FCTable, FCEntry, FCButton, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, \
+from AppGUI.GUIElements import FCTable, FCEntry, FCButton, FCDoubleSpinner, FCComboBox, FCCheckBox, FCSpinner, \
     FCTree, RadioSet, FCFileSaveDialog
 from camlib import to_dict
 
@@ -8,8 +8,10 @@ import json
 
 from copy import deepcopy
 from datetime import datetime
+import math
+
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -655,7 +657,7 @@ class ToolsDB(QtWidgets.QWidget):
                                                                l_save=str(self.app.get_last_save_folder()),
                                                                n=_("Tools_Database"),
                                                                date=date),
-                                                           filter=filter__)
+                                                           ext_filter=filter__)
 
         filename = str(filename)
 
@@ -1030,6 +1032,7 @@ class ToolsDB2(QtWidgets.QWidget):
         self.advanced_box.setTitle(_("Advanced Geo Parameters"))
         self.advanced_box.setFixedWidth(250)
 
+        # NCC TOOL BOX
         self.ncc_box = QtWidgets.QGroupBox()
         self.ncc_box.setStyleSheet("""
                         QGroupBox
@@ -1042,6 +1045,7 @@ class ToolsDB2(QtWidgets.QWidget):
         self.ncc_box.setTitle(_("NCC Parameters"))
         self.ncc_box.setFixedWidth(250)
 
+        # PAINT TOOL BOX
         self.paint_box = QtWidgets.QGroupBox()
         self.paint_box.setStyleSheet("""
                         QGroupBox
@@ -1054,10 +1058,24 @@ class ToolsDB2(QtWidgets.QWidget):
         self.paint_box.setTitle(_("Paint Parameters"))
         self.paint_box.setFixedWidth(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.setFixedWidth(250)
+
         self.basic_box.setLayout(self.basic_vlay)
         self.advanced_box.setLayout(self.advanced_vlay)
         self.ncc_box.setLayout(self.ncc_vlay)
         self.paint_box.setLayout(self.paint_vlay)
+        self.iso_box.setLayout(self.iso_vlay)
 
         geo_vlay = QtWidgets.QVBoxLayout()
         geo_vlay.addWidget(self.basic_box)
@@ -1067,6 +1085,7 @@ class ToolsDB2(QtWidgets.QWidget):
         tools_vlay = QtWidgets.QVBoxLayout()
         tools_vlay.addWidget(self.ncc_box)
         tools_vlay.addWidget(self.paint_box)
+        tools_vlay.addWidget(self.iso_box)
         tools_vlay.addStretch()
 
         param_hlay.addLayout(geo_vlay)
@@ -1478,7 +1497,7 @@ class ToolsDB2(QtWidgets.QWidget):
 
         self.ncc_method_combo = FCComboBox()
         self.ncc_method_combo.addItems(
-            [_("Standard"), _("Seed"), _("Lines")]
+            [_("Standard"), _("Seed"), _("Lines"), _("Combo")]
         )
         self.ncc_method_combo.setObjectName("gdb_n_method")
 
@@ -1621,6 +1640,101 @@ class ToolsDB2(QtWidgets.QWidget):
         self.grid3.addWidget(self.pathconnect_cb, 10, 0)
         self.grid3.addWidget(self.paintcontour_cb, 10, 1)
 
+        # ###########################################################################
+        # ############### Paint 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 = 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("gdb_i_passes")
+
+        self.grid4.addWidget(passlabel, 0, 0)
+        self.grid4.addWidget(self.passes_entry, 0, 1)
+
+        # Overlap Entry
+        overlabel = QtWidgets.QLabel('%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.milling_type_label = QtWidgets.QLabel('%s:' % _('Milling Type'))
+        self.milling_type_label.setToolTip(
+            _("Milling type when the selected tool is of type: 'iso_op':\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 when the selected tool is of type: 'iso_op':\n"
+              "- climb / best for precision milling and to reduce tool usage\n"
+              "- conventional / useful when there is no backlash compensation")
+        )
+        self.milling_type_radio.setObjectName("gdb_i_milling_type")
+
+        self.grid4.addWidget(self.milling_type_label, 4, 0)
+        self.grid4.addWidget(self.milling_type_radio, 4, 1)
+
+        # 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("gdb_i_follow")
+
+        self.grid4.addWidget(self.follow_label, 6, 0)
+        self.grid4.addWidget(self.follow_cb, 6, 1)
+
+        # 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("gdb_i_iso_type")
+
+        self.grid4.addWidget(self.iso_type_label, 8, 0)
+        self.grid4.addWidget(self.iso_type_radio, 8, 1)
+
         # ####################################################################
         # ####################################################################
         # GUI for the lower part of the window
@@ -1743,6 +1857,13 @@ class ToolsDB2(QtWidgets.QWidget):
             "tools_paintmethod":        self.paintmethod_combo,
             "tools_pathconnect":        self.pathconnect_cb,
             "tools_paintcontour":       self.paintcontour_cb,
+
+            # Isolation
+            "tools_iso_passes":         self.passes_entry,
+            "tools_iso_overlap":        self.iso_overlap_entry,
+            "tools_iso_milling_type":   self.milling_type_radio,
+            "tools_iso_follow":         self.follow_cb,
+            "tools_iso_isotype":        self.iso_type_radio
         }
 
         self.name2option = {
@@ -1787,6 +1908,13 @@ class ToolsDB2(QtWidgets.QWidget):
             'gdb_p_method':         "tools_paintmethod",
             'gdb_p_connect':        "tools_pathconnect",
             'gdb_p_contour':        "tools_paintcontour",
+
+            # 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"
         }
 
         self.current_toolid = None
@@ -1939,21 +2067,23 @@ class ToolsDB2(QtWidgets.QWidget):
             if self.db_tool_dict:
                 self.storage_to_form(self.db_tool_dict['1'])
 
-                # Enable GUI
+                # Enable AppGUI
                 self.basic_box.setEnabled(True)
                 self.advanced_box.setEnabled(True)
                 self.ncc_box.setEnabled(True)
                 self.paint_box.setEnabled(True)
+                self.iso_box.setEnabled(True)
 
                 self.tree_widget.setCurrentItem(self.tree_widget.topLevelItem(0))
                 # self.tree_widget.setFocus()
 
             else:
-                # Disable GUI
+                # Disable AppGUI
                 self.basic_box.setEnabled(False)
                 self.advanced_box.setEnabled(False)
                 self.ncc_box.setEnabled(False)
                 self.paint_box.setEnabled(False)
+                self.iso_box.setEnabled(False)
         else:
             self.storage_to_form(self.db_tool_dict[str(self.current_toolid)])
 
@@ -2006,6 +2136,13 @@ class ToolsDB2(QtWidgets.QWidget):
             "tools_paintmethod":        self.app.defaults["tools_paintmethod"],
             "tools_pathconnect":        self.app.defaults["tools_pathconnect"],
             "tools_paintcontour":       self.app.defaults["tools_paintcontour"],
+
+            # 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"],
         })
 
         dict_elem = {}
@@ -2117,7 +2254,7 @@ class ToolsDB2(QtWidgets.QWidget):
                                                                 l_save=str(self.app.get_last_save_folder()),
                                                                 n=_("Tools_Database"),
                                                                 date=date),
-                                                           filter=filter__)
+                                                           ext_filter=filter__)
 
         filename = str(filename)
 
@@ -2218,6 +2355,18 @@ class ToolsDB2(QtWidgets.QWidget):
         self.app.tools_db_changed_flag = False
         self.on_save_tools_db()
 
+    def on_calculate_tooldia(self):
+        if self.shape_combo.get_value() == 'V':
+            tip_dia = float(self.vdia_entry.get_value())
+            half_tip_angle = float(self.vangle_entry.get_value()) / 2.0
+            cut_z = float(self.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.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()
@@ -2247,12 +2396,40 @@ class ToolsDB2(QtWidgets.QWidget):
             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.shape_combo.currentIndexChanged.connect(self.on_calculate_tooldia)
+        self.cutz_entry.valueChanged.connect(self.on_calculate_tooldia)
+        self.vdia_entry.valueChanged.connect(self.on_calculate_tooldia)
+        self.vangle_entry.valueChanged.connect(self.on_calculate_tooldia)
+
+
     def ui_disconnect(self):
         try:
             self.name_entry.editingFinished.disconnect(self.update_tree_name)
         except (TypeError, AttributeError):
             pass
 
+        try:
+            self.shape_combo.currentIndexChanged.disconnect(self.on_calculate_tooldia)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.cutz_entry.valueChanged.disconnect(self.on_calculate_tooldia)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.vdia_entry.valueChanged.disconnect(self.on_calculate_tooldia)
+        except (TypeError, AttributeError):
+            pass
+
+        try:
+            self.vangle_entry.valueChanged.disconnect(self.on_calculate_tooldia)
+        except (TypeError, AttributeError):
+            pass
+
         for key in self.form_fields:
             wdg = self.form_fields[key]
 
@@ -2398,6 +2575,18 @@ class ToolsDB2(QtWidgets.QWidget):
             elif wdg_name == "gdb_p_contour":
                 self.db_tool_dict[tool_id]['data']['tools_paintcontour'] = 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
+
         self.callback_app()
 
     def on_tool_requested_from_app(self):

+ 29 - 48
flatcamEditors/FlatCAMExcEditor.py → AppEditors/FlatCAMExcEditor.py

@@ -9,9 +9,9 @@ from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 
 from camlib import distance, arc, FlatCAMRTreeStorage
-from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, RadioSet, FCSpinner
-from flatcamEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
-from flatcamParsers.ParseExcellon import Excellon
+from AppGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, RadioSet, FCSpinner
+from AppEditors.FlatCAMGeoEditor import FCShapeTool, DrawTool, DrawToolShape, DrawToolUtilityShape, FlatCAMGeoEditor
+from AppParsers.ParseExcellon import Excellon
 
 from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon, Point
 import shapely.affinity as affinity
@@ -26,7 +26,7 @@ import logging
 from copy import deepcopy
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -2123,7 +2123,7 @@ class FlatCAMExcEditor(QtCore.QObject):
             else:
                 self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1)
         else:
-            from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+            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')
 
@@ -2239,7 +2239,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         # store the status of the editor so the Delete at object level will not work until the edit is finished
         self.editor_active = False
-        log.debug("Initialization of the FlatCAM Excellon Editor is finished ...")
+        log.debug("Initialization of the Excellon Editor is finished ...")
 
     def pool_recreated(self, pool):
         self.shapes.pool = pool
@@ -2312,7 +2312,7 @@ class FlatCAMExcEditor(QtCore.QObject):
                 tool_dia = float('%.*f' % (self.decimals, v['C']))
                 self.tool2tooldia[int(k)] = tool_dia
 
-        # Init GUI
+        # Init AppGUI
         self.addtool_entry.set_value(float(self.app.defaults['excellon_editor_newdia']))
         self.drill_array_size_entry.set_value(int(self.app.defaults['excellon_editor_array_size']))
         self.drill_axis_radio.set_value(self.app.defaults['excellon_editor_lin_dir'])
@@ -2819,10 +2819,8 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.tool_shape.enabled = True
         # self.app.app_cursor.enabled = True
 
-        self.app.ui.snap_max_dist_entry.setEnabled(True)
-        self.app.ui.corner_snap_btn.setEnabled(True)
-        self.app.ui.snap_magnet.setVisible(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)
@@ -2832,12 +2830,11 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         self.app.ui.exc_edit_toolbar.setDisabled(False)
         self.app.ui.exc_edit_toolbar.setVisible(True)
-        # self.app.ui.snap_toolbar.setDisabled(False)
+        # self.app.ui.status_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.on_grid_snap_triggered(state=True)
 
         self.app.ui.popmenu_disable.setVisible(False)
         self.app.ui.cmenu_newmenu.menuAction().setVisible(False)
@@ -2869,30 +2866,8 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.clear()
         self.app.ui.exc_edit_toolbar.setDisabled(True)
 
-        settings = QSettings("Open Source", "FlatCAM")
-        if settings.contains("layout"):
-            layout = settings.value('layout', type=str)
-            if layout == 'standard':
-                # self.app.ui.exc_edit_toolbar.setVisible(False)
-
-                self.app.ui.snap_max_dist_entry.setEnabled(False)
-                self.app.ui.corner_snap_btn.setEnabled(False)
-                self.app.ui.snap_magnet.setVisible(False)
-                self.app.ui.corner_snap_btn.setVisible(False)
-            else:
-                # self.app.ui.exc_edit_toolbar.setVisible(True)
-
-                self.app.ui.snap_max_dist_entry.setEnabled(False)
-                self.app.ui.corner_snap_btn.setEnabled(False)
-                self.app.ui.snap_magnet.setVisible(True)
-                self.app.ui.corner_snap_btn.setVisible(True)
-        else:
-            # self.app.ui.exc_edit_toolbar.setVisible(False)
-
-            self.app.ui.snap_max_dist_entry.setEnabled(False)
-            self.app.ui.corner_snap_btn.setEnabled(False)
-            self.app.ui.snap_magnet.setVisible(False)
-            self.app.ui.corner_snap_btn.setVisible(False)
+        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 \
@@ -3068,7 +3043,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
         self.set_ui()
 
-        # now that we hava data, create the GUI interface and add it to the Tool Tab
+        # now that we hava 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
@@ -3361,15 +3336,17 @@ class FlatCAMExcEditor(QtCore.QObject):
         with self.app.proc_container.new(_("Creating Excellon.")):
 
             try:
-                edited_obj = self.app.new_object("excellon", outname, obj_init)
+                edited_obj = self.app.app_obj.new_object("excellon", outname, obj_init)
                 edited_obj.source_file = self.app.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):
@@ -3463,8 +3440,8 @@ class FlatCAMExcEditor(QtCore.QObject):
             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))
+            # 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:
@@ -3801,18 +3778,22 @@ class FlatCAMExcEditor(QtCore.QObject):
         self.snap_x = x
         self.snap_y = y
 
-        # update the position label in the infobar since the APP mouse event handlers are disconnected
-        self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
-                                           "<b>Y</b>: %.4f" % (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 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))
+        # # 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))
@@ -4045,7 +4026,7 @@ class FlatCAMExcEditor(QtCore.QObject):
 
     def select_tool(self, toolname):
         """
-        Selects a drawing tool. Impacts the object and GUI.
+        Selects a drawing tool. Impacts the object and AppGUI.
 
         :param toolname: Name of the tool.
         :return: None

+ 69 - 73
flatcamEditors/FlatCAMGeoEditor.py → AppEditors/FlatCAMGeoEditor.py

@@ -15,11 +15,10 @@ from PyQt5 import QtGui, QtCore, QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 
 from camlib import distance, arc, three_point_circle, Geometry, FlatCAMRTreeStorage
-from FlatCAMTool import FlatCAMTool
-from flatcamGUI.ObjectUI import RadioSet
-from flatcamGUI.GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FCComboBox, FCTextAreaRich, \
-    FCTable, FCDoubleSpinner, FCButton, EvalEntry2, FCInputDialog, FCTree
-from flatcamParsers.ParseFont import *
+from AppTool import AppTool
+from AppGUI.GUIElements import OptionalInputSection, FCCheckBox, FCEntry, FCComboBox, FCTextAreaRich, \
+    FCDoubleSpinner, FCButton, FCInputDialog, FCTree
+from AppParsers.ParseFont import *
 
 from shapely.geometry import LineString, LinearRing, MultiLineString, Polygon, MultiPolygon
 from shapely.ops import cascaded_union, unary_union, linemerge
@@ -34,7 +33,7 @@ from rtree import index as rtindex
 from copy import deepcopy
 # from vispy.io import read_png
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -42,7 +41,7 @@ if '_' not in builtins.__dict__:
     _ = gettext.gettext
 
 
-class BufferSelectionTool(FlatCAMTool):
+class BufferSelectionTool(AppTool):
     """
     Simple input for buffer distance.
     """
@@ -50,7 +49,7 @@ class BufferSelectionTool(FlatCAMTool):
     toolName = "Buffer Selection"
 
     def __init__(self, app, draw_app):
-        FlatCAMTool.__init__(self, app)
+        AppTool.__init__(self, app)
 
         self.draw_app = draw_app
         self.decimals = app.decimals
@@ -118,12 +117,12 @@ class BufferSelectionTool(FlatCAMTool):
         self.buffer_int_button.clicked.connect(self.on_buffer_int)
         self.buffer_ext_button.clicked.connect(self.on_buffer_ext)
 
-        # Init GUI
+        # Init AppGUI
         self.buffer_distance_entry.set_value(0.01)
 
     def run(self):
         self.app.defaults.report_usage("Geo Editor ToolBuffer()")
-        FlatCAMTool.run(self)
+        AppTool.run(self)
 
         # if the splitter us hidden, display it
         if self.app.ui.splitter.sizes()[0] == 0:
@@ -187,7 +186,7 @@ class BufferSelectionTool(FlatCAMTool):
         self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
 
 
-class TextInputTool(FlatCAMTool):
+class TextInputTool(AppTool):
     """
     Simple input for buffer distance.
     """
@@ -195,7 +194,7 @@ class TextInputTool(FlatCAMTool):
     toolName = "Text Input Tool"
 
     def __init__(self, app):
-        FlatCAMTool.__init__(self, app)
+        AppTool.__init__(self, app)
 
         self.app = app
         self.text_path = []
@@ -340,7 +339,7 @@ class TextInputTool(FlatCAMTool):
 
     def run(self):
         self.app.defaults.report_usage("Geo Editor TextInputTool()")
-        FlatCAMTool.run(self)
+        AppTool.run(self)
 
         # if the splitter us hidden, display it
         if self.app.ui.splitter.sizes()[0] == 0:
@@ -405,7 +404,7 @@ class TextInputTool(FlatCAMTool):
         self.app.ui.notebook.setTabText(2, _("Tool"))
 
 
-class PaintOptionsTool(FlatCAMTool):
+class PaintOptionsTool(AppTool):
     """
     Inputs to specify how to paint the selected polygons.
     """
@@ -413,7 +412,7 @@ class PaintOptionsTool(FlatCAMTool):
     toolName = "Paint Tool"
 
     def __init__(self, app, fcdraw):
-        FlatCAMTool.__init__(self, app)
+        AppTool.__init__(self, app)
 
         self.app = app
         self.fcdraw = fcdraw
@@ -538,7 +537,7 @@ class PaintOptionsTool(FlatCAMTool):
 
     def run(self):
         self.app.defaults.report_usage("Geo Editor ToolPaint()")
-        FlatCAMTool.run(self)
+        AppTool.run(self)
 
         # if the splitter us hidden, display it
         if self.app.ui.splitter.sizes()[0] == 0:
@@ -547,7 +546,7 @@ class PaintOptionsTool(FlatCAMTool):
         self.app.ui.notebook.setTabText(2, _("Paint Tool"))
 
     def set_tool_ui(self):
-        # Init GUI
+        # Init AppGUI
         if self.app.defaults["tools_painttooldia"]:
             self.painttooldia_entry.set_value(self.app.defaults["tools_painttooldia"])
         else:
@@ -599,7 +598,7 @@ class PaintOptionsTool(FlatCAMTool):
         self.app.ui.splitter.setSizes([0, 1])
 
 
-class TransformEditorTool(FlatCAMTool):
+class TransformEditorTool(AppTool):
     """
     Inputs to specify how to paint the selected polygons.
     """
@@ -612,7 +611,7 @@ class TransformEditorTool(FlatCAMTool):
     offsetName = _("Offset")
 
     def __init__(self, app, draw_app):
-        FlatCAMTool.__init__(self, app)
+        AppTool.__init__(self, app)
 
         self.app = app
         self.draw_app = draw_app
@@ -933,7 +932,7 @@ class TransformEditorTool(FlatCAMTool):
               "the 'y' in (x, y) will be used when using Flip on Y.")
         )
         self.flip_ref_label.setFixedWidth(50)
-        self.flip_ref_entry = FCEntry("(0, 0)")
+        self.flip_ref_entry = FCEntry("0, 0")
 
         self.flip_ref_button = FCButton()
         self.flip_ref_button.set_value(_("Add"))
@@ -981,7 +980,7 @@ class TransformEditorTool(FlatCAMTool):
 
     def run(self):
         self.app.defaults.report_usage("Geo Editor Transform Tool()")
-        FlatCAMTool.run(self)
+        AppTool.run(self)
         self.set_tool_ui()
 
         # if the splitter us hidden, display it
@@ -991,7 +990,7 @@ class TransformEditorTool(FlatCAMTool):
         self.app.ui.notebook.setTabText(2, _("Transform Tool"))
 
     def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
+        AppTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
 
     def set_tool_ui(self):
         # Initialize form
@@ -1048,7 +1047,7 @@ class TransformEditorTool(FlatCAMTool):
         if self.app.defaults["tools_transform_mirror_point"]:
             self.flip_ref_entry.set_value(self.app.defaults["tools_transform_mirror_point"])
         else:
-            self.flip_ref_entry.set_value((0, 0))
+            self.flip_ref_entry.set_value("0, 0")
 
     def template(self):
         if not self.draw_app.selected:
@@ -3383,7 +3382,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             self.shapes = self.app.plotcanvas.new_shape_collection(layers=1)
             self.tool_shape = self.app.plotcanvas.new_shape_collection(layers=1)
         else:
-            from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+            from AppGUI.PlotCanvasLegacy import ShapeCollectionLegacy
             self.shapes = ShapeCollectionLegacy(obj=self, app=self.app, name='shapes_geo_editor')
             self.tool_shape = ShapeCollectionLegacy(obj=self, app=self.app, name='tool_shapes_geo_editor')
 
@@ -3467,22 +3466,32 @@ class FlatCAMGeoEditor(QtCore.QObject):
             :return:
             """
             try:
-                self.options[opt] = float(entry.text())
+                text_value = entry.text()
+                if ',' in text_value:
+                    text_value = text_value.replace(',', '.')
+                self.options[opt] = float(text_value)
             except Exception as e:
+                entry.set_value(self.app.defaults[opt])
                 log.debug("FlatCAMGeoEditor.__init__().entry2option() --> %s" % str(e))
                 return
 
-        def gridx_changed(goption, gentry):
+        def grid_changed(goption, gentry):
             """
 
-            :param goption: String. Can be either 'global_gridx' or 'global_gridy'
-            :param gentry:  A GUI element which text value is read and used
+            :param goption:     String. Can be either 'global_gridx' or 'global_gridy'
+            :param gentry:      A GUI element which text value is read and used
             :return:
             """
+            if goption not in ['global_gridx', 'global_gridy']:
+                return
+
             entry2option(opt=goption, entry=gentry)
             # if the grid link is checked copy the value in the GridX field to GridY
             try:
-                val = float(gentry.get_value())
+                text_value = gentry.text()
+                if ',' in text_value:
+                    text_value = text_value.replace(',', '.')
+                val = float(text_value)
             except ValueError:
                 return
 
@@ -3491,7 +3500,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.app.ui.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator())
         self.app.ui.grid_gap_x_entry.textChanged.connect(
-            lambda: gridx_changed("global_gridx", self.app.ui.grid_gap_x_entry))
+            lambda: grid_changed("global_gridx", self.app.ui.grid_gap_x_entry))
 
         self.app.ui.grid_gap_y_entry.setValidator(QtGui.QDoubleValidator())
         self.app.ui.grid_gap_y_entry.textChanged.connect(
@@ -3542,7 +3551,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         # store the status of the editor so the Delete at object level will not work until the edit is finished
         self.editor_active = False
-        log.debug("Initialization of the FlatCAM Geometry Editor is finished ...")
+        log.debug("Initialization of the Geometry Editor is finished ...")
 
     def pool_recreated(self, pool):
         self.shapes.pool = pool
@@ -3559,14 +3568,14 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         # Remove anything else in the GUI Selected Tab
         self.app.ui.selected_scroll_area.takeWidget()
-        # Put ourselves in the GUI Selected Tab
+        # Put ourselves in the AppGUI Selected Tab
         self.app.ui.selected_scroll_area.setWidget(self.geo_edit_widget)
         # Switch notebook to Selected page
         self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
 
     def build_ui(self):
         """
-        Build the GUI in the Selected Tab for this editor
+        Build the AppGUI in the Selected Tab for this editor
 
         :return:
         """
@@ -3644,10 +3653,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.tool_shape.enabled = True
         self.app.app_cursor.enabled = True
 
-        self.app.ui.snap_max_dist_entry.setEnabled(True)
-        self.app.ui.corner_snap_btn.setEnabled(True)
-        self.app.ui.snap_magnet.setVisible(True)
         self.app.ui.corner_snap_btn.setVisible(True)
+        self.app.ui.snap_magnet.setVisible(True)
 
         self.app.ui.geo_editor_menu.setDisabled(False)
         self.app.ui.geo_editor_menu.menuAction().setVisible(True)
@@ -3658,7 +3665,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.geo_edit_toolbar.setDisabled(False)
         self.app.ui.geo_edit_toolbar.setVisible(True)
 
-        self.app.ui.snap_toolbar.setDisabled(False)
+        self.app.ui.status_toolbar.setDisabled(False)
 
         self.app.ui.popmenu_disable.setVisible(False)
         self.app.ui.cmenu_newmenu.menuAction().setVisible(False)
@@ -3675,7 +3682,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         self.item_selected.connect(self.on_geo_elem_selected)
 
-        # ## GUI Events
+        # ## AppGUI Events
         self.tw.itemSelectionChanged.connect(self.on_tree_selection_change)
         # self.tw.keyPressed.connect(self.app.ui.keyPressEvent)
         # self.tw.customContextMenuRequested.connect(self.on_menu_request)
@@ -3703,27 +3710,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.app.ui.geo_edit_toolbar.setDisabled(True)
 
         settings = QSettings("Open Source", "FlatCAM")
-        if settings.contains("layout"):
-            layout = settings.value('layout', type=str)
-            if layout == 'standard':
-                # self.app.ui.geo_edit_toolbar.setVisible(False)
-
-                self.app.ui.snap_max_dist_entry.setEnabled(False)
-                self.app.ui.corner_snap_btn.setEnabled(False)
-                self.app.ui.snap_magnet.setVisible(False)
-                self.app.ui.corner_snap_btn.setVisible(False)
-            else:
-                # self.app.ui.geo_edit_toolbar.setVisible(True)
-
-                self.app.ui.snap_max_dist_entry.setEnabled(False)
-                self.app.ui.corner_snap_btn.setEnabled(False)
-        else:
-            # self.app.ui.geo_edit_toolbar.setVisible(False)
-
-            self.app.ui.snap_magnet.setVisible(False)
-            self.app.ui.corner_snap_btn.setVisible(False)
-            self.app.ui.snap_max_dist_entry.setEnabled(False)
-            self.app.ui.corner_snap_btn.setEnabled(False)
+        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.geo_edit_toolbar.setVisible(False) if self.toolbar_old_state is False \
@@ -3757,7 +3745,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
             pass
 
         try:
-            # ## GUI Events
+            # ## AppGUI Events
             self.tw.itemSelectionChanged.disconnect(self.on_tree_selection_change)
             # self.tw.keyPressed.connect(self.app.ui.keyPressEvent)
             # self.tw.customContextMenuRequested.connect(self.on_menu_request)
@@ -4100,7 +4088,6 @@ class FlatCAMGeoEditor(QtCore.QObject):
         # 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.on_grid_snap_triggered(state=True)
 
     def on_buffer_tool(self):
         buff_tool = BufferSelectionTool(self.app, self)
@@ -4148,9 +4135,11 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
         # make sure that the cursor shape is enabled/disabled, too
         if self.options['grid_snap'] is True:
+            self.app.inform[str, bool].emit(_("Grid Snap enabled."), False)
             self.app.app_cursor.enabled = True
         else:
             self.app.app_cursor.enabled = False
+            self.app.inform[str, bool].emit(_("Grid Snap disabled."), False)
 
     def on_canvas_click(self, event):
         """
@@ -4173,8 +4162,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
             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))
+            # self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
+            #                                        "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (0, 0))
 
             modifiers = QtWidgets.QApplication.keyboardModifiers()
             # If the SHIFT key is pressed when LMB is clicked then the coordinates are copied to clipboard
@@ -4261,18 +4250,23 @@ class FlatCAMGeoEditor(QtCore.QObject):
         self.snap_y = y
         self.app.mouse = [x, y]
 
-        # update the position label in the infobar since the APP mouse event handlers are disconnected
-        self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
-                                           "<b>Y</b>: %.4f" % (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 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))
+        # # 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)
 
         if event.button == 1 and event_is_dragging and isinstance(self.active_tool, FCEraser):
             pass
@@ -4665,7 +4659,7 @@ class FlatCAMGeoEditor(QtCore.QObject):
 
     def select_tool(self, toolname):
         """
-        Selects a drawing tool. Impacts the object and GUI.
+        Selects a drawing tool. Impacts the object and AppGUI.
 
         :param toolname: Name of the tool.
         :return: None
@@ -4750,8 +4744,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
         Transfers the geometry tool shape buffer to the selected geometry
         object. The geometry already in the object are removed.
 
-        :param fcgeometry: GeometryObject
-        :return: None
+        :param fcgeometry:  GeometryObject
+        :return:            None
         """
         if self.multigeo_tool:
             fcgeometry.tools[self.multigeo_tool]['solid_geometry'] = []
@@ -4776,6 +4770,8 @@ class FlatCAMGeoEditor(QtCore.QObject):
                 new_geo = linemerge(new_geo)
             fcgeometry.solid_geometry.append(new_geo)
 
+        self.deactivate()
+
     def update_options(self, obj):
         if self.paint_tooldia:
             obj.options['cnctooldia'] = deepcopy(str(self.paint_tooldia))

+ 120 - 126
flatcamEditors/FlatCAMGrbEditor.py → AppEditors/FlatCAMGrbEditor.py

@@ -14,15 +14,13 @@ import shapely.affinity as affinity
 
 from vispy.geometry import Rect
 
-import threading
-import time
 from copy import copy, deepcopy
 import logging
 
 from camlib import distance, arc, three_point_circle
-from flatcamGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, FCSpinner, RadioSet, \
+from AppGUI.GUIElements import FCEntry, FCComboBox, FCTable, FCDoubleSpinner, FCSpinner, RadioSet, \
     EvalEntry2, FCInputDialog, FCButton, OptionalInputSection, FCCheckBox
-from FlatCAMTool import FlatCAMTool
+from AppTool import AppTool
 
 import numpy as np
 from numpy.linalg import norm as numpy_norm
@@ -32,7 +30,7 @@ import math
 # import pngcanvas
 import traceback
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -1086,15 +1084,6 @@ class FCRegion(FCShapeTool):
 
         self.draw_app.app.inform.emit('[success] %s' % _("Done."))
 
-    def clean_up(self):
-        self.draw_app.selected = []
-        self.draw_app.apertures_table.clearSelection()
-        self.draw_app.plot_all()
-        try:
-            self.draw_app.app.jump_signal.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
     def on_key(self, key):
         # Jump to coords
         if key == QtCore.Qt.Key_J or key == 'J':
@@ -1160,16 +1149,36 @@ class FCRegion(FCShapeTool):
 
             return msg
 
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
 
-class FCTrack(FCRegion):
+class FCTrack(FCShapeTool):
     """
     Resulting type: Polygon
     """
     def __init__(self, draw_app):
-        FCRegion.__init__(self, draw_app)
+        DrawTool.__init__(self, draw_app)
         self.name = 'track'
         self.draw_app = draw_app
 
+        self.steps_per_circle = self.draw_app.app.defaults["gerber_circle_steps"]
+
+        size_ap = float(self.draw_app.storage_dict[self.draw_app.last_aperture_selected]['size'])
+        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. final_click = False
         try:
             QtGui.QGuiApplication.restoreOverrideCursor()
         except Exception as e:
@@ -1183,52 +1192,23 @@ class FCTrack(FCRegion):
 
         self.draw_app.app.inform.emit(_('Track Mode 1: 45 degrees ...'))
 
-    def make(self):
-        new_geo_el = {}
-        if len(self.temp_points) == 1:
-            new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val,
-                                                                 resolution=int(self.steps_per_circle / 4))
-            new_geo_el['follow'] = Point(self.temp_points)
-        else:
-            new_geo_el['solid'] = (LineString(self.temp_points).buffer(
-                self.buf_val, resolution=int(self.steps_per_circle / 4))).buffer(0)
-            new_geo_el['follow'] = LineString(self.temp_points)
-
-        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.apertures_table.clearSelection()
-        self.draw_app.plot_all()
-        try:
-            self.draw_app.app.jump_signal.disconnect()
-        except (TypeError, AttributeError):
-            pass
-
     def click(self, point):
         self.draw_app.in_action = True
-        try:
-            if point != self.points[-1]:
-                self.points.append(point)
-        except IndexError:
+
+        if not self.points:
             self.points.append(point)
+        elif point != self.points[-1]:
+            self.points.append(point)
+        else:
+            return
 
         new_geo_el = {}
 
         if len(self.temp_points) == 1:
-            new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val,
-                                                                 resolution=int(self.steps_per_circle / 4))
+            new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val, int(self.steps_per_circle))
             new_geo_el['follow'] = Point(self.temp_points)
         else:
-            new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val,
-                                                                      resolution=int(self.steps_per_circle / 4))
+            new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val, int(self.steps_per_circle))
             new_geo_el['follow'] = LineString(self.temp_points)
 
         self.draw_app.add_gerber_shape(DrawToolShape(new_geo_el),
@@ -1241,23 +1221,25 @@ class FCTrack(FCRegion):
 
         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):
         self.update_grid_info()
         new_geo_el = {}
 
-        if len(self.points) == 0:
-            new_geo_el['solid'] = Point(data).buffer(self.buf_val,
-                                                     resolution=int(self.steps_per_circle / 4))
-
+        if not self.points:
+            new_geo_el['solid'] = Point(data).buffer(self.buf_val, int(self.steps_per_circle))
             return DrawToolUtilityShape(new_geo_el)
-        elif len(self.points) > 0:
-
-            self.temp_points = [self.points[-1]]
+        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))
 
@@ -1305,14 +1287,30 @@ class FCTrack(FCRegion):
 
             self.temp_points.append(data)
             if len(self.temp_points) == 1:
-                new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val,
-                                                                     resolution=int(self.steps_per_circle / 4))
+                new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val, int(self.steps_per_circle))
                 return DrawToolUtilityShape(new_geo_el)
 
-            new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val,
-                                                                      resolution=int(self.steps_per_circle / 4))
+            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):
+        new_geo_el = {}
+        if len(self.temp_points) == 1:
+            new_geo_el['solid'] = Point(self.temp_points).buffer(self.buf_val, int(self.steps_per_circle))
+            new_geo_el['follow'] = Point(self.temp_points)
+        else:
+            new_geo_el['solid'] = LineString(self.temp_points).buffer(self.buf_val, int(self.steps_per_circle))
+            new_geo_el['solid'] = new_geo_el['solid'].buffer(0)     # try to clean the geometry
+            new_geo_el['follow'] = LineString(self.temp_points)
+
+        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:
@@ -1405,6 +1403,15 @@ class FCTrack(FCRegion):
 
             return msg
 
+    def clean_up(self):
+        self.draw_app.selected = []
+        self.draw_app.apertures_table.clearSelection()
+        self.draw_app.plot_all()
+        try:
+            self.draw_app.app.jump_signal.disconnect()
+        except (TypeError, AttributeError):
+            pass
+
 
 class FCDisc(FCShapeTool):
     """
@@ -2955,7 +2962,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         # this var will store the state of the toolbar before starting the editor
         self.toolbar_old_state = False
 
-        # Init GUI
+        # Init AppGUI
         self.apdim_lbl.hide()
         self.apdim_entry.hide()
         self.gerber_obj = None
@@ -2967,7 +2974,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             self.tool_shape = self.canvas.new_shape_collection(layers=1)
             self.ma_annotation = self.canvas.new_text_group()
         else:
-            from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+            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(
@@ -3110,7 +3117,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.complete = True
 
         self.set_ui()
-        log.debug("Initialization of the FlatCAM Gerber Editor is finished ...")
+        log.debug("Initialization of the Gerber Editor is finished ...")
 
     def pool_recreated(self, pool):
         self.shapes.pool = pool
@@ -3139,7 +3146,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             tt_aperture = self.sorted_apcode[i]
             self.tid2apcode[i + 1] = tt_aperture
 
-        # Init GUI
+        # Init AppGUI
 
         self.buffer_distance_entry.set_value(self.app.defaults["gerber_editor_buff_f"])
         self.scale_factor_entry.set_value(self.app.defaults["gerber_editor_scale_f"])
@@ -3685,10 +3692,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.shapes.enabled = True
         self.tool_shape.enabled = True
 
-        self.app.ui.snap_max_dist_entry.setEnabled(True)
-        self.app.ui.corner_snap_btn.setEnabled(True)
-        self.app.ui.snap_magnet.setVisible(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)
@@ -3698,12 +3703,11 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         self.app.ui.grb_edit_toolbar.setDisabled(False)
         self.app.ui.grb_edit_toolbar.setVisible(True)
-        # self.app.ui.snap_toolbar.setDisabled(False)
+        # self.app.ui.status_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.on_grid_snap_triggered(state=True)
 
         # adjust the visibility of some of the canvas context menu
         self.app.ui.popmenu_edit.setVisible(False)
@@ -3736,29 +3740,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.app.ui.grb_edit_toolbar.setDisabled(True)
 
         settings = QSettings("Open Source", "FlatCAM")
-        if settings.contains("layout"):
-            layout = settings.value('layout', type=str)
-            if layout == 'standard':
-                # self.app.ui.exc_edit_toolbar.setVisible(False)
-
-                self.app.ui.snap_max_dist_entry.setEnabled(False)
-                self.app.ui.corner_snap_btn.setEnabled(False)
-                self.app.ui.snap_magnet.setVisible(False)
-                self.app.ui.corner_snap_btn.setVisible(False)
-            else:
-                # self.app.ui.exc_edit_toolbar.setVisible(True)
-
-                self.app.ui.snap_max_dist_entry.setEnabled(False)
-                self.app.ui.corner_snap_btn.setEnabled(False)
-                self.app.ui.snap_magnet.setVisible(True)
-                self.app.ui.corner_snap_btn.setVisible(True)
-        else:
-            # self.app.ui.exc_edit_toolbar.setVisible(False)
-
-            self.app.ui.snap_max_dist_entry.setEnabled(False)
-            self.app.ui.corner_snap_btn.setEnabled(False)
-            self.app.ui.snap_magnet.setVisible(False)
-            self.app.ui.corner_snap_btn.setVisible(False)
+        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 \
@@ -4210,7 +4193,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
     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.app.inform.emit('[success] %s.' % _("Adding geometry finished. Preparing the AppGUI"))
         self.set_ui()
         self.build_ui(first_run=True)
         self.plot_all()
@@ -4245,6 +4228,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             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):
@@ -4266,9 +4250,10 @@ class FlatCAMGrbEditor(QtCore.QObject):
         """
         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
+        :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
         """
 
@@ -4360,26 +4345,27 @@ class FlatCAMGrbEditor(QtCore.QObject):
                 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 = '[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.export_gerber(obj_name=out_name, filename=None,
                                                          local_use=grb_obj, use_thread=False)
 
         with self.app.proc_container.new(_("Creating Gerber.")):
             try:
-                self.app.new_object("gerber", outname, obj_init)
+                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
 
-            self.app.inform.emit('[success] %s' % _("Done. Gerber editing finished."))
             # make sure to clean the previous results
             self.results = []
+            self.deactivate_grb_editor()
+            self.app.inform.emit('[success] %s' % _("Done. Gerber editing finished."))
 
     def on_tool_select(self, tool):
         """
@@ -4537,8 +4523,8 @@ class FlatCAMGrbEditor(QtCore.QObject):
             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))
+            # 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:
@@ -4550,8 +4536,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                         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."))
+                    self.app.inform.emit('[success] %s' % _("Coordinates copied to clipboard."))
                     return
 
                 # Dispatch event to active_tool
@@ -4562,6 +4547,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                     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()
@@ -4569,6 +4555,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
                         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:
@@ -4629,12 +4616,14 @@ class FlatCAMGrbEditor(QtCore.QObject):
                         # 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, FCShapeTool):
-                            self.active_tool.click(self.app.geo_editor.snap(self.x, self.y))
-                            self.active_tool.make()
+                            if isinstance(self.active_tool, FCTrack):
+                                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."))
+                                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 FCTrack
@@ -4774,18 +4763,23 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
         self.app.mouse = [x, y]
 
-        # update the position label in the infobar since the APP mouse event handlers are disconnected
-        self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   " 
-                                           "<b>Y</b>: %.4f" % (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 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))
+        # # 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))
 
@@ -5032,7 +5026,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
 
     def select_tool(self, toolname):
         """
-        Selects a drawing tool. Impacts the object and GUI.
+        Selects a drawing tool. Impacts the object and AppGUI.
 
         :param toolname: Name of the tool.
         :return: None
@@ -5298,7 +5292,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
         self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
 
 
-class TransformEditorTool(FlatCAMTool):
+class TransformEditorTool(AppTool):
     """
     Inputs to specify how to paint the selected polygons.
     """
@@ -5311,7 +5305,7 @@ class TransformEditorTool(FlatCAMTool):
     offsetName = _("Offset")
 
     def __init__(self, app, draw_app):
-        FlatCAMTool.__init__(self, app)
+        AppTool.__init__(self, app)
 
         self.app = app
         self.draw_app = draw_app
@@ -5633,7 +5627,7 @@ class TransformEditorTool(FlatCAMTool):
               "the 'y' in (x, y) will be used when using Flip on Y.")
         )
         self.flip_ref_label.setMinimumWidth(50)
-        self.flip_ref_entry = EvalEntry2("(0, 0)")
+        self.flip_ref_entry = FCEntry()
         self.flip_ref_entry.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
         # self.flip_ref_entry.setFixedWidth(60)
 
@@ -5697,13 +5691,13 @@ class TransformEditorTool(FlatCAMTool):
             except AttributeError:
                 pass
 
-        FlatCAMTool.run(self)
+        AppTool.run(self)
         self.set_tool_ui()
 
         self.app.ui.notebook.setTabText(2, _("Transform Tool"))
 
     def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
+        AppTool.install(self, icon, separator, shortcut='Alt+T', **kwargs)
 
     def set_tool_ui(self):
         # Initialize form
@@ -5760,7 +5754,7 @@ class TransformEditorTool(FlatCAMTool):
         if self.app.defaults["tools_transform_mirror_point"]:
             self.flip_ref_entry.set_value(self.app.defaults["tools_transform_mirror_point"])
         else:
-            self.flip_ref_entry.set_value((0, 0))
+            self.flip_ref_entry.set_value("0, 0")
 
     def template(self):
         if not self.draw_app.selected:

+ 4 - 4
flatcamEditors/FlatCAMTextEditor.py → AppEditors/FlatCAMTextEditor.py

@@ -5,7 +5,7 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from flatcamGUI.GUIElements import FCFileSaveDialog, FCEntry, FCTextAreaExtended, FCTextAreaLineNumber
+from AppGUI.GUIElements import FCFileSaveDialog, FCEntry, FCTextAreaExtended, FCTextAreaLineNumber
 from PyQt5 import QtPrintSupport, QtWidgets, QtCore, QtGui
 
 from reportlab.platypus import SimpleDocTemplate, Paragraph
@@ -15,7 +15,7 @@ from reportlab.lib.units import inch, mm
 # from io import StringIO
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -214,10 +214,10 @@ class TextEditor(QtWidgets.QWidget):
             filename = str(FCFileSaveDialog.get_saved_filename(
                 caption=_("Export Code ..."),
                 directory=self.app.defaults["global_last_folder"] + '/' + str(obj_name),
-                filter=_filter_
+                ext_filter=_filter_
             )[0])
         except TypeError:
-            filename = str(FCFileSaveDialog.get_saved_filename(caption=_("Export Code ..."), filter=_filter_)[0])
+            filename = str(FCFileSaveDialog.get_saved_filename(caption=_("Export Code ..."), ext_filter=_filter_)[0])
 
         if filename == "":
             self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))

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


+ 298 - 19
flatcamGUI/GUIElements.py → AppGUI/GUIElements.py

@@ -20,9 +20,10 @@ from copy import copy
 import re
 import logging
 import html
+import sys
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 log = logging.getLogger('base')
@@ -569,9 +570,13 @@ class FCEntry3(FCEntry):
 
 
 class EvalEntry(QtWidgets.QLineEdit):
-    def __init__(self, parent=None):
+    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):
@@ -598,7 +603,6 @@ class EvalEntry(QtWidgets.QLineEdit):
 
     def get_value(self):
         raw = str(self.text()).strip(' ')
-        evaled = 0.0
         try:
             evaled = eval(raw)
         except Exception as e:
@@ -638,7 +642,7 @@ class EvalEntry2(QtWidgets.QLineEdit):
 
     def get_value(self):
         raw = str(self.text()).strip(' ')
-        evaled = 0.0
+
         try:
             evaled = eval(raw)
         except Exception as e:
@@ -655,6 +659,30 @@ class EvalEntry2(QtWidgets.QLineEdit):
         return QtCore.QSize(EDIT_SIZE_HINT, default_hint_size.height())
 
 
+class NumericalEvalEntry(EvalEntry):
+    """
+    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)
+
+
+class NumericalEvalTupleEntry(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)
+
+
 class FCSpinner(QtWidgets.QSpinBox):
 
     returnPressed = QtCore.pyqtSignal()
@@ -683,6 +711,8 @@ class FCSpinner(QtWidgets.QSpinBox):
             self.setAlignment(align_val)
 
         self.prev_readyToEdit = True
+        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:
@@ -815,6 +845,8 @@ class FCDoubleSpinner(QtWidgets.QDoubleSpinBox):
             self.setAlignment(align_val)
 
         self.prev_readyToEdit = True
+        sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Preferred)
+        self.setSizePolicy(sizePolicy)
 
     def on_edit_finished(self):
         self.clearFocus()
@@ -1336,7 +1368,8 @@ class FCComboBox(QtWidgets.QComboBox):
         return str(self.currentText())
 
     def set_value(self, val):
-        self.setCurrentIndex(self.findText(str(val)))
+        idx = self.findText(str(val))
+        self.setCurrentIndex(idx)
 
     @property
     def is_last(self):
@@ -1480,9 +1513,11 @@ 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__()
+        super().__init__(parent=parent)
 
         self.tabBar = self.FCTabBar(self)
         self.tabBar.onMoveTabSignal.connect(self.moveTab)
@@ -1619,7 +1654,7 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         self.insertTab(toIndex, widget, icon, text)
         self.setCurrentIndex(toIndex)
 
-    @pyqtSlot(int, QtCore.QPoint)
+    # @pyqtSlot(int, QtCore.QPoint)
     def detachTab(self, index, point):
         """
         Detach the tab by removing it's contents and placing them in
@@ -1656,6 +1691,8 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         # 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,
@@ -1668,11 +1705,11 @@ class FCDetachableTab(QtWidgets.QTabWidget):
         :return:
         """
 
+        old_name = name
+
         # Make the content widget a child of this widget
         contentWidget.setParent(self)
 
-        # Remove the reference
-        del self.detachedTabs[name]
         # make sure that we strip the 'FlatCAM' part of the detached name otherwise the tab name will be too long
         name = name.partition(' ')[2]
 
@@ -1712,6 +1749,9 @@ class FCDetachableTab(QtWidgets.QTabWidget):
             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)
@@ -1727,6 +1767,14 @@ class FCDetachableTab(QtWidgets.QTabWidget):
             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
@@ -2179,11 +2227,14 @@ class OptionalHideInputSection:
         """
         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
+        :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), \
@@ -2215,6 +2266,7 @@ class OptionalHideInputSection:
 class FCTable(QtWidgets.QTableWidget):
 
     drag_drop_sig = QtCore.pyqtSignal()
+    lost_focus = QtCore.pyqtSignal()
 
     def __init__(self, drag_drop=False, protected_rows=None, parent=None):
         super(FCTable, self).__init__(parent)
@@ -2278,6 +2330,10 @@ class FCTable(QtWidgets.QTableWidget):
         else:
             QtWidgets.QTableWidget.mousePressEvent(self, event)
 
+    def focusOutEvent(self, event):
+        self.lost_focus.emit()
+        super().focusOutEvent(event)
+
     def setupContextMenu(self):
         self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
 
@@ -2449,7 +2505,8 @@ class SpinBoxDelegate(QtWidgets.QItemDelegate):
     def updateEditorGeometry(self, editor, option, index):
         editor.setGeometry(option.rect)
 
-    def setDecimals(self, spinbox, digits):
+    @staticmethod
+    def setDecimals(spinbox, digits):
         spinbox.setDecimals(digits)
 
 
@@ -2497,7 +2554,7 @@ class DialogBoxRadio(QtWidgets.QDialog):
         :param title: string with the window title
         :param label: string with the message inside the dialog box
         """
-        super(DialogBoxRadio, self).__init__()
+        super(DialogBoxRadio, self).__init__(parent=parent)
         if initial_text is None:
             self.location = str((0, 0))
         else:
@@ -2740,9 +2797,12 @@ class MyCompleter(QCompleter):
     insertText = QtCore.pyqtSignal(str)
 
     def __init__(self, parent=None):
-        QCompleter.__init__(self)
+        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):
@@ -2898,9 +2958,9 @@ class FCFileSaveDialog(QtWidgets.QFileDialog):
         super(FCFileSaveDialog, self).__init__(*args)
 
     @staticmethod
-    def get_saved_filename(parent=None, caption='', directory='', filter='', initialFilter=''):
+    def get_saved_filename(parent=None, caption='', directory='', ext_filter='', initialFilter=''):
         filename, _filter = QtWidgets.QFileDialog.getSaveFileName(parent=parent, caption=caption,
-                                                                  directory=directory, filter=filter,
+                                                                  directory=directory, filter=ext_filter,
                                                                   initialFilter=initialFilter)
 
         filename = str(filename)
@@ -2916,6 +2976,225 @@ class FCFileSaveDialog(QtWidgets.QFileDialog):
             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 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'),
+                                                 _('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'),
+                                                _('Open &Gerber ...\tCtrl+G'), self)
+            self.menu_open.addAction(menu_opengerber)
+
+            # Open Excellon ...
+            menu_openexcellon = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/open_excellon32.png'),
+                                                  _('Open &Excellon ...\tCtrl+E'), self)
+            self.menu_open.addAction(menu_openexcellon)
+
+            # Open G-Code ...
+            menu_opengcode = QtWidgets.QAction(QtGui.QIcon(self.app.resource_location + '/code.png'),
+                                               _('Open G-&Code ...'), self)
+            self.menu_open.addAction(menu_opengcode)
+
+            self.menu_open.addSeparator()
+
+            menu_openproject.triggered.connect(self.app.on_file_openproject)
+            menu_opengerber.triggered.connect(self.app.on_fileopengerber)
+            menu_openexcellon.triggered.connect(self.app.on_fileopenexcellon)
+            menu_opengcode.triggered.connect(self.app.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:

ファイルの差分が大きいため隠しています
+ 122 - 899
AppGUI/MainGUI.py


ファイルの差分が大きいため隠しています
+ 274 - 383
AppGUI/ObjectUI.py


+ 153 - 14
flatcamGUI/PlotCanvas.py → AppGUI/PlotCanvas.py

@@ -8,13 +8,21 @@
 from PyQt5 import QtCore
 
 import logging
-from flatcamGUI.VisPyCanvas import VisPyCanvas, Color
-from flatcamGUI.VisPyVisuals import ShapeGroup, ShapeCollection, TextCollection, TextGroup, Cursor
-from vispy.scene.visuals import InfiniteLine, Line
+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')
 
 
@@ -54,8 +62,12 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
 
         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
@@ -129,11 +141,6 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         self.h_line = InfiniteLine(pos=0, color=(0.70, 0.3, 0.3, 0.8), vertical=False,
                                    parent=self.view.scene)
 
-        # 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"])
-
         self.line_parent = None
         if self.fcapp.defaults["global_cursor_color_enabled"]:
             c_color = Color(self.fcapp.defaults["global_cursor_color"]).rgba
@@ -146,13 +153,61 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
         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)
+
+        # Axis Display
+        self.axis_enabled = True
+
+        # enable Axis
+        self.on_toggle_axis(state=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()
 
-        # TODO: Should be setting to show/hide CNC job annotations (global or per object)
         self.text_collection.enabled = True
 
         self.c = None
@@ -163,6 +218,76 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
 
         self.graph_event_connect('mouse_wheel', self.on_mouse_scroll)
 
+    def on_toggle_axis(self, signal=None, state=None):
+        if state is None:
+            state = not self.axis_enabled
+
+        if state:
+            self.axis_enabled = 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: peachpuff;
+                                                          }
+                                                          """)
+            self.fcapp.inform[str, bool].emit(_("Axis enabled."), False)
+        else:
+            self.axis_enabled = False
+            self.v_line.parent = None
+            self.h_line.parent = None
+            self.fcapp.ui.axis_status_label.setStyleSheet("")
+            self.fcapp.inform[str, bool].emit(_("Axis disabled."), False)
+
+    def on_toggle_hud(self, signal=None, state=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: lightblue;
+                                                  }
+                                                  """)
+            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("")
+            self.fcapp.inform[str, bool].emit(_("HUD disabled."), False)
+
+    def on_toggle_grid_lines(self):
+        state = not self.grid_lines_enabled
+
+        if state:
+            self.grid_lines_enabled = True
+            self.grid.parent = self.view.scene
+            self.fcapp.inform[str, bool].emit(_("Grid enabled."), False)
+        else:
+            self.grid_lines_enabled = False
+            self.grid.parent = 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.
@@ -183,17 +308,30 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
 
         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
+        # 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: lightgreen;
+                        }
+                        """)
 
     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):
@@ -287,6 +425,7 @@ class PlotCanvas(QtCore.QObject, VisPyCanvas):
     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

+ 235 - 11
flatcamGUI/PlotCanvasLegacy.py → AppGUI/PlotCanvasLegacy.py

@@ -19,8 +19,10 @@ from shapely.geometry import Polygon, LineString, LinearRing
 from copy import deepcopy
 import logging
 
+import numpy as np
+
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 # Prevent conflict with Qt5 and above.
@@ -29,13 +31,13 @@ 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')
 
 
@@ -45,9 +47,9 @@ class CanvasCache(QtCore.QObject):
     Case story #1:
 
     1) No objects in the project.
-    2) Object is created (new_object() emits object_created(obj)).
+    2) Object is created (app_obj.new_object() emits object_created(obj)).
        on_object_created() adds (i) object to collection and emits
-       (ii) new_object_available() then calls (iii) object.plot()
+       (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).
@@ -113,7 +115,7 @@ class CanvasCache(QtCore.QObject):
 
         # Continue to update the cache.
 
-    # def on_new_object_available(self):
+    # def on_app_obj.new_object_available(self):
     #
     #     log.debug("A new object is available. Should plot it!")
 
@@ -147,9 +149,13 @@ class PlotCanvasLegacy(QtCore.QObject):
         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
@@ -295,14 +301,163 @@ class PlotCanvasLegacy(QtCore.QObject):
         # 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
 
+        self.hud_enabled = False
+        self.text_hud = self.Thud(plotcanvas=self)
+
+        # 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"])
 
+        if self.app.defaults['global_hud'] is True:
+            self.on_toggle_hud(state=True)
+
+        # Axis Display
+        self.axis_enabled = True
+
+        # enable Axis
+        self.on_toggle_axis(state=True)
+
+    def on_toggle_axis(self, signal=None, state=None):
+        if state is None:
+            state = not self.axis_enabled
+
+        if state:
+            self.axis_enabled = 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: peachpuff;
+                                                            }
+                                                            """)
+                self.app.inform[str, bool].emit(_("Axis enabled."), False)
+        else:
+            self.axis_enabled = 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("")
+                self.app.inform[str, bool].emit(_("Axis disabled."), False)
+
+        self.canvas.draw()
+
+    def on_toggle_hud(self, signal=None, state=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: lightblue;
+                                                }
+                                                """)
+            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("")
+            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):
+        state = not self.grid_lines_enabled
+
+        if state:
+            self.grid_lines_enabled = True
+            self.axes.grid(True)
+            try:
+                self.canvas.draw()
+            except IndexError:
+                pass
+            self.app.inform[str, bool].emit(_("Grid enabled."), False)
+        else:
+            self.grid_lines_enabled = False
+            self.axes.grid(False)
+            try:
+                self.canvas.draw()
+            except IndexError:
+                pass
+            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.
@@ -329,12 +484,23 @@ class PlotCanvasLegacy(QtCore.QObject):
             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: lightgreen;
+                        }
+                        """)
+
     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):
         """
@@ -423,7 +589,7 @@ class PlotCanvasLegacy(QtCore.QObject):
 
             if self.big_cursor is False:
                 try:
-                    x, y = self.app.geo_editor.snap(x_pos, y_pos)
+                    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
@@ -456,7 +622,7 @@ class PlotCanvasLegacy(QtCore.QObject):
                     pass
                 self.canvas.draw_idle()
 
-        self.canvas.blit(self.axes.bbox)
+            self.canvas.blit(self.axes.bbox)
 
     def clear_cursor(self, state):
         if state is True:
@@ -781,6 +947,7 @@ class PlotCanvasLegacy(QtCore.QObject):
     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':
@@ -806,7 +973,11 @@ class PlotCanvasLegacy(QtCore.QObject):
 
     def on_mouse_release(self, event):
 
-        self.is_dragging = False
+        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
@@ -858,7 +1029,7 @@ class PlotCanvasLegacy(QtCore.QObject):
             self.canvas.draw_idle()
 
             # #### Temporary place-holder for cached update #####
-            self.update_screen_request.emit([0, 0, 0, 0, 0])
+            # 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)
@@ -910,6 +1081,59 @@ class PlotCanvasLegacy(QtCore.QObject):
 
         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):
     """
@@ -948,10 +1172,10 @@ class ShapeCollectionLegacy:
         """
 
         :param obj:             This is the object to which the shapes collection is attached and for
-        which it will have to draw shapes
+                                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
+                                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)
         """

+ 1 - 0
flatcamGUI/VisPyCanvas.py → AppGUI/VisPyCanvas.py

@@ -13,6 +13,7 @@ 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

+ 0 - 0
flatcamGUI/VisPyData/data/fonts/opensans-regular.ttf → AppGUI/VisPyData/data/fonts/opensans-regular.ttf


+ 0 - 0
flatcamGUI/VisPyData/data/freetype/freetype253.dll → AppGUI/VisPyData/data/freetype/freetype253.dll


+ 0 - 0
flatcamGUI/VisPyData/data/freetype/freetype253_x64.dll → AppGUI/VisPyData/data/freetype/freetype253_x64.dll


+ 0 - 0
flatcamGUI/VisPyPatches.py → AppGUI/VisPyPatches.py


+ 0 - 0
flatcamGUI/VisPyTesselators.py → AppGUI/VisPyTesselators.py


+ 1 - 1
flatcamGUI/VisPyVisuals.py → AppGUI/VisPyVisuals.py

@@ -13,7 +13,7 @@ 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):

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


+ 0 - 0
flatcamGUI/preferences/OptionsGroupUI.py → AppGUI/preferences/OptionsGroupUI.py


+ 183 - 112
flatcamGUI/preferences/PreferencesUIManager.py → AppGUI/preferences/PreferencesUIManager.py

@@ -5,7 +5,7 @@ from defaults import FlatCAMDefaults
 import logging
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -29,7 +29,7 @@ class PreferencesUIManager:
 
         :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 FlatCAMGUI class which constructs the UI
+        :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
         """
 
@@ -43,7 +43,7 @@ class PreferencesUIManager:
         self.preferences_changed_flag = False
 
         # when adding entries here read the comments in the  method found below named:
-        # def new_object(self, kind, name, initialize, active=True, fit=True, plot=True)
+        # 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,
@@ -127,12 +127,6 @@ class PreferencesUIManager:
             "gerber_plot_line": self.ui.gerber_defaults_form.gerber_gen_group.pl_color_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_iso_scope": self.ui.gerber_defaults_form.gerber_opt_group.iso_scope_radio,
-            "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,
@@ -143,12 +137,6 @@ class PreferencesUIManager:
             # "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_tool_type": self.ui.gerber_defaults_form.gerber_adv_opt_group.tool_type_radio,
-            "gerber_vtipdia": self.ui.gerber_defaults_form.gerber_adv_opt_group.tipdia_spinner,
-            "gerber_vtipangle": self.ui.gerber_defaults_form.gerber_adv_opt_group.tipangle_spinner,
-            "gerber_vcutz": self.ui.gerber_defaults_form.gerber_adv_opt_group.cutz_spinner,
-            "gerber_iso_type": self.ui.gerber_defaults_form.gerber_adv_opt_group.iso_type_radio,
-
             "gerber_buffering": self.ui.gerber_defaults_form.gerber_adv_opt_group.buffering_radio,
             "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,
@@ -180,6 +168,7 @@ class PreferencesUIManager:
             # 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_format_upper_in":
                 self.ui.excellon_defaults_form.excellon_gen_group.excellon_format_upper_in_entry,
             "excellon_format_lower_in":
@@ -221,31 +210,31 @@ class PreferencesUIManager:
             "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_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_spindledir": self.ui.excellon_defaults_form.excellon_adv_opt_group.spindledir_radio,
-            "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_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_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_spindledir":      self.ui.excellon_defaults_form.excellon_adv_opt_group.spindledir_radio,
+            "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_exp_slot_type": self.ui.excellon_defaults_form.excellon_exp_group.slot_type_radio,
+            "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_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,
@@ -270,94 +259,117 @@ class PreferencesUIManager:
                 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_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_plot_line": self.ui.geometry_defaults_form.geometry_gen_group.line_color_entry,
+            "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_plot_line":       self.ui.geometry_defaults_form.geometry_gen_group.line_color_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_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_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,
+            "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,
 
             # 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,
+            "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_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_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,
+            "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_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_prepend": self.ui.cncjob_defaults_form.cncjob_opt_group.prepend_text,
-            "cncjob_append": self.ui.cncjob_defaults_form.cncjob_opt_group.append_text,
+            "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,
-            "cncjob_annotation_fontsize": self.ui.cncjob_defaults_form.cncjob_adv_opt_group.annotation_fontsize_sp,
+            "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,
+            "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,
 
+            # 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_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_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,
+
             # NCC Tool
-            "tools_ncctools": self.ui.tools_defaults_form.tools_ncc_group.ncc_tool_dia_entry,
-            "tools_nccorder": self.ui.tools_defaults_form.tools_ncc_group.ncc_order_radio,
-            "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_combo,
-            "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_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_nccref": 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_plotting": self.ui.tools_defaults_form.tools_ncc_group.ncc_plotting_radio,
-            "tools_nccmilling_type": self.ui.tools_defaults_form.tools_ncc_group.milling_type_radio,
-            "tools_ncctool_type": self.ui.tools_defaults_form.tools_ncc_group.tool_type_radio,
-            "tools_ncccutz": self.ui.tools_defaults_form.tools_ncc_group.cutz_entry,
-            "tools_ncctipdia": self.ui.tools_defaults_form.tools_ncc_group.tipdia_entry,
-            "tools_ncctipangle": self.ui.tools_defaults_form.tools_ncc_group.tipangle_entry,
-            "tools_nccnewdia": self.ui.tools_defaults_form.tools_ncc_group.newdia_entry,
+            "tools_ncctools":           self.ui.tools_defaults_form.tools_ncc_group.ncc_tool_dia_entry,
+            "tools_nccorder":           self.ui.tools_defaults_form.tools_ncc_group.ncc_order_radio,
+            "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_combo,
+            "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_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_nccref":             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_nccmilling_type":    self.ui.tools_defaults_form.tools_ncc_group.milling_type_radio,
+            "tools_ncctool_type":       self.ui.tools_defaults_form.tools_ncc_group.tool_type_radio,
+            "tools_ncccutz":            self.ui.tools_defaults_form.tools_ncc_group.cutz_entry,
+            "tools_ncctipdia":          self.ui.tools_defaults_form.tools_ncc_group.tipdia_entry,
+            "tools_ncctipangle":        self.ui.tools_defaults_form.tools_ncc_group.tipangle_entry,
+            "tools_nccnewdia":          self.ui.tools_defaults_form.tools_ncc_group.newdia_entry,
+            "tools_ncc_plotting":       self.ui.tools_defaults_form.tools_ncc_group.plotting_radio,
 
             # CutOut Tool
             "tools_cutouttooldia": self.ui.tools_defaults_form.tools_cutout_group.cutout_tooldia_entry,
@@ -467,6 +479,12 @@ class PreferencesUIManager:
             "tools_solderpaste_pp": self.ui.tools_defaults_form.tools_solderpaste_group.pp_combo,
             "tools_sub_close_paths": self.ui.tools_defaults_form.tools_sub_group.close_paths_cb,
 
+            # Corner Markers Tool
+
+            "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 2 ####################################################
             # #######################################################################################################
@@ -650,7 +668,7 @@ class PreferencesUIManager:
 
     def show_preferences_gui(self):
         """
-        Called to initialize and show the Preferences GUI
+        Called to initialize and show the Preferences AppGUI
 
         :return: None
         """
@@ -913,7 +931,51 @@ class PreferencesUIManager:
         # make sure we update the self.current_defaults dict used to undo changes to self.defaults
         self.defaults.current_defaults.update(self.defaults)
 
-        if save_to_file:
+        # 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 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:
             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'))
@@ -935,6 +997,10 @@ class PreferencesUIManager:
         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
@@ -951,6 +1017,9 @@ class PreferencesUIManager:
                     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
@@ -1000,6 +1069,7 @@ class PreferencesUIManager:
         :return:            None
         """
         self.defaults.report_usage("save_defaults")
+        log.debug("App.PreferencesUIManager.save_defaults()")
 
         if data_path is None:
             data_path = self.data_path
@@ -1036,7 +1106,7 @@ class PreferencesUIManager:
         if self.ui.toolbarfile.isVisible():
             tb_status += 1
 
-        if self.ui.toolbargeo.isVisible():
+        if self.ui.toolbaredit.isVisible():
             tb_status += 2
 
         if self.ui.toolbarview.isVisible():
@@ -1054,7 +1124,7 @@ class PreferencesUIManager:
         if self.ui.grb_edit_toolbar.isVisible():
             tb_status += 64
 
-        if self.ui.snap_toolbar.isVisible():
+        if self.ui.status_toolbar.isVisible():
             tb_status += 128
 
         if self.ui.toolbarshell.isVisible():
@@ -1118,6 +1188,7 @@ class PreferencesUIManager:
                              "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)

+ 2 - 2
flatcamGUI/preferences/__init__.py → AppGUI/preferences/__init__.py

@@ -1,6 +1,6 @@
-from flatcamGUI.GUIElements import *
+from AppGUI.GUIElements import *
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 

+ 3 - 3
flatcamGUI/preferences/cncjob/CNCJobAdvOptPrefGroupUI.py → AppGUI/preferences/cncjob/CNCJobAdvOptPrefGroupUI.py

@@ -1,10 +1,10 @@
 from PyQt5 import QtWidgets, QtGui, QtCore
 from PyQt5.QtCore import QSettings, Qt
 
-from flatcamGUI.GUIElements import FCTextArea, FCCheckBox, FCComboBox, FCSpinner, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCTextArea, FCCheckBox, FCComboBox, FCSpinner, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/cncjob/CNCJobGenPrefGroupUI.py → AppGUI/preferences/cncjob/CNCJobGenPrefGroupUI.py

@@ -1,10 +1,10 @@
 from PyQt5 import QtWidgets, QtCore, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCCheckBox, RadioSet, FCSpinner, FCDoubleSpinner, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCCheckBox, RadioSet, FCSpinner, FCDoubleSpinner, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/cncjob/CNCJobOptPrefGroupUI.py → AppGUI/preferences/cncjob/CNCJobOptPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCTextArea
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCTextArea
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/cncjob/CNCJobPreferencesUI.py → AppGUI/preferences/cncjob/CNCJobPreferencesUI.py

@@ -1,8 +1,8 @@
 from PyQt5 import QtWidgets
 
-from flatcamGUI.preferences.cncjob.CNCJobAdvOptPrefGroupUI import CNCJobAdvOptPrefGroupUI
-from flatcamGUI.preferences.cncjob.CNCJobOptPrefGroupUI import CNCJobOptPrefGroupUI
-from flatcamGUI.preferences.cncjob.CNCJobGenPrefGroupUI import CNCJobGenPrefGroupUI
+from AppGUI.preferences.cncjob.CNCJobAdvOptPrefGroupUI import CNCJobAdvOptPrefGroupUI
+from AppGUI.preferences.cncjob.CNCJobOptPrefGroupUI import CNCJobOptPrefGroupUI
+from AppGUI.preferences.cncjob.CNCJobGenPrefGroupUI import CNCJobGenPrefGroupUI
 
 
 class CNCJobPreferencesUI(QtWidgets.QWidget):

+ 0 - 0
flatcamGUI/preferences/cncjob/__init__.py → AppGUI/preferences/cncjob/__init__.py


+ 5 - 5
flatcamGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py → AppGUI/preferences/excellon/ExcellonAdvOptPrefGroupUI.py

@@ -1,10 +1,10 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner, FCEntry, FloatEntry, RadioSet, FCCheckBox
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner, RadioSet, FCCheckBox, NumericalEvalTupleEntry, NumericalEvalEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -60,7 +60,7 @@ class ExcellonAdvOptPrefGroupUI(OptionsGroupUI):
         toolchange_xy_label.setToolTip(
             _("Toolchange X,Y position.")
         )
-        self.toolchangexy_entry = FCEntry()
+        self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid1.addWidget(toolchange_xy_label, 1, 0)
         grid1.addWidget(self.toolchangexy_entry, 1, 1)
@@ -71,7 +71,7 @@ class ExcellonAdvOptPrefGroupUI(OptionsGroupUI):
             _("Height of the tool just after start.\n"
               "Delete the value if you don't need this feature.")
         )
-        self.estartz_entry = FloatEntry()
+        self.estartz_entry = NumericalEvalEntry(border_color='#0069A9')
 
         grid1.addWidget(startzlabel, 2, 0)
         grid1.addWidget(self.estartz_entry, 2, 1)

+ 3 - 3
flatcamGUI/preferences/excellon/ExcellonEditorPrefGroupUI.py → AppGUI/preferences/excellon/ExcellonEditorPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCSpinner, FCDoubleSpinner, RadioSet
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCSpinner, FCDoubleSpinner, RadioSet
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/excellon/ExcellonExpPrefGroupUI.py → AppGUI/preferences/excellon/ExcellonExpPrefGroupUI.py

@@ -1,10 +1,10 @@
 from PyQt5 import QtWidgets, QtCore
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import RadioSet, FCSpinner
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import RadioSet, FCSpinner
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 121 - 4
flatcamGUI/preferences/excellon/ExcellonGenPrefGroupUI.py → AppGUI/preferences/excellon/ExcellonGenPrefGroupUI.py

@@ -3,10 +3,10 @@ import platform
 from PyQt5 import QtWidgets, QtCore, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -36,22 +36,31 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         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, 2)
+        grid1.addWidget(separator_line, 1, 0, 1, 3)
 
         grid2 = QtWidgets.QGridLayout()
         self.layout.addLayout(grid2)
@@ -341,6 +350,12 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
 
         # 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"])
 
     def optimization_selection(self):
         if self.excellon_optimization_radio.get_value() == 'M':
@@ -413,3 +428,105 @@ class ExcellonGenPrefGroupUI(OptionsGroupUI):
         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()

+ 6 - 6
flatcamGUI/preferences/excellon/ExcellonOptPrefGroupUI.py → AppGUI/preferences/excellon/ExcellonOptPrefGroupUI.py

@@ -1,12 +1,12 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 
-from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCEntry, FCSpinner, OptionalInputSection, \
-    FCComboBox
-from flatcamGUI.preferences import machinist_setting
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+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 FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -198,7 +198,7 @@ class ExcellonOptPrefGroupUI(OptionsGroupUI):
               "If no value is entered then there is no move\n"
               "on X,Y plane at the end of the job.")
         )
-        self.endxy_entry = FCEntry()
+        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid2.addWidget(endmove_xy_label, 9, 0)
         grid2.addWidget(self.endxy_entry, 9, 1)

+ 7 - 7
flatcamGUI/preferences/excellon/ExcellonPreferencesUI.py → AppGUI/preferences/excellon/ExcellonPreferencesUI.py

@@ -1,14 +1,14 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.preferences.excellon.ExcellonEditorPrefGroupUI import ExcellonEditorPrefGroupUI
-from flatcamGUI.preferences.excellon.ExcellonExpPrefGroupUI import ExcellonExpPrefGroupUI
-from flatcamGUI.preferences.excellon.ExcellonAdvOptPrefGroupUI import ExcellonAdvOptPrefGroupUI
-from flatcamGUI.preferences.excellon.ExcellonOptPrefGroupUI import ExcellonOptPrefGroupUI
-from flatcamGUI.preferences.excellon.ExcellonGenPrefGroupUI import ExcellonGenPrefGroupUI
+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 FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -31,7 +31,7 @@ class ExcellonPreferencesUI(QtWidgets.QWidget):
         self.decimals = decimals
 
         self.excellon_gen_group = ExcellonGenPrefGroupUI(decimals=self.decimals)
-        self.excellon_gen_group.setMinimumWidth(220)
+        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)

+ 0 - 0
flatcamGUI/preferences/excellon/__init__.py → AppGUI/preferences/excellon/__init__.py


+ 26 - 15
flatcamGUI/preferences/general/GeneralAPPSetGroupUI.py → AppGUI/preferences/general/GeneralAPPSetGroupUI.py

@@ -1,13 +1,13 @@
 from PyQt5 import QtCore, QtWidgets, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCComboBox, RadioSet, OptionalInputSection, FCSpinner, \
+from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, FCComboBox, RadioSet, OptionalInputSection, FCSpinner, \
     FCEntry
-from flatcamGUI.preferences import settings
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.preferences import settings
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -177,14 +177,6 @@ class GeneralAPPSetGroupUI(OptionsGroupUI):
                                               {'label': _('Landscape'), 'value': 'l'},
                                               ], stretch=False)
 
-        self.wks = OptionalInputSection(self.workspace_cb,
-                                        [
-                                            self.workspace_type_lbl,
-                                            self.wk_cb,
-                                            self.wk_orientation_label,
-                                            self.wk_orientation_radio
-                                        ])
-
         grid0.addWidget(self.wk_orientation_label, 8, 0)
         grid0.addWidget(self.wk_orientation_radio, 8, 1)
 
@@ -201,7 +193,7 @@ class GeneralAPPSetGroupUI(OptionsGroupUI):
         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"
+              "The notebook is the collapsible area in the left side of the AppGUI,\n"
               "and include the Project, Selected and Tool tabs.")
         )
 
@@ -240,7 +232,7 @@ class GeneralAPPSetGroupUI(OptionsGroupUI):
         # 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"
+            _("This sets the font size for the Textbox AppGUI\n"
               "elements that are used in FlatCAM.")
         )
 
@@ -257,10 +249,29 @@ class GeneralAPPSetGroupUI(OptionsGroupUI):
         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, 14, 0, 1, 2)
+        grid0.addWidget(separator_line, 16, 0, 1, 2)
 
         # -----------------------------------------------------------
         # -------------- MOUSE SETTINGS -----------------------------

+ 5 - 6
flatcamGUI/preferences/general/GeneralAppPrefGroupUI.py → AppGUI/preferences/general/GeneralAppPrefGroupUI.py

@@ -3,12 +3,12 @@ import sys
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import RadioSet, FCSpinner, FCCheckBox, FCComboBox, FCButton, OptionalInputSection, \
+from AppGUI.GUIElements import RadioSet, FCSpinner, FCCheckBox, FCComboBox, FCButton, OptionalInputSection, \
     FCDoubleSpinner
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -380,12 +380,11 @@ class GeneralAppPrefGroupUI(OptionsGroupUI):
 
     def on_toggle_shell_from_settings(self, state):
         """
-        Toggle shell: if is visible close it, if it is closed then open it
+        Toggle shell ui: if is visible close it, if it is closed then open it
+
         :return: None
         """
 
-        self.app.defaults.report_usage("on_toggle_shell_from_settings()")
-
         if state is True:
             if not self.app.ui.shell_dock.isVisible():
                 self.app.ui.shell_dock.show()

+ 20 - 47
flatcamGUI/preferences/general/GeneralGUIPrefGroupUI.py → AppGUI/preferences/general/GeneralGUIPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets, QtCore, QtGui
 from PyQt5.QtCore import QSettings, Qt
 
-from flatcamGUI.GUIElements import RadioSet, FCCheckBox, FCButton, FCComboBox, FCEntry, FCSpinner
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import RadioSet, FCCheckBox, FCButton, FCComboBox, FCEntry, FCSpinner
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -56,13 +56,13 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         )
         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)
+        # 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)
@@ -381,8 +381,6 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
 
         self.layout.addStretch()
 
-        self.theme_button.clicked.connect(self.on_theme_change)
-
         # #############################################################################
         # ############################# GUI COLORS SIGNALS ############################
         # #############################################################################
@@ -418,16 +416,6 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
 
         self.layout_combo.activated.connect(self.on_layout)
 
-    def on_theme_change(self):
-        val = self.theme_radio.get_value()
-        qsettings = QSettings("Open Source", "FlatCAM")
-        qsettings.setValue('theme', val)
-
-        # This will write the setting to the platform specific storage.
-        del qsettings
-
-        self.app.on_app_restart()
-
     @staticmethod
     def handle_style(style):
         # set current style
@@ -659,14 +647,13 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # first remove the toolbars:
         try:
             self.app.ui.removeToolBar(self.app.ui.toolbarfile)
-            self.app.ui.removeToolBar(self.app.ui.toolbargeo)
+            self.app.ui.removeToolBar(self.app.ui.toolbaredit)
             self.app.ui.removeToolBar(self.app.ui.toolbarview)
             self.app.ui.removeToolBar(self.app.ui.toolbarshell)
             self.app.ui.removeToolBar(self.app.ui.toolbartools)
             self.app.ui.removeToolBar(self.app.ui.exc_edit_toolbar)
             self.app.ui.removeToolBar(self.app.ui.geo_edit_toolbar)
             self.app.ui.removeToolBar(self.app.ui.grb_edit_toolbar)
-            self.app.ui.removeToolBar(self.app.ui.snap_toolbar)
             self.app.ui.removeToolBar(self.app.ui.toolbarshell)
         except Exception:
             pass
@@ -677,9 +664,9 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
             self.app.ui.toolbarfile.setObjectName('File_TB')
             self.app.ui.addToolBar(Qt.LeftToolBarArea, self.app.ui.toolbarfile)
 
-            self.app.ui.toolbargeo = QtWidgets.QToolBar('Edit Toolbar')
-            self.app.ui.toolbargeo.setObjectName('Edit_TB')
-            self.app.ui.addToolBar(Qt.LeftToolBarArea, self.app.ui.toolbargeo)
+            self.app.ui.toolbaredit = QtWidgets.QToolBar('Edit Toolbar')
+            self.app.ui.toolbaredit.setObjectName('Edit_TB')
+            self.app.ui.addToolBar(Qt.LeftToolBarArea, self.app.ui.toolbaredit)
 
             self.app.ui.toolbarshell = QtWidgets.QToolBar('Shell Toolbar')
             self.app.ui.toolbarshell.setObjectName('Shell_TB')
@@ -709,22 +696,15 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
             self.app.ui.exc_edit_toolbar.setObjectName('ExcEditor_TB')
             self.app.ui.addToolBar(Qt.RightToolBarArea, self.app.ui.exc_edit_toolbar)
 
-            self.app.ui.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar')
-            self.app.ui.snap_toolbar.setObjectName('Snap_TB')
-            self.app.ui.snap_toolbar.setMaximumHeight(30)
-            self.app.ui.splitter_left.addWidget(self.app.ui.snap_toolbar)
-
-            self.app.ui.corner_snap_btn.setVisible(True)
-            self.app.ui.snap_magnet.setVisible(True)
         else:
             # ## TOOLBAR INSTALLATION # ##
             self.app.ui.toolbarfile = QtWidgets.QToolBar('File Toolbar')
             self.app.ui.toolbarfile.setObjectName('File_TB')
             self.app.ui.addToolBar(self.app.ui.toolbarfile)
 
-            self.app.ui.toolbargeo = QtWidgets.QToolBar('Edit Toolbar')
-            self.app.ui.toolbargeo.setObjectName('Edit_TB')
-            self.app.ui.addToolBar(self.app.ui.toolbargeo)
+            self.app.ui.toolbaredit = QtWidgets.QToolBar('Edit Toolbar')
+            self.app.ui.toolbaredit.setObjectName('Edit_TB')
+            self.app.ui.addToolBar(self.app.ui.toolbaredit)
 
             self.app.ui.toolbarview = QtWidgets.QToolBar('View Toolbar')
             self.app.ui.toolbarview.setObjectName('View_TB')
@@ -755,18 +735,9 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
             self.app.ui.grb_edit_toolbar.setObjectName('GrbEditor_TB')
             self.app.ui.addToolBar(self.app.ui.grb_edit_toolbar)
 
-            self.app.ui.snap_toolbar = QtWidgets.QToolBar('Grid Toolbar')
-            self.app.ui.snap_toolbar.setObjectName('Snap_TB')
-            # self.app.ui.snap_toolbar.setMaximumHeight(30)
-            self.app.ui.addToolBar(self.app.ui.snap_toolbar)
-
-            self.app.ui.corner_snap_btn.setVisible(False)
-            self.app.ui.snap_magnet.setVisible(False)
-
         if current_layout == 'minimal':
             self.app.ui.toolbarview.setVisible(False)
             self.app.ui.toolbarshell.setVisible(False)
-            self.app.ui.snap_toolbar.setVisible(False)
             self.app.ui.geo_edit_toolbar.setVisible(False)
             self.app.ui.grb_edit_toolbar.setVisible(False)
             self.app.ui.exc_edit_toolbar.setVisible(False)
@@ -779,7 +750,9 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         self.app.connect_toolbar_signals()
 
         self.app.ui.grid_snap_btn.setChecked(True)
-        self.app.ui.on_grid_snap_triggered(state=True)
+
+        self.app.ui.corner_snap_btn.setVisible(False)
+        self.app.ui.snap_magnet.setVisible(False)
 
         self.app.ui.grid_gap_x_entry.setText(str(self.app.defaults["global_gridx"]))
         self.app.ui.grid_gap_y_entry.setText(str(self.app.defaults["global_gridy"]))

+ 4 - 4
flatcamGUI/preferences/general/GeneralPreferencesUI.py → AppGUI/preferences/general/GeneralPreferencesUI.py

@@ -1,12 +1,12 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.preferences.general.GeneralAppPrefGroupUI import GeneralAppPrefGroupUI
-from flatcamGUI.preferences.general.GeneralAPPSetGroupUI import GeneralAPPSetGroupUI
-from flatcamGUI.preferences.general.GeneralGUIPrefGroupUI import GeneralGUIPrefGroupUI
+from AppGUI.preferences.general.GeneralAppPrefGroupUI import GeneralAppPrefGroupUI
+from AppGUI.preferences.general.GeneralAPPSetGroupUI import GeneralAPPSetGroupUI
+from AppGUI.preferences.general.GeneralGUIPrefGroupUI import GeneralGUIPrefGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 0 - 0
flatcamGUI/preferences/general/__init__.py → AppGUI/preferences/general/__init__.py


+ 20 - 12
flatcamGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py → AppGUI/preferences/geometry/GeometryAdvOptPrefGroupUI.py

@@ -1,11 +1,12 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCEntry, FloatEntry, FCDoubleSpinner, FCCheckBox, RadioSet, FCLabel
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCLabel, NumericalEvalTupleEntry, \
+    NumericalEvalEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -46,8 +47,9 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         toolchange_xy_label.setToolTip(
             _("Toolchange X,Y position.")
         )
+        self.toolchangexy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
+
         grid1.addWidget(toolchange_xy_label, 1, 0)
-        self.toolchangexy_entry = FCEntry()
         grid1.addWidget(self.toolchangexy_entry, 1, 1)
 
         # Start move Z
@@ -56,8 +58,9 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
             _("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)
-        self.gstartz_entry = FloatEntry()
         grid1.addWidget(self.gstartz_entry, 2, 1)
 
         # Feedrate rapids
@@ -186,6 +189,11 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         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 ----------
         # -----------------------------
@@ -195,10 +203,10 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
               "Those parameters are available only for\n"
               "Advanced App. Level.")
         )
-        grid1.addWidget(self.adv_label, 12, 0, 1, 2)
+        grid1.addWidget(self.adv_label, 13, 0, 1, 2)
 
         # Exclusion Area CB
-        self.exclusion_cb = FCCheckBox('%s:' % _("Exclusion areas"))
+        self.exclusion_cb = FCCheckBox('%s' % _("Exclusion areas"))
         self.exclusion_cb.setToolTip(
             _(
                 "Include exclusion areas.\n"
@@ -206,7 +214,7 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
                 "is forbidden."
             )
         )
-        grid1.addWidget(self.exclusion_cb, 13, 0, 1, 2)
+        grid1.addWidget(self.exclusion_cb, 14, 0, 1, 2)
 
         # Area Selection shape
         self.area_shape_label = QtWidgets.QLabel('%s:' % _("Shape"))
@@ -217,8 +225,8 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         self.area_shape_radio = RadioSet([{'label': _("Square"), 'value': 'square'},
                                           {'label': _("Polygon"), 'value': 'polygon'}])
 
-        grid1.addWidget(self.area_shape_label, 14, 0)
-        grid1.addWidget(self.area_shape_radio, 14, 1)
+        grid1.addWidget(self.area_shape_label, 15, 0)
+        grid1.addWidget(self.area_shape_radio, 15, 1)
 
         # Chose Strategy
         self.strategy_label = FCLabel('%s:' % _("Strategy"))
@@ -229,8 +237,8 @@ class GeometryAdvOptPrefGroupUI(OptionsGroupUI):
         self.strategy_radio = RadioSet([{'label': _('Over'), 'value': 'over'},
                                         {'label': _('Around'), 'value': 'around'}])
 
-        grid1.addWidget(self.strategy_label, 15, 0)
-        grid1.addWidget(self.strategy_radio, 15, 1)
+        grid1.addWidget(self.strategy_label, 16, 0)
+        grid1.addWidget(self.strategy_radio, 16, 1)
 
         # Over Z
         self.over_z_label = FCLabel('%s:' % _("Over Z"))

+ 3 - 3
flatcamGUI/preferences/geometry/GeometryEditorPrefGroupUI.py → AppGUI/preferences/geometry/GeometryEditorPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCSpinner, RadioSet
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCSpinner, RadioSet
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 14 - 4
flatcamGUI/preferences/geometry/GeometryGenPrefGroupUI.py → AppGUI/preferences/geometry/GeometryGenPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets, QtCore, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCCheckBox, FCSpinner, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCCheckBox, FCSpinner, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -31,12 +31,22 @@ class GeometryGenPrefGroupUI(OptionsGroupUI):
         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.")
         )
-        self.layout.addWidget(self.plot_cb)
+        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)

+ 6 - 5
flatcamGUI/preferences/geometry/GeometryOptPrefGroupUI.py → AppGUI/preferences/geometry/GeometryOptPrefGroupUI.py

@@ -1,12 +1,13 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import Qt, QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner, FCCheckBox, OptionalInputSection, FCEntry, FCSpinner, FCComboBox
-from flatcamGUI.preferences import machinist_setting
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+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 FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -176,7 +177,7 @@ class GeometryOptPrefGroupUI(OptionsGroupUI):
               "If no value is entered then there is no move\n"
               "on X,Y plane at the end of the job.")
         )
-        self.endxy_entry = FCEntry()
+        self.endxy_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid1.addWidget(endmove_xy_label, 7, 0)
         grid1.addWidget(self.endxy_entry, 7, 1)

+ 5 - 5
flatcamGUI/preferences/geometry/GeometryPreferencesUI.py → AppGUI/preferences/geometry/GeometryPreferencesUI.py

@@ -1,13 +1,13 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.preferences.geometry.GeometryEditorPrefGroupUI import GeometryEditorPrefGroupUI
-from flatcamGUI.preferences.geometry.GeometryAdvOptPrefGroupUI import GeometryAdvOptPrefGroupUI
-from flatcamGUI.preferences.geometry.GeometryOptPrefGroupUI import GeometryOptPrefGroupUI
-from flatcamGUI.preferences.geometry.GeometryGenPrefGroupUI import GeometryGenPrefGroupUI
+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 FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 0 - 0
flatcamGUI/preferences/geometry/__init__.py → AppGUI/preferences/geometry/__init__.py


+ 3 - 82
flatcamGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py → AppGUI/preferences/gerber/GerberAdvOptPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCCheckBox, RadioSet, FCDoubleSpinner, FCSpinner, OptionalInputSection
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCCheckBox, RadioSet, FCDoubleSpinner, FCSpinner, OptionalInputSection
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -63,85 +63,6 @@ class GerberAdvOptPrefGroupUI(OptionsGroupUI):
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         grid0.addWidget(separator_line, 2, 0, 1, 2)
 
-        # Tool Type
-        self.tool_type_label = QtWidgets.QLabel('<b>%s</b>' % _('Tool Type'))
-        self.tool_type_label.setToolTip(
-            _("Choose which tool to use for Gerber isolation:\n"
-              "'Circular' or 'V-shape'.\n"
-              "When the 'V-shape' is selected then the tool\n"
-              "diameter will depend on the chosen cut depth.")
-        )
-        self.tool_type_radio = RadioSet([{'label': 'Circular', 'value': 'circular'},
-                                         {'label': 'V-Shape', 'value': 'v'}])
-
-        grid0.addWidget(self.tool_type_label, 3, 0)
-        grid0.addWidget(self.tool_type_radio, 3, 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_spinner = FCDoubleSpinner()
-        self.tipdia_spinner.set_precision(self.decimals)
-        self.tipdia_spinner.set_range(-99.9999, 99.9999)
-        self.tipdia_spinner.setSingleStep(0.1)
-        self.tipdia_spinner.setWrapping(True)
-        grid0.addWidget(self.tipdialabel, 4, 0)
-        grid0.addWidget(self.tipdia_spinner, 4, 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 degree.")
-        )
-        self.tipangle_spinner = FCSpinner()
-        self.tipangle_spinner.set_range(1, 180)
-        self.tipangle_spinner.set_step(5)
-        self.tipangle_spinner.setWrapping(True)
-        grid0.addWidget(self.tipanglelabel, 5, 0)
-        grid0.addWidget(self.tipangle_spinner, 5, 1, 1, 2)
-
-        # Cut Z
-        self.cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
-        self.cutzlabel.setToolTip(
-            _("Cutting depth (negative)\n"
-              "below the copper surface.")
-        )
-        self.cutz_spinner = FCDoubleSpinner()
-        self.cutz_spinner.set_precision(self.decimals)
-        self.cutz_spinner.set_range(-99.9999, 0.0000)
-        self.cutz_spinner.setSingleStep(0.1)
-        self.cutz_spinner.setWrapping(True)
-
-        grid0.addWidget(self.cutzlabel, 6, 0)
-        grid0.addWidget(self.cutz_spinner, 6, 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': _('Exterior'), 'value': 'ext'},
-                                        {'label': _('Interior'), 'value': 'int'}])
-
-        grid0.addWidget(self.iso_type_label, 7, 0,)
-        grid0.addWidget(self.iso_type_radio, 7, 1, 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)
-
         # Buffering Type
         buffering_label = QtWidgets.QLabel('%s:' % _('Buffering'))
         buffering_label.setToolTip(

+ 5 - 4
flatcamGUI/preferences/gerber/GerberEditorPrefGroupUI.py → AppGUI/preferences/gerber/GerberEditorPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCSpinner, FCDoubleSpinner, FCComboBox, FCEntry, RadioSet
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCSpinner, FCDoubleSpinner, FCComboBox, FCEntry, RadioSet, NumericalEvalTupleEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -109,8 +109,9 @@ class GerberEditorPrefGroupUI(OptionsGroupUI):
               "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)
-        self.adddim_entry = FCEntry()
         grid0.addWidget(self.adddim_entry, 5, 1)
 
         self.grb_array_linear_label = QtWidgets.QLabel('<b>%s:</b>' % _('Linear Pad Array'))

+ 3 - 3
flatcamGUI/preferences/gerber/GerberExpPrefGroupUI.py → AppGUI/preferences/gerber/GerberExpPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets, QtCore
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import RadioSet, FCSpinner
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import RadioSet, FCSpinner
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 12 - 12
flatcamGUI/preferences/gerber/GerberGenPrefGroupUI.py → AppGUI/preferences/gerber/GerberGenPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets, QtCore, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCCheckBox, FCSpinner, RadioSet, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -34,26 +34,26 @@ class GerberGenPrefGroupUI(OptionsGroupUI):
         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, 0)
+        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, 1)
-
-        # Plot CB
-        self.plot_cb = FCCheckBox(label='%s' % _('Plot'))
-        self.plot_options_label.setToolTip(
-            _("Plot (show) this object.")
-        )
-        grid0.addWidget(self.plot_cb, 0, 2)
+        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"))

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

+ 7 - 6
flatcamGUI/preferences/gerber/GerberPreferencesUI.py → AppGUI/preferences/gerber/GerberPreferencesUI.py

@@ -1,14 +1,14 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.preferences.gerber.GerberEditorPrefGroupUI import GerberEditorPrefGroupUI
-from flatcamGUI.preferences.gerber.GerberExpPrefGroupUI import GerberExpPrefGroupUI
-from flatcamGUI.preferences.gerber.GerberAdvOptPrefGroupUI import GerberAdvOptPrefGroupUI
-from flatcamGUI.preferences.gerber.GerberOptPrefGroupUI import GerberOptPrefGroupUI
-from flatcamGUI.preferences.gerber.GerberGenPrefGroupUI import GerberGenPrefGroupUI
+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 FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -44,6 +44,7 @@ class GerberPreferencesUI(QtWidgets.QWidget):
         self.vlay = QtWidgets.QVBoxLayout()
         self.vlay.addWidget(self.gerber_opt_group)
         self.vlay.addWidget(self.gerber_exp_group)
+        self.vlay.addStretch()
 
         self.layout.addWidget(self.gerber_gen_group)
         self.layout.addLayout(self.vlay)

+ 0 - 0
flatcamGUI/preferences/gerber/__init__.py → AppGUI/preferences/gerber/__init__.py


+ 3 - 3
flatcamGUI/preferences/tools/Tools2CThievingPrefGroupUI.py → AppGUI/preferences/tools/Tools2CThievingPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCSpinner, FCDoubleSpinner, RadioSet
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCSpinner, FCDoubleSpinner, RadioSet
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 4 - 4
flatcamGUI/preferences/tools/Tools2CalPrefGroupUI.py → AppGUI/preferences/tools/Tools2CalPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCCheckBox, NumericalEvalTupleEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -116,7 +116,7 @@ class Tools2CalPrefGroupUI(OptionsGroupUI):
               "(x, y) point will be used,")
         )
 
-        self.toolchange_xy_entry = FCEntry()
+        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)

+ 3 - 3
flatcamGUI/preferences/tools/Tools2EDrillsPrefGroupUI.py → AppGUI/preferences/tools/Tools2EDrillsPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCCheckBox, RadioSet, FCDoubleSpinner
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCCheckBox, RadioSet, FCDoubleSpinner
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/tools/Tools2FiducialsPrefGroupUI.py → AppGUI/preferences/tools/Tools2FiducialsPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner, RadioSet
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner, RadioSet
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/tools/Tools2InvertPrefGroupUI.py → AppGUI/preferences/tools/Tools2InvertPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner, RadioSet
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner, RadioSet
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/tools/Tools2OptimalPrefGroupUI.py → AppGUI/preferences/tools/Tools2OptimalPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCSpinner
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCSpinner
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 10 - 10
flatcamGUI/preferences/tools/Tools2PreferencesUI.py → AppGUI/preferences/tools/Tools2PreferencesUI.py

@@ -1,18 +1,18 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.preferences.tools.Tools2InvertPrefGroupUI import Tools2InvertPrefGroupUI
-from flatcamGUI.preferences.tools.Tools2PunchGerberPrefGroupUI import Tools2PunchGerberPrefGroupUI
-from flatcamGUI.preferences.tools.Tools2EDrillsPrefGroupUI import Tools2EDrillsPrefGroupUI
-from flatcamGUI.preferences.tools.Tools2CalPrefGroupUI import Tools2CalPrefGroupUI
-from flatcamGUI.preferences.tools.Tools2FiducialsPrefGroupUI import Tools2FiducialsPrefGroupUI
-from flatcamGUI.preferences.tools.Tools2CThievingPrefGroupUI import Tools2CThievingPrefGroupUI
-from flatcamGUI.preferences.tools.Tools2QRCodePrefGroupUI import Tools2QRCodePrefGroupUI
-from flatcamGUI.preferences.tools.Tools2OptimalPrefGroupUI import Tools2OptimalPrefGroupUI
-from flatcamGUI.preferences.tools.Tools2RulesCheckPrefGroupUI import Tools2RulesCheckPrefGroupUI
+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 FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/tools/Tools2PunchGerberPrefGroupUI.py → AppGUI/preferences/tools/Tools2PunchGerberPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCCheckBox, RadioSet, FCDoubleSpinner
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCCheckBox, RadioSet, FCDoubleSpinner
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 68 - 4
flatcamGUI/preferences/tools/Tools2QRCodePrefGroupUI.py → AppGUI/preferences/tools/Tools2QRCodePrefGroupUI.py

@@ -1,11 +1,11 @@
-from PyQt5 import QtWidgets, QtCore
+from PyQt5 import QtWidgets, QtCore, QtGui
 from PyQt5.QtCore import Qt, QSettings
 
-from flatcamGUI.GUIElements import FCSpinner, RadioSet, FCTextArea, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCSpinner, RadioSet, FCTextArea, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -205,3 +205,67 @@ class Tools2QRCodePrefGroupUI(OptionsGroupUI):
         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.fill_color_button.clicked.connect(self.on_qrcode_fill_color_button)
+        self.back_color_entry.editingFinished.connect(self.on_qrcode_back_color_entry)
+        self.back_color_button.clicked.connect(self.on_qrcode_back_color_button)
+
+    def on_qrcode_fill_color_entry(self):
+        self.app.defaults['tools_qrcode_fill_color'] = self.fill_color_entry.get_value()
+        self.fill_color_button.setStyleSheet(
+            "background-color:%s;"
+            "border-color: dimgray" % str(self.defaults['tools_qrcode_fill_color'])
+        )
+
+    def on_qrcode_fill_color_button(self):
+        current_color = QtGui.QColor(self.app.defaults['tools_qrcode_fill_color'])
+
+        c_dialog = QtWidgets.QColorDialog()
+        fill_color = c_dialog.getColor(initial=current_color)
+
+        if fill_color.isValid() is False:
+            return
+
+        # if new color is different then mark that the Preferences are changed
+        if fill_color != current_color:
+            self.app.preferencesUiManager.on_preferences_edited()
+
+        self.fill_color_button.setStyleSheet(
+            "background-color:%s;"
+            "border-color: dimgray" % str(fill_color.name())
+        )
+
+        new_val_sel = str(fill_color.name())
+        self.fill_color_entry.set_value(new_val_sel)
+        self.app.defaults['tools_qrcode_fill_color'] = new_val_sel
+
+    def on_qrcode_back_color_entry(self):
+        self.app.defaults['tools_qrcode_back_color'] = self.back_color_entry.get_value()
+        self.back_color_button.setStyleSheet(
+            "background-color:%s;"
+            "border-color: dimgray" % str(self.defaults['tools_qrcode_back_color'])
+        )
+
+    def on_qrcode_back_color_button(self):
+        current_color = QtGui.QColor(self.app.defaults['tools_qrcode_back_color'])
+
+        c_dialog = QtWidgets.QColorDialog()
+        back_color = c_dialog.getColor(initial=current_color)
+
+        if back_color.isValid() is False:
+            return
+
+        # if new color is different then mark that the Preferences are changed
+        if back_color != current_color:
+            self.app.preferencesUiManager.on_preferences_edited()
+
+        self.back_color_button.setStyleSheet(
+            "background-color:%s;"
+            "border-color: dimgray" % str(back_color.name())
+        )
+
+        new_val_sel = str(back_color.name())
+        self.back_color_entry.set_value(new_val_sel)
+        self.app.defaults['tools_qrcode_back_color'] = new_val_sel

+ 3 - 3
flatcamGUI/preferences/tools/Tools2RulesCheckPrefGroupUI.py → AppGUI/preferences/tools/Tools2RulesCheckPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCCheckBox, FCDoubleSpinner
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCCheckBox, FCDoubleSpinner
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/tools/Tools2sidedPrefGroupUI.py → AppGUI/preferences/tools/Tools2sidedPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner, RadioSet
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner, RadioSet
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/tools/ToolsCalculatorsPrefGroupUI.py → AppGUI/preferences/tools/ToolsCalculatorsPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 81 - 0
AppGUI/preferences/tools/ToolsCornersPrefGroupUI.py

@@ -0,0 +1,81 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from AppGUI.GUIElements import 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 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 = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
+        self.param_label.setToolTip(
+            _("Parameters used for this tool.")
+        )
+        grid0.addWidget(self.param_label, 0, 0, 1, 2)
+
+        # Thickness #
+        self.thick_label = QtWidgets.QLabel('%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, 1, 0)
+        grid0.addWidget(self.thick_entry, 1, 1)
+
+        # Length #
+        self.l_label = QtWidgets.QLabel('%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(-9999.9999, 9999.9999)
+        self.l_entry.set_precision(self.decimals)
+        self.l_entry.setSingleStep(10 ** -self.decimals)
+
+        # Margin #
+        self.margin_label = QtWidgets.QLabel('%s:' % _("Margin"))
+        self.margin_label.setToolTip(
+            _("Bounding box margin.")
+        )
+        self.margin_entry = FCDoubleSpinner()
+        self.margin_entry.set_range(-9999.9999, 9999.9999)
+        self.margin_entry.set_precision(self.decimals)
+        self.margin_entry.setSingleStep(0.1)
+
+        grid0.addWidget(self.margin_label, 2, 0)
+        grid0.addWidget(self.margin_entry, 2, 1)
+
+        grid0.addWidget(self.l_label, 4, 0)
+        grid0.addWidget(self.l_entry, 4, 1)
+
+        self.layout.addStretch()

+ 4 - 4
flatcamGUI/preferences/tools/ToolsCutoutPrefGroupUI.py → AppGUI/preferences/tools/ToolsCutoutPrefGroupUI.py

@@ -1,12 +1,12 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox
-from flatcamGUI.preferences import machinist_setting
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox
+from AppGUI.preferences import machinist_setting
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 36 - 4
flatcamGUI/preferences/tools/ToolsFilmPrefGroupUI.py → AppGUI/preferences/tools/ToolsFilmPrefGroupUI.py

@@ -1,11 +1,11 @@
-from PyQt5 import QtWidgets, QtCore
+from PyQt5 import QtWidgets, QtCore, QtGui
 from PyQt5.QtCore import Qt, QSettings
 
-from flatcamGUI.GUIElements import RadioSet, FCEntry, FCDoubleSpinner, FCCheckBox, FCComboBox
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import RadioSet, FCEntry, FCDoubleSpinner, FCCheckBox, FCComboBox
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -314,3 +314,35 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.pagesize_combo, 17, 1)
 
         self.layout.addStretch()
+
+        # Film Tool
+        self.film_color_entry.editingFinished.connect(self.on_film_color_entry)
+        self.film_color_button.clicked.connect(self.on_film_color_button)
+
+    def on_film_color_entry(self):
+        self.app.defaults['tools_film_color'] = self.film_color_entry.get_value()
+        self.film_color_button.setStyleSheet(
+            "background-color:%s;"
+            "border-color: dimgray" % str(self.defaults['tools_film_color'])
+        )
+
+    def on_film_color_button(self):
+        current_color = QtGui.QColor(self.app.defaults['tools_film_color'])
+
+        c_dialog = QtWidgets.QColorDialog()
+        film_color = c_dialog.getColor(initial=current_color)
+
+        if film_color.isValid() is False:
+            return
+
+        # if new color is different then mark that the Preferences are changed
+        if film_color != current_color:
+            self.app.preferencesUiManager.on_preferences_edited()
+
+        self.film_color_button.setStyleSheet(
+            "background-color:%s;"
+            "border-color: dimgray" % str(film_color.name())
+        )
+        new_val_sel = str(film_color.name())
+        self.film_color_entry.set_value(new_val_sel)
+        self.app.defaults['tools_film_color'] = new_val_sel

+ 319 - 0
AppGUI/preferences/tools/ToolsISOPrefGroupUI.py

@@ -0,0 +1,319 @@
+from PyQt5 import QtWidgets
+from PyQt5.QtCore import QSettings
+
+from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, 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 FlatCAM units.")
+        )
+        self.cutz_entry = FCDoubleSpinner()
+        self.cutz_entry.set_precision(self.decimals)
+        self.cutz_entry.set_range(-9999.9999, 0.0000)
+        self.cutz_entry.setSingleStep(0.1)
+
+        self.cutz_entry.setToolTip(
+           _("Depth of cut into material. Negative value.\n"
+             "In FlatCAM 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, 9999.9999)
+        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 when the selected tool is of type: 'iso_op':\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 when the selected tool is of type: 'iso_op':\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 isolate outside PCB features,\n"
+              "using the biggest tool and continue with the next tools,\n"
+              "from bigger to smaller, to isolate the copper features that\n"
+              "could not be cleared by previous tool, until there is\n"
+              "no more copper features to isolate or there are no more tools.\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)
+
+        # 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"
+              "- 'Selection' -> Isolate a selection of polygons.\n"
+              "- 'Reference Object' - will process the area specified by another object.")
+        )
+        self.select_combo = FCComboBox()
+        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)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 22, 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, 23, 0)
+        grid0.addWidget(self.plotting_radio, 23, 1, 1, 2)
+
+        self.layout.addStretch()

+ 12 - 12
flatcamGUI/preferences/tools/ToolsNCCPrefGroupUI.py → AppGUI/preferences/tools/ToolsNCCPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCEntry, RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, NumericalEvalTupleEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -45,7 +45,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
               "Valid values: 0.3, 1.0")
         )
         grid0.addWidget(ncctdlabel, 0, 0)
-        self.ncc_tool_dia_entry = FCEntry(border_color='#0069A9')
+        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)
 
@@ -227,7 +227,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         # ], orientation='vertical', stretch=False)
         self.ncc_method_combo = FCComboBox()
         self.ncc_method_combo.addItems(
-            [_("Standard"), _("Seed"), _("Lines")]
+            [_("Standard"), _("Seed"), _("Lines"), _("Combo")]
         )
 
         grid0.addWidget(methodlabel, 12, 0)
@@ -285,7 +285,7 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(separator_line, 16, 0, 1, 2)
 
         # Rest machining CheckBox
-        self.ncc_rest_cb = FCCheckBox('%s' % _("Rest Machining"))
+        self.ncc_rest_cb = FCCheckBox('%s' % _("Rest"))
         self.ncc_rest_cb.setToolTip(
             _("If checked, use 'rest machining'.\n"
               "Basically it will clear copper outside PCB features,\n"
@@ -336,14 +336,14 @@ class ToolsNCCPrefGroupUI(OptionsGroupUI):
         grid0.addWidget(separator_line, 20, 0, 1, 2)
 
         # ## Plotting type
-        self.ncc_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
-                                            {"label": _("Progressive"), "value": "progressive"}])
-        plotting_label = QtWidgets.QLabel('%s:' % _("NCC Plotting"))
+        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 NCC job\n"
-              "- 'Progressive' - after each shape is generated it will be plotted.")
+            _("- '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.ncc_plotting_radio, 21, 1)
+        grid0.addWidget(self.plotting_radio, 21, 1)
 
         self.layout.addStretch()

+ 10 - 10
flatcamGUI/preferences/tools/ToolsPaintPrefGroupUI.py → AppGUI/preferences/tools/ToolsPaintPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCEntry, RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import RadioSet, FCDoubleSpinner, FCComboBox, FCCheckBox, NumericalEvalTupleEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -53,7 +53,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         )
         grid0.addWidget(ptdlabel, 0, 0)
 
-        self.painttooldia_entry = FCEntry(border_color='#0069A9')
+        self.painttooldia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
         self.painttooldia_entry.setPlaceholderText(_("Comma separated values"))
 
         grid0.addWidget(self.painttooldia_entry, 0, 1)
@@ -241,8 +241,8 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
         grid0.addWidget(separator_line, 13, 0, 1, 2)
 
-        self.rest_cb = FCCheckBox('%s' % _("Rest Machining"))
-        self.rest_cb.setObjectName(_("Rest Machining"))
+        self.rest_cb = FCCheckBox('%s' % _("Rest"))
+        self.rest_cb.setObjectName(_("Rest"))
         self.rest_cb.setToolTip(
             _("If checked, use 'rest machining'.\n"
               "Basically it will clear copper outside PCB features,\n"
@@ -277,7 +277,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         # )
         self.selectmethod_combo = FCComboBox()
         self.selectmethod_combo.addItems(
-            [_("Polygon Selection"), _("Area Selection"), _("All Polygons"), _("Reference Object")]
+            [_("Polygon Selection"), _("Area Selection"), _("All"), _("Reference Object")]
         )
 
         grid0.addWidget(selectlabel, 15, 0)
@@ -302,10 +302,10 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         # ## Plotting type
         self.paint_plotting_radio = RadioSet([{'label': _('Normal'), 'value': 'normal'},
                                               {"label": _("Progressive"), "value": "progressive"}])
-        plotting_label = QtWidgets.QLabel('%s:' % _("Paint Plotting"))
+        plotting_label = QtWidgets.QLabel('%s:' % _("Plotting"))
         plotting_label.setToolTip(
-            _("- 'Normal' -  normal plotting, done at the end of the Paint job\n"
-              "- 'Progressive' - after each shape is generated it will be plotted.")
+            _("- '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)

+ 3 - 3
flatcamGUI/preferences/tools/ToolsPanelizePrefGroupUI.py → AppGUI/preferences/tools/ToolsPanelizePrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner, FCSpinner, RadioSet, FCCheckBox
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner, FCSpinner, RadioSet, FCCheckBox
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 23 - 12
flatcamGUI/preferences/tools/ToolsPreferencesUI.py → AppGUI/preferences/tools/ToolsPreferencesUI.py

@@ -1,19 +1,22 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.preferences.tools.ToolsSubPrefGroupUI import ToolsSubPrefGroupUI
-from flatcamGUI.preferences.tools.ToolsSolderpastePrefGroupUI import ToolsSolderpastePrefGroupUI
-from flatcamGUI.preferences.tools.ToolsTransformPrefGroupUI import ToolsTransformPrefGroupUI
-from flatcamGUI.preferences.tools.ToolsCalculatorsPrefGroupUI import ToolsCalculatorsPrefGroupUI
-from flatcamGUI.preferences.tools.ToolsPanelizePrefGroupUI import ToolsPanelizePrefGroupUI
-from flatcamGUI.preferences.tools.ToolsFilmPrefGroupUI import ToolsFilmPrefGroupUI
-from flatcamGUI.preferences.tools.ToolsPaintPrefGroupUI import ToolsPaintPrefGroupUI
-from flatcamGUI.preferences.tools.Tools2sidedPrefGroupUI import Tools2sidedPrefGroupUI
-from flatcamGUI.preferences.tools.ToolsCutoutPrefGroupUI import ToolsCutoutPrefGroupUI
-from flatcamGUI.preferences.tools.ToolsNCCPrefGroupUI import ToolsNCCPrefGroupUI
+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
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -35,6 +38,9 @@ class ToolsPreferencesUI(QtWidgets.QWidget):
         self.setLayout(self.layout)
         self.decimals = decimals
 
+        self.tools_iso_group = ToolsISOPrefGroupUI(decimals=self.decimals)
+        self.tools_iso_group.setMinimumWidth(220)
+
         self.tools_ncc_group = ToolsNCCPrefGroupUI(decimals=self.decimals)
         self.tools_ncc_group.setMinimumWidth(220)
 
@@ -62,6 +68,9 @@ class ToolsPreferencesUI(QtWidgets.QWidget):
         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)
 
@@ -71,7 +80,7 @@ class ToolsPreferencesUI(QtWidgets.QWidget):
 
         self.vlay1 = QtWidgets.QVBoxLayout()
         self.vlay1.addWidget(self.tools_paint_group)
-        self.vlay1.addWidget(self.tools_panelize_group)
+        self.vlay1.addWidget(self.tools_iso_group)
 
         self.vlay2 = QtWidgets.QVBoxLayout()
         self.vlay2.addWidget(self.tools_transform_group)
@@ -84,6 +93,8 @@ class ToolsPreferencesUI(QtWidgets.QWidget):
 
         self.vlay4 = QtWidgets.QVBoxLayout()
         self.vlay4.addWidget(self.tools_solderpaste_group)
+        self.vlay4.addWidget(self.tools_corners_group)
+        self.vlay4.addWidget(self.tools_panelize_group)
 
         self.layout.addLayout(self.vlay)
         self.layout.addLayout(self.vlay1)

+ 5 - 5
flatcamGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py → AppGUI/preferences/tools/ToolsSolderpastePrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCEntry, FCDoubleSpinner, FCSpinner, FCComboBox
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner, FCSpinner, FCComboBox, NumericalEvalTupleEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -45,7 +45,7 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI):
               "The value of the diameter has to use the dot decimals separator.\n"
               "Valid values: 0.3, 1.0")
         )
-        self.nozzle_tool_dia_entry = FCEntry()
+        self.nozzle_tool_dia_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid0.addWidget(nozzletdlabel, 0, 0)
         grid0.addWidget(self.nozzle_tool_dia_entry, 0, 1)
@@ -130,7 +130,7 @@ class ToolsSolderpastePrefGroupUI(OptionsGroupUI):
         grid0.addWidget(self.z_toolchange_entry, 6, 1)
 
         # X,Y Toolchange location
-        self.xy_toolchange_entry = FCEntry()
+        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"

+ 3 - 3
flatcamGUI/preferences/tools/ToolsSubPrefGroupUI.py → AppGUI/preferences/tools/ToolsSubPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCCheckBox
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCCheckBox
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 4 - 4
flatcamGUI/preferences/tools/ToolsTransformPrefGroupUI.py → AppGUI/preferences/tools/ToolsTransformPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCDoubleSpinner, FCCheckBox, EvalEntry2
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCDoubleSpinner, FCCheckBox, NumericalEvalTupleEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -191,7 +191,7 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
               "The 'x' in (x, y) will be used when using Flip on X and\n"
               "the 'y' in (x, y) will be used when using Flip on Y and")
         )
-        self.flip_ref_entry = EvalEntry2("(0, 0)")
+        self.flip_ref_entry = NumericalEvalTupleEntry(border_color='#0069A9')
 
         grid0.addWidget(self.flip_ref_label, 14, 0, 1, 2)
         grid0.addWidget(self.flip_ref_entry, 15, 0, 1, 2)

+ 0 - 0
flatcamGUI/preferences/tools/__init__.py → AppGUI/preferences/tools/__init__.py


+ 3 - 3
flatcamGUI/preferences/utilities/AutoCompletePrefGroupUI.py → AppGUI/preferences/utilities/AutoCompletePrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCButton, FCTextArea, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCButton, FCTextArea, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/utilities/FAExcPrefGroupUI.py → AppGUI/preferences/utilities/FAExcPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import VerticalScrollArea, FCButton, FCTextArea, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import VerticalScrollArea, FCButton, FCTextArea, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/utilities/FAGcoPrefGroupUI.py → AppGUI/preferences/utilities/FAGcoPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCButton, FCTextArea, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCButton, FCTextArea, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 3 - 3
flatcamGUI/preferences/utilities/FAGrbPrefGroupUI.py → AppGUI/preferences/utilities/FAGrbPrefGroupUI.py

@@ -1,11 +1,11 @@
 from PyQt5 import QtWidgets, QtGui
 from PyQt5.QtCore import QSettings
 
-from flatcamGUI.GUIElements import FCButton, FCTextArea, FCEntry
-from flatcamGUI.preferences.OptionsGroupUI import OptionsGroupUI
+from AppGUI.GUIElements import FCButton, FCTextArea, FCEntry
+from AppGUI.preferences.OptionsGroupUI import OptionsGroupUI
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 4 - 4
flatcamGUI/preferences/utilities/UtilPreferencesUI.py → AppGUI/preferences/utilities/UtilPreferencesUI.py

@@ -1,9 +1,9 @@
 from PyQt5 import QtWidgets
 
-from flatcamGUI.preferences.utilities.AutoCompletePrefGroupUI import AutoCompletePrefGroupUI
-from flatcamGUI.preferences.utilities.FAGrbPrefGroupUI import FAGrbPrefGroupUI
-from flatcamGUI.preferences.utilities.FAGcoPrefGroupUI import FAGcoPrefGroupUI
-from flatcamGUI.preferences.utilities.FAExcPrefGroupUI import FAExcPrefGroupUI
+from AppGUI.preferences.utilities.AutoCompletePrefGroupUI import AutoCompletePrefGroupUI
+from AppGUI.preferences.utilities.FAGrbPrefGroupUI import FAGrbPrefGroupUI
+from AppGUI.preferences.utilities.FAGcoPrefGroupUI import FAGcoPrefGroupUI
+from AppGUI.preferences.utilities.FAExcPrefGroupUI import FAExcPrefGroupUI
 
 
 class UtilPreferencesUI(QtWidgets.QWidget):

+ 0 - 0
flatcamGUI/preferences/utilities/__init__.py → AppGUI/preferences/utilities/__init__.py


+ 394 - 0
AppObjects/AppObject.py

@@ -0,0 +1,394 @@
+# ###########################################################
+# FlatCAM: 2D Post-processing for Manufacturing             #
+# http://flatcam.org                                        #
+# Author: Juan Pablo Caram (c)                              #
+# Date: 2/5/2014                                            #
+# MIT Licence                                               #
+# Modified by Marius Stanciu (2020)                         #
+# ###########################################################
+
+from PyQt5 import QtCore
+from AppObjects.ObjectCollection import *
+from AppObjects.FlatCAMCNCJob import CNCJobObject
+from AppObjects.FlatCAMDocument import DocumentObject
+from AppObjects.FlatCAMExcellon import ExcellonObject
+from AppObjects.FlatCAMGeometry import GeometryObject
+from AppObjects.FlatCAMGerber import GerberObject
+from AppObjects.FlatCAMScript import ScriptObject
+
+import time
+import traceback
+
+# FlatCAM Translation
+import gettext
+import AppTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class AppObject(QtCore.QObject):
+
+    # Emitted by app_obj.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 app_obj.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)
+
+    plots_updated = QtCore.pyqtSignal()
+
+    def __init__(self, app):
+        super(AppObject, self).__init__()
+        self.app = app
+        self.inform = app.inform
+
+        # signals that are emitted when object state changes
+        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.app.on_plots_updated)
+
+    def new_object(self, kind, name, initialize, 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
+        :param plot: If to plot the resulting object
+        :param autoselected: if the resulting object is autoselected in the Project tab and therefore in the
+        self.collection
+        :return: None
+        :rtype: None
+        """
+
+        log.debug("AppObject.new_object()")
+        obj_plot = plot
+        obj_autoselected = autoselected
+
+        t0 = time.time()  # Debug
+
+        # ## Create object
+        classdict = {
+            "gerber": GerberObject,
+            "excellon": ExcellonObject,
+            "cncjob": CNCJobObject,
+            "geometry": GeometryObject,
+            "script": ScriptObject,
+            "document": DocumentObject
+        }
+
+        log.debug("Calling object constructor...")
+
+        # Object creation/instantiation
+        obj = classdict[kind](name)
+
+        obj.units = self.app.options["units"]
+
+        # 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. Above the App default values from self.defaults are
+        # copied to 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.app.options:
+            if option.find(kind + "_") == 0:
+                oname = option[len(kind) + 1:]
+                obj.options[oname] = self.app.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()
+        log.debug("%f seconds before initialize()." % (t1 - t0))
+        try:
+            return_value = initialize(obj, self.app)
+        except Exception as e:
+            msg = '[ERROR_NOTCL] %s' % _("An internal error has occurred. See shell.\n")
+            msg += _("Object ({kind}) failed because: {error} \n\n").format(kind=kind, error=str(e))
+            msg += traceback.format_exc()
+            self.app.inform.emit(msg)
+            return "fail"
+
+        t2 = time.time()
+        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.app.options["units"].upper() != obj.units.upper():
+            self.app.inform.emit('%s: %s' % (_("Converting units to "), self.app.options["units"]))
+            obj.convert_units(self.app.options["units"])
+            t3 = time.time()
+            log.debug("%f seconds converting units." % (t3 - t2))
+
+        # Create the bounding box for the object and then add the results to the obj.options
+        # But not for Scripts or for Documents
+        if kind != 'document' and kind != 'script':
+            try:
+                xmin, ymin, xmax, ymax = obj.bounds()
+                obj.options['xmin'] = xmin
+                obj.options['ymin'] = ymin
+                obj.options['xmax'] = xmax
+                obj.options['ymax'] = ymax
+            except Exception as e:
+                log.warning("AppObject.new_object() -> The object has no bounds properties. %s" % str(e))
+                return "fail"
+
+            try:
+                if kind == 'excellon':
+                    obj.fill_color = self.app.defaults["excellon_plot_fill"]
+                    obj.outline_color = self.app.defaults["excellon_plot_line"]
+
+                if kind == 'gerber':
+                    obj.fill_color = self.app.defaults["gerber_plot_fill"]
+                    obj.outline_color = self.app.defaults["gerber_plot_line"]
+            except Exception as e:
+                log.warning("AppObject.new_object() -> setting colors error. %s" % str(e))
+
+        # update the KeyWords list with the name of the file
+        self.app.myKeywords.append(obj.options['name'])
+
+        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.app.main_thread)
+        self.object_created.emit(obj, obj_plot, obj_autoselected)
+
+        return obj
+
+    def new_excellon_object(self):
+        """
+        Creates a new, blank Excellon object.
+
+        :return: None
+        """
+
+        self.new_object('excellon', 'new_exc', lambda x, y: None, plot=False)
+
+    def new_geometry_object(self):
+        """
+        Creates a new, blank and single-tool Geometry object.
+
+        :return: None
+        """
+
+        def initialize(obj, app):
+            obj.multitool = False
+
+        self.new_object('geometry', 'new_geo', initialize, plot=False)
+
+    def new_gerber_object(self):
+        """
+        Creates a new, blank Gerber object.
+
+        :return: None
+        """
+
+        def initialize(grb_obj, app):
+            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 new_script_object(self):
+        """
+        Creates a new, blank TCL Script object.
+
+        :return: None
+        """
+
+        # commands_list = "# AddCircle, AddPolygon, AddPolyline, AddRectangle, AlignDrill, " \
+        #                 "AlignDrillGrid, Bbox, Bounds, ClearShell, CopperClear,\n" \
+        #                 "# Cncjob, Cutout, Delete, Drillcncjob, ExportDXF, ExportExcellon, ExportGcode,\n" \
+        #                 "# ExportGerber, ExportSVG, Exteriors, Follow, GeoCutout, GeoUnion, GetNames,\n" \
+        #                 "# GetSys, ImportSvg, Interiors, Isolate, JoinExcellon, JoinGeometry, " \
+        #                 "ListSys, MillDrills,\n" \
+        #                 "# MillSlots, Mirror, New, NewExcellon, NewGeometry, NewGerber, Nregions, " \
+        #                 "Offset, OpenExcellon, OpenGCode, OpenGerber, OpenProject,\n" \
+        #                 "# Options, Paint, Panelize, PlotAl, PlotObjects, SaveProject, " \
+        #                 "SaveSys, Scale, SetActive, SetSys, SetOrigin, Skew, SubtractPoly,\n" \
+        #                 "# SubtractRectangle, Version, WriteGCode\n"
+
+        new_source_file = '# %s\n' % _('CREATE A NEW FLATCAM TCL SCRIPT') + \
+                          '# %s:\n' % _('TCL Tutorial is here') + \
+                          '# https://www.tcl.tk/man/tcl8.5/tutorial/tcltutorial.html\n' + '\n\n' + \
+                          '# %s:\n' % _("FlatCAM commands list")
+        new_source_file += '# %s\n\n' % _("Type >help< followed by Run Code for a list of FlatCAM Tcl Commands "
+                                          "(displayed in Tcl Shell).")
+
+        def initialize(obj, app):
+            obj.source_file = deepcopy(new_source_file)
+
+        outname = 'new_script'
+        self.new_object('script', outname, initialize, plot=False)
+
+    def new_document_object(self):
+        """
+        Creates a new, blank Document object.
+
+        :return: None
+        """
+
+        def initialize(obj, app):
+            obj.source_file = ""
+
+        self.new_object('document', 'new_document', initialize, plot=False)
+
+    def on_object_created(self, obj, plot, auto_select):
+        """
+        Event callback for object creation.
+        It will add the new object to the collection. After that it will plot the object in a threaded way
+
+        :param obj: The newly created FlatCAM object.
+        :param plot: if the newly create object t obe plotted
+        :param auto_select: if the newly created object to be autoselected after creation
+        :return: None
+        """
+        t0 = time.time()  # DEBUG
+        log.debug("on_object_created()")
+
+        # The Collection might change the name if there is a collision
+        self.app.collection.append(obj)
+
+        # after adding the object to the collection always update the list of objects that are in the collection
+        self.app.all_objects_list = self.app.collection.get_list()
+
+        # self.app.inform.emit('[selected] %s created & selected: %s' %
+        #                  (str(obj.kind).capitalize(), str(obj.options['name'])))
+        if obj.kind == 'gerber':
+            self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='green',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
+        elif obj.kind == 'excellon':
+            self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='brown',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
+        elif obj.kind == 'cncjob':
+            self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='blue',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
+        elif obj.kind == 'geometry':
+            self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='red',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
+        elif obj.kind == 'script':
+            self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='orange',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
+        elif obj.kind == 'document':
+            self.app.inform.emit('[selected] {kind} {tx}: <span style="color:{color};">{name}</span>'.format(
+                kind=obj.kind.capitalize(),
+                color='darkCyan',
+                name=str(obj.options['name']), tx=_("created/selected"))
+            )
+
+        # update the SHELL auto-completer model with the name of the new object
+        self.app.shell._edit.set_model_data(self.app.myKeywords)
+
+        if auto_select:
+            # select the just opened object but deselect the previous ones
+            self.app.collection.set_all_inactive()
+            self.app.collection.set_active(obj.options["name"])
+        else:
+            self.app.collection.set_all_inactive()
+
+        # here it is done the object plotting
+        def task(t_obj):
+            with self.app.proc_container.new(_("Plotting")):
+                if t_obj.kind == 'cncjob':
+                    t_obj.plot(kind=self.app.defaults["cncjob_plot_kind"])
+                else:
+                    t_obj.plot()
+
+                t1 = time.time()  # DEBUG
+                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.app.worker_task.emit({'fcn': task, 'params': [obj]})
+
+    def on_object_changed(self, obj):
+        """
+        Called whenever the geometry of the object was changed in some way.
+        This require the update of it's bounding values so it can be the selected on canvas.
+        Update the bounding box data from obj.options
+
+        :param obj: the object that was changed
+        :return: None
+        """
+
+        try:
+            xmin, ymin, xmax, ymax = obj.bounds()
+        except TypeError:
+            return
+        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.app.delete_selection_shape()
+        self.app.should_we_save = True
+
+    def on_object_plotted(self):
+        """
+        Callback called whenever the plotted object needs to be fit into the viewport (canvas)
+
+        :return: None
+        """
+        self.app.on_zoom_fit()

+ 17 - 14
flatcamObjects/FlatCAMCNCJob.py → AppObjects/FlatCAMCNCJob.py

@@ -14,8 +14,8 @@ from copy import deepcopy
 from io import StringIO
 from datetime import datetime
 
-from flatcamEditors.FlatCAMTextEditor import TextEditor
-from flatcamObjects.FlatCAMObj import *
+from AppEditors.FlatCAMTextEditor import TextEditor
+from AppObjects.FlatCAMObj import *
 
 from camlib import CNCjob
 
@@ -24,7 +24,7 @@ import sys
 import math
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -506,10 +506,10 @@ class CNCJobObject(FlatCAMObj, CNCjob):
             filename, _f = FCFileSaveDialog.get_saved_filename(
                 caption=_("Export Machine Code ..."),
                 directory=dir_file_to_save,
-                filter=_filter_
+                ext_filter=_filter_
             )
         except TypeError:
-            filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Machine Code ..."), filter=_filter_)
+            filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Machine Code ..."), ext_filter=_filter_)
 
         filename = str(filename)
 
@@ -564,7 +564,7 @@ class CNCJobObject(FlatCAMObj, CNCjob):
 
         # 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.app.ui.rel_position_label.setText("")
 
         # first clear previous text in text editor (if any)
         self.gcode_editor_tab.code_editor.clear()
@@ -989,7 +989,6 @@ class CNCJobObject(FlatCAMObj, CNCjob):
                 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] %s' %
@@ -1107,7 +1106,7 @@ class CNCJobObject(FlatCAMObj, CNCjob):
                 except ValueError:
                     # we may have a tuple with only one element and a comma
                     dia_plot = [float(el) for el in self.options["tooldia"].split(',') if el != ''][0]
-                self.plot2(dia_plot, obj=self, visible=visible, kind=kind)
+                self.plot2(tooldia=dia_plot, obj=self, visible=visible, kind=kind)
             else:
                 # multiple tools usage
                 if self.cnc_tools:
@@ -1117,12 +1116,16 @@ class CNCJobObject(FlatCAMObj, CNCjob):
                         self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
 
                 # TODO: until the gcode parsed will be stored on each Excellon tool this will not get executed
-                if self.exc_cnc_tools:
-                    for tooldia_key in self.exc_cnc_tools:
-                        tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
-                        # gcode_parsed = self.cnc_tools[tooldia_key]['gcode_parsed']
-                        gcode_parsed = self.gcode_parsed
-                        self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
+                # I do this so the travel lines thickness will reflect the tool diameter
+                # may work only for objects created within the app and not Gcode imported from elsewhere for which we
+                # don't know the origin
+                if self.origin_kind == "excellon":
+                    if self.exc_cnc_tools:
+                        for tooldia_key in self.exc_cnc_tools:
+                            tooldia = float('%.*f' % (self.decimals, float(tooldia_key)))
+                            # gcode_parsed = self.exc_cnc_tools[tooldia_key]['gcode_parsed']
+                            gcode_parsed = self.gcode_parsed
+                            self.plot2(tooldia=tooldia, obj=self, visible=visible, gcode_parsed=gcode_parsed, kind=kind)
 
             self.shapes.redraw()
         except (ObjectDeleted, AttributeError):

+ 24 - 3
flatcamObjects/FlatCAMDocument.py → AppObjects/FlatCAMDocument.py

@@ -10,11 +10,11 @@
 # File modified by: Marius Stanciu                         #
 # ##########################################################
 
-from flatcamEditors.FlatCAMTextEditor import TextEditor
-from flatcamObjects.FlatCAMObj import *
+from AppEditors.FlatCAMTextEditor import TextEditor
+from AppObjects.FlatCAMObj import *
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -284,6 +284,27 @@ class DocumentObject(FlatCAMObj):
         self.ui.sel_color_entry.set_value(new_val)
         self.app.defaults['document_sel_color'] = new_val
 
+    def mirror(self, axis, point):
+        pass
+
+    def offset(self, vect):
+        pass
+
+    def rotate(self, angle, point):
+        pass
+
+    def scale(self, xfactor, yfactor=None, point=None):
+        pass
+
+    def skew(self, angle_x, angle_y, point):
+        pass
+
+    def buffer(self, distance, join, factor=None):
+        pass
+
+    def bounds(self, flatten=False):
+        return None, None, None, None
+
     def to_dict(self):
         """
         Returns a representation of the object as a dictionary.

+ 367 - 42
flatcamObjects/FlatCAMExcellon.py → AppObjects/FlatCAMExcellon.py

@@ -15,13 +15,14 @@ from shapely.geometry import Point, LineString
 
 from copy import deepcopy
 
-from flatcamParsers.ParseExcellon import Excellon
-from flatcamObjects.FlatCAMObj import *
+from AppParsers.ParseExcellon import Excellon
+from AppObjects.FlatCAMObj import *
 
 import itertools
+import numpy as np
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -31,7 +32,7 @@ if '_' not in builtins.__dict__:
 
 class ExcellonObject(FlatCAMObj, Excellon):
     """
-    Represents Excellon/Drill code.
+    Represents Excellon/Drill code. An object stored in the FlatCAM objects collection (a dict)
     """
 
     ui_type = ExcellonObjectUI
@@ -50,6 +51,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.options.update({
             "plot": True,
             "solid": False,
+            "multicolored": False,
 
             "operation": "drill",
             "milling_type": "drills",
@@ -125,6 +127,10 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.outline_color = self.app.defaults['excellon_plot_line']
         self.alpha_level = 'bf'
 
+        # store here the state of the exclusion checkbox state to be restored after building the UI
+        # TODO add this in the sel.app.defaults dict and in Preferences
+        self.exclusion_area_cb_is_checked = False
+
         # Attributes to be included in serialization
         # Always append to it because it carries contents
         # from predecessors.
@@ -142,9 +148,11 @@ class ExcellonObject(FlatCAMObj, Excellon):
 
         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 ExcellonObject Objects to join.
-        :param exc_final: Destination ExcellonObject object.
-        :return: None
+        :param exc_list:    List or one object of ExcellonObject Objects to join.
+        :type exc_list:     list
+        :param exc_final:   Destination ExcellonObject object.
+        :type exc_final:    class
+        :return:            None
         """
 
         if decimals is None:
@@ -312,8 +320,23 @@ class ExcellonObject(FlatCAMObj, Excellon):
         exc_final.create_geometry()
 
     def build_ui(self):
+        """
+        Will (re)build the Excellon UI updating it (the tool table)
+
+        :return:    None
+        :rtype:
+        """
         FlatCAMObj.build_ui(self)
 
+        # Area Exception - exclusion shape added signal
+        # first disconnect it from any other object
+        try:
+            self.app.exc_areas.e_shape_modified.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        # then connect it to the current build_ui() method
+        self.app.exc_areas.e_shape_modified.connect(self.update_exclusion_table)
+
         self.units = self.app.defaults['units'].upper()
 
         for row in range(self.ui.tools_table.rowCount()):
@@ -514,6 +537,58 @@ class ExcellonObject(FlatCAMObj, Excellon):
                 "<b>%s: <font color='#0000FF'>%s</font></b>" % (_('Parameters for'), _("Multiple Tools"))
             )
 
+        # Build Exclusion Areas section
+        e_len = len(self.app.exc_areas.exclusion_areas_storage)
+        self.ui.exclusion_table.setRowCount(e_len)
+
+        area_id = 0
+
+        for area in range(e_len):
+            area_id += 1
+
+            area_dict = self.app.exc_areas.exclusion_areas_storage[area]
+
+            area_id_item = QtWidgets.QTableWidgetItem('%d' % int(area_id))
+            area_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 0, area_id_item)  # Area id
+
+            object_item = QtWidgets.QTableWidgetItem('%s' % area_dict["obj_type"])
+            object_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 1, object_item)  # Origin Object
+
+            strategy_item = QtWidgets.QTableWidgetItem('%s' % area_dict["strategy"])
+            strategy_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 2, strategy_item)  # Strategy
+
+            overz_item = QtWidgets.QTableWidgetItem('%s' % area_dict["overz"])
+            overz_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 3, overz_item)  # Over Z
+
+        self.ui.exclusion_table.resizeColumnsToContents()
+        self.ui.exclusion_table.resizeRowsToContents()
+
+        area_vheader = self.ui.exclusion_table.verticalHeader()
+        area_vheader.hide()
+        self.ui.exclusion_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        area_hheader = self.ui.exclusion_table.horizontalHeader()
+        area_hheader.setMinimumSectionSize(10)
+        area_hheader.setDefaultSectionSize(70)
+
+        area_hheader.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        area_hheader.resizeSection(0, 20)
+        area_hheader.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        area_hheader.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        area_hheader.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+
+        # area_hheader.setStretchLastSection(True)
+        self.ui.exclusion_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.exclusion_table.setColumnWidth(0, 20)
+
+        self.ui.exclusion_table.setMinimumHeight(self.ui.exclusion_table.getHeight())
+        self.ui.exclusion_table.setMaximumHeight(self.ui.exclusion_table.getHeight())
+
         self.ui_connect()
 
     def set_ui(self, ui):
@@ -521,9 +596,9 @@ class ExcellonObject(FlatCAMObj, Excellon):
         Configures the user interface for this object.
         Connects options to form fields.
 
-        :param ui: User interface object.
-        :type ui: ExcellonObjectUI
-        :return: None
+        :param ui:  User interface object.
+        :type ui:   ExcellonObjectUI
+        :return:    None
         """
         FlatCAMObj.set_ui(self, ui)
 
@@ -534,6 +609,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.form_fields.update({
             "plot": self.ui.plot_cb,
             "solid": self.ui.solid_cb,
+            "multicolored": self.ui.multicolored_cb,
 
             "operation": self.ui.operation_radio,
             "milling_type": self.ui.milling_type_radio,
@@ -567,7 +643,11 @@ class ExcellonObject(FlatCAMObj, Excellon):
             "ppname_g": self.ui.pp_geo_name_cb,
             "z_pdepth": self.ui.pdepth_entry,
             "feedrate_probe": self.ui.feedrate_probe_entry,
-            # "gcode_type": self.ui.excellon_gcode_type_radio
+            # "gcode_type": self.ui.excellon_gcode_type_radio,
+            "area_exclusion": self.ui.exclusion_cb,
+            "area_shape": self.ui.area_shape_radio,
+            "area_strategy": self.ui.strategy_radio,
+            "area_overz": self.ui.over_z_entry,
         })
 
         self.name2option = {
@@ -628,12 +708,24 @@ class ExcellonObject(FlatCAMObj, Excellon):
 
         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.multicolored_cb.stateChanged.connect(self.on_multicolored_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)
 
+        # Exclusion areas signals
+        self.ui.exclusion_table.horizontalHeader().sectionClicked.connect(self.exclusion_table_toggle_all)
+        self.ui.exclusion_table.lost_focus.connect(self.clear_selection)
+        self.ui.exclusion_table.itemClicked.connect(self.draw_sel_shape)
+        self.ui.add_area_button.clicked.connect(self.on_add_area_click)
+        self.ui.delete_area_button.clicked.connect(self.on_clear_area_click)
+        self.ui.delete_sel_area_button.clicked.connect(self.on_delete_sel_areas)
+        self.ui.strategy_radio.activated_custom.connect(self.on_strategy)
+
         self.on_operation_type(val='drill')
         self.ui.operation_radio.activated_custom.connect(self.on_operation_type)
 
@@ -650,6 +742,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui.operation_radio.setEnabled(False)
 
     def ui_connect(self):
+        """
+        Will connect all signals in the Excellon UI that needs to be connected
+
+        :return:    None
+        :rtype:
+        """
 
         # selective plotting
         for row in range(self.ui.tools_table.rowCount() - 2):
@@ -672,6 +770,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
                 current_widget.returnPressed.connect(self.form_to_storage)
 
     def ui_disconnect(self):
+        """
+        Will disconnect all signals in the Excellon UI that needs to be disconnected
+
+        :return:    None
+        :rtype:
+        """
         # selective plotting
         for row in range(self.ui.tools_table.rowCount()):
             try:
@@ -714,6 +818,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
                     pass
 
     def on_row_selection_change(self):
+        """
+        Called when the user clicks on a row in Tools Table
+
+        :return:    None
+        :rtype:
+        """
         self.ui_disconnect()
 
         sel_rows = []
@@ -764,6 +874,14 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui_connect()
 
     def storage_to_form(self, dict_storage):
+        """
+        Will update the GUI with data from the "storage" in this case the dict self.tools
+
+        :param dict_storage:    A dictionary holding the data relevant for gnerating Gcode from Excellon
+        :type dict_storage:     dict
+        :return:                None
+        :rtype:
+        """
         for form_key in self.form_fields:
             for storage_key in dict_storage:
                 if form_key == storage_key and form_key not in \
@@ -775,6 +893,12 @@ class ExcellonObject(FlatCAMObj, Excellon):
                         pass
 
     def form_to_storage(self):
+        """
+        Will update the 'storage' attribute which is the dict self.tools with data collected from GUI
+
+        :return:    None
+        :rtype:
+        """
         if self.ui.tools_table.rowCount() == 0:
             # there is no tool in tool table so we can't save the GUI elements values to storage
             return
@@ -803,6 +927,14 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.ui_connect()
 
     def on_operation_type(self, val):
+        """
+        Called by a RadioSet activated_custom signal
+
+        :param val:     Parameter passes by the signal that called this method
+        :type val:      str
+        :return:        None
+        :rtype:
+        """
         if val == 'mill':
             self.ui.mill_type_label.show()
             self.ui.milling_type_radio.show()
@@ -831,10 +963,10 @@ class ExcellonObject(FlatCAMObj, Excellon):
     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.
+        to the selections on the tool list in the AppGUI.
 
-        :return: List of tools.
-        :rtype: list
+        :return:    List of tools.
+        :rtype:     list
         """
 
         return [str(x.text()) for x in self.ui.tools_table.selectedItems()]
@@ -843,8 +975,8 @@ class ExcellonObject(FlatCAMObj, Excellon):
         """
         Returns a list of lists, each list in the list is made out of row elements
 
-        :return: List of table_tools items.
-        :rtype: list
+        :return:    List of table_tools items.
+        :rtype:     list
         """
         table_tools_items = []
         for x in self.ui.tools_table.selectedItems():
@@ -872,7 +1004,21 @@ class ExcellonObject(FlatCAMObj, Excellon):
     def export_excellon(self, whole, fract, e_zeros=None, form='dec', factor=1, slot_type='routing'):
         """
         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
+
+        :param whole:       Integer part digits
+        :type whole:        int
+        :param fract:       Fractional part digits
+        :type fract:        int
+        :param e_zeros:     Excellon zeros suppression: LZ or TZ
+        :type e_zeros:      str
+        :param form:        Excellon format: 'dec',
+        :type form:         str
+        :param factor:      Conversion factor
+        :type factor:       float
+        :param slot_type:   How to treat slots: "routing" or "drilling"
+        :type slot_type:    str
+        :return:            A tuple: (has_slots, Excellon_code) -> (bool, str)
+        :rtype:             tuple
         """
 
         excellon_code = ''
@@ -1039,13 +1185,25 @@ class ExcellonObject(FlatCAMObj, Excellon):
 
     def generate_milling_drills(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False):
         """
+        Will generate an Geometry Object allowing to cut a drill hole instead of drilling it.
+
         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
+        :param tools:       A list of tools where the drills are to be milled or a string: "all"
+        :type tools:
+        :param outname:     the name of the resulting Geometry object
+        :type outname:      str
+        :param tooldia:     the tool diameter to be used in creation of the milling path (Geometry Object)
+        :type tooldia:      float
+        :param plot:        if to plot the resulting object
+        :type plot:         bool
+        :param use_thread:  if to use threading for creation of the Geometry object
+        :type use_thread:   bool
+        :return:            Success/failure condition tuple (bool, str).
+        :rtype:             tuple
         """
 
         # Get the tools from the list. These are keys
@@ -1088,6 +1246,15 @@ class ExcellonObject(FlatCAMObj, Excellon):
                 return False, "Error: Milling tool is larger than hole."
 
         def geo_init(geo_obj, app_obj):
+            """
+
+            :param geo_obj:     New object
+            :type geo_obj:      GeometryObject
+            :param app_obj:     App
+            :type app_obj:      FlatCAMApp.App
+            :return:
+            :rtype:
+            """
             assert geo_obj.kind == 'geometry', "Initializer expected a GeometryObject, got %s" % type(geo_obj)
 
             # ## Add properties to the object
@@ -1100,7 +1267,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
             geo_obj.options['Tools_in_use'] = tool_table_items
             geo_obj.options['type'] = 'Excellon Geometry'
             geo_obj.options["cnctooldia"] = str(tooldia)
-
+            geo_obj.options["multidepth"] = self.options["multidepth"]
             geo_obj.solid_geometry = []
 
             # in case that the tool used has the same diameter with the hole, and since the maximum resolution
@@ -1115,9 +1282,10 @@ class ExcellonObject(FlatCAMObj, Excellon):
                     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, plot=plot)
+            def geo_thread(a_obj):
+                a_obj.app_obj.new_object("geometry", outname, geo_init, plot=plot)
 
             # Create a promise with the new name
             self.app.collection.promise(outname)
@@ -1125,19 +1293,31 @@ class ExcellonObject(FlatCAMObj, Excellon):
             # Send to worker
             self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
         else:
-            self.app.new_object("geometry", outname, geo_init, plot=plot)
+            self.app.app_obj.new_object("geometry", outname, geo_init, plot=plot)
 
         return True, ""
 
-    def generate_milling_slots(self, tools=None, outname=None, tooldia=None, plot=True, use_thread=False):
+    def generate_milling_slots(self, tools=None, outname=None, tooldia=None, plot=False, use_thread=False):
         """
+        Will generate an Geometry Object allowing to cut/mill a slot hole.
+
         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
+        :param tools:       A list of tools where the drills are to be milled or a string: "all"
+        :type tools:
+        :param outname:     the name of the resulting Geometry object
+        :type outname:      str
+        :param tooldia:     the tool diameter to be used in creation of the milling path (Geometry Object)
+        :type tooldia:      float
+        :param plot:        if to plot the resulting object
+        :type plot:         bool
+        :param use_thread:  if to use threading for creation of the Geometry object
+        :type use_thread:   bool
+        :return:            Success/failure condition tuple (bool, str).
+        :rtype:             tuple
         """
 
         # Get the tools from the list. These are keys
@@ -1178,7 +1358,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
                 return False, "Error: Milling tool is larger than hole."
 
         def geo_init(geo_obj, app_obj):
-            assert geo_obj.kind == 'geometry' "Initializer expected a GeometryObject, got %s" % type(geo_obj)
+            assert geo_obj.kind == 'geometry', "Initializer expected a GeometryObject, got %s" % type(geo_obj)
 
             # ## Add properties to the object
 
@@ -1190,7 +1370,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
             geo_obj.options['Tools_in_use'] = tool_table_items
             geo_obj.options['type'] = 'Excellon Geometry'
             geo_obj.options["cnctooldia"] = str(tooldia)
-
+            geo_obj.options["multidepth"] = self.options["multidepth"]
             geo_obj.solid_geometry = []
 
             # in case that the tool used has the same diameter with the hole, and since the maximum resolution
@@ -1220,8 +1400,8 @@ class ExcellonObject(FlatCAMObj, Excellon):
                         geo_obj.solid_geometry.append(poly)
 
         if use_thread:
-            def geo_thread(app_obj):
-                app_obj.new_object("geometry", outname + '_slot', geo_init, plot=plot)
+            def geo_thread(a_obj):
+                a_obj.app_obj.new_object("geometry", outname + '_slot', geo_init, plot=plot)
 
             # Create a promise with the new name
             self.app.collection.promise(outname)
@@ -1229,7 +1409,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
             # Send to worker
             self.app.worker_task.emit({'fcn': geo_thread, 'params': [self.app]})
         else:
-            self.app.new_object("geometry", outname + '_slot', geo_init, plot=plot)
+            self.app.app_obj.new_object("geometry", outname + '_slot', geo_init, plot=plot)
 
         return True, ""
 
@@ -1237,13 +1417,13 @@ class ExcellonObject(FlatCAMObj, Excellon):
         self.app.defaults.report_usage("excellon_on_create_milling_drills button")
         self.read_form()
 
-        self.generate_milling_drills(use_thread=False)
+        self.generate_milling_drills(use_thread=False, plot=True)
 
     def on_generate_milling_slots_button_click(self, *args):
         self.app.defaults.report_usage("excellon_on_create_milling_slots_button")
         self.read_form()
 
-        self.generate_milling_slots(use_thread=False)
+        self.generate_milling_slots(use_thread=False, plot=True)
 
     def on_pp_changed(self):
         current_pp = self.ui.pp_excellon_name_cb.get_value()
@@ -1363,7 +1543,7 @@ class ExcellonObject(FlatCAMObj, Excellon):
         job_name = self.options["name"] + "_cnc"
         pp_excellon_name = self.options["ppname_e"]
 
-        # Object initialization function for app.new_object()
+        # Object initialization function for app.app_obj.new_object()
         def job_init(job_obj, app_obj):
             assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
 
@@ -1426,9 +1606,9 @@ class ExcellonObject(FlatCAMObj, Excellon):
             job_obj.create_geometry()
 
         # To be run in separate thread
-        def job_thread(app_obj):
+        def job_thread(a_obj):
             with self.app.proc_container.new(_("Generating CNC Code")):
-                app_obj.new_object("cncjob", job_name, job_init)
+                a_obj.app_obj.new_object("cncjob", job_name, job_init)
 
         # Create promise for the new name.
         self.app.collection.promise(job_name)
@@ -1466,12 +1646,122 @@ class ExcellonObject(FlatCAMObj, Excellon):
         #     self.options['startz'] = float(self.options['startz']) * factor
         # self.options['endz'] = float(self.options['endz']) * factor
 
+    def on_add_area_click(self):
+        shape_button = self.ui.area_shape_radio
+        overz_button = self.ui.over_z_entry
+        strategy_radio = self.ui.strategy_radio
+        cnc_button = self.ui.generate_cnc_button
+        solid_geo = self.solid_geometry
+        obj_type = self.kind
+
+        self.app.exc_areas.on_add_area_click(
+            shape_button=shape_button, overz_button=overz_button, cnc_button=cnc_button, strategy_radio=strategy_radio,
+            solid_geo=solid_geo, obj_type=obj_type)
+
+    def on_clear_area_click(self):
+        if not self.app.exc_areas.exclusion_areas_storage:
+            self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. There are no exclusion areas to delete."))
+            return
+
+        self.app.exc_areas.on_clear_area_click()
+        self.app.exc_areas.e_shape_modified.emit()
+
+    def on_delete_sel_areas(self):
+        sel_model = self.ui.exclusion_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
+        # so the duplicate rows will not be added
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if not sel_rows:
+            self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. Nothing is selected."))
+            return
+
+        self.app.exc_areas.delete_sel_shapes(idxs=list(sel_rows))
+        self.app.exc_areas.e_shape_modified.emit()
+
+    def draw_sel_shape(self):
+        sel_model = self.ui.exclusion_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())
+
+        self.delete_sel_shape()
+
+        if self.app.is_legacy is False:
+            face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.2 * 255)))[2:]
+            outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(0.8 * 255)))[2:]
+        else:
+            face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.4 * 255)))[2:]
+            outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(1.0 * 255)))[2:]
+
+        for row in sel_rows:
+            sel_rect = self.app.exc_areas.exclusion_areas_storage[row]['shape']
+            self.app.move_tool.sel_shapes.add(sel_rect, color=outline, face_color=face, update=True, layer=0,
+                                              tolerance=None)
+        if self.app.is_legacy is True:
+            self.app.move_tool.sel_shapes.redraw()
+
+    def clear_selection(self):
+        self.app.delete_selection_shape()
+        # self.ui.exclusion_table.clearSelection()
+
+    def delete_sel_shape(self):
+        self.app.delete_selection_shape()
+
+    def update_exclusion_table(self):
+        self.exclusion_area_cb_is_checked = True if self.ui.exclusion_cb.isChecked() else False
+
+        self.build_ui()
+        self.ui.exclusion_cb.set_value(self.exclusion_area_cb_is_checked)
+
+    def on_strategy(self, val):
+        if val == 'around':
+            self.ui.over_z_label.setDisabled(True)
+            self.ui.over_z_entry.setDisabled(True)
+        else:
+            self.ui.over_z_label.setDisabled(False)
+            self.ui.over_z_entry.setDisabled(False)
+
+    def exclusion_table_toggle_all(self):
+        """
+        will toggle the selection of all rows in Exclusion Areas table
+
+        :return:
+        """
+        sel_model = self.ui.exclusion_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 sel_rows:
+            self.ui.exclusion_table.clearSelection()
+            self.delete_sel_shape()
+        else:
+            self.ui.exclusion_table.selectAll()
+            self.draw_sel_shape()
+
     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_plot_cb_click(self, *args):
         if self.muted_ui:
             return
@@ -1545,6 +1835,27 @@ class ExcellonObject(FlatCAMObj, Excellon):
         if not FlatCAMObj.plot(self):
             return
 
+        if self.app.is_legacy is False:
+            def random_color():
+                r_color = np.random.rand(4)
+                r_color[3] = 1
+                return r_color
+        else:
+            def random_color():
+                while True:
+                    r_color = np.random.rand(4)
+                    r_color[3] = 1
+
+                    new_color = '#'
+                    for idx in range(len(r_color)):
+                        new_color += '%x' % int(r_color[idx] * 255)
+                    # do it until a valid color is generated
+                    # a valid color has the # symbol, another 6 chars for the color and the last 2 chars for alpha
+                    # for a total of 9 chars
+                    if len(new_color) == 9:
+                        break
+                return new_color
+
         # try:
         #     # Plot Excellon (All polygons?)
         #     if self.options["solid"]:
@@ -1576,12 +1887,26 @@ class ExcellonObject(FlatCAMObj, Excellon):
         try:
             # Plot Excellon (All polygons?)
             if self.options["solid"]:
-                for geo in self.solid_geometry:
-                    self.add_shape(shape=geo,
-                                   color=self.outline_color,
-                                   face_color=self.fill_color,
-                                   visible=visible,
-                                   layer=2)
+                # for geo in self.solid_geometry:
+                #     self.add_shape(shape=geo,
+                #                    color=self.outline_color,
+                #                    face_color=random_color() if self.options['multicolored'] else self.fill_color,
+                #                    visible=visible,
+                #                    layer=2)
+
+                # plot polygons for each tool separately
+                for tool in self.tools:
+                    # set the color here so we have one color for each tool
+                    geo_color = random_color()
+
+                    # tool is a dict also
+                    for geo in self.tools[tool]["solid_geometry"]:
+                        self.add_shape(shape=geo,
+                                       color=geo_color if self.options['multicolored'] else self.outline_color,
+                                       face_color=geo_color if self.options['multicolored'] else self.fill_color,
+                                       visible=visible,
+                                       layer=2)
+
             else:
                 for geo in self.solid_geometry:
                     self.add_shape(shape=geo.exterior, color='red', visible=visible)

+ 269 - 283
flatcamObjects/FlatCAMGeometry.py → AppObjects/FlatCAMGeometry.py

@@ -15,8 +15,7 @@ import shapely.affinity as affinity
 
 from camlib import Geometry
 
-from flatcamObjects.FlatCAMObj import *
-import FlatCAMTool
+from AppObjects.FlatCAMObj import *
 
 import ezdxf
 import math
@@ -25,7 +24,7 @@ from copy import deepcopy
 import traceback
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -53,6 +52,7 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         self.options.update({
             "plot": True,
+            "multicolored": False,
             "cutz": -0.002,
             "vtipdia": 0.1,
             "vtipangle": 30,
@@ -151,17 +151,9 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         self.param_fields = {}
 
-        # 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.exclusion_areas_list = []
-        self.first_click = False
-        self.points = []
-        self.poly_drawn = False
+        # store here the state of the exclusion checkbox state to be restored after building the UI
+        # TODO add this in the sel.app.defaults dict and in Preferences
+        self.exclusion_area_cb_is_checked = False
 
         # Attributes to be included in serialization
         # Always append to it because it carries contents
@@ -172,6 +164,15 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui_disconnect()
         FlatCAMObj.build_ui(self)
 
+        # Area Exception - exclusion shape added signal
+        # first disconnect it from any other object
+        try:
+            self.app.exc_areas.e_shape_modified.disconnect()
+        except (TypeError, AttributeError):
+            pass
+        # then connect it to the current build_ui() method
+        self.app.exc_areas.e_shape_modified.connect(self.update_exclusion_table)
+
         self.units = self.app.defaults['units']
 
         tool_idx = 0
@@ -192,7 +193,6 @@ class GeometryObject(FlatCAMObj, Geometry):
             # For INCH the decimals should be no more than 3. There are no tools under 10mils.
 
             dia_item = QtWidgets.QTableWidgetItem('%.*f' % (self.decimals, float(tooluid_value['tooldia'])))
-
             dia_item.setFlags(QtCore.Qt.ItemIsEnabled)
 
             offset_item = FCComboBox()
@@ -305,6 +305,63 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         self.set_tool_offset_visibility(selected_row)
 
+        # -----------------------------
+        # Build Exclusion Areas section
+        # -----------------------------
+        e_len = len(self.app.exc_areas.exclusion_areas_storage)
+        self.ui.exclusion_table.setRowCount(e_len)
+
+        area_id = 0
+
+        for area in range(e_len):
+            area_id += 1
+
+            area_dict = self.app.exc_areas.exclusion_areas_storage[area]
+
+            area_id_item = QtWidgets.QTableWidgetItem('%d' % int(area_id))
+            area_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 0, area_id_item)  # Area id
+
+            object_item = QtWidgets.QTableWidgetItem('%s' % area_dict["obj_type"])
+            object_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 1, object_item)  # Origin Object
+
+            strategy_item = QtWidgets.QTableWidgetItem('%s' % area_dict["strategy"])
+            strategy_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 2, strategy_item)  # Strategy
+
+            overz_item = QtWidgets.QTableWidgetItem('%s' % area_dict["overz"])
+            overz_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
+            self.ui.exclusion_table.setItem(area, 3, overz_item)  # Over Z
+
+        self.ui.exclusion_table.resizeColumnsToContents()
+        self.ui.exclusion_table.resizeRowsToContents()
+
+        area_vheader = self.ui.exclusion_table.verticalHeader()
+        area_vheader.hide()
+        self.ui.exclusion_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        area_hheader = self.ui.exclusion_table.horizontalHeader()
+        area_hheader.setMinimumSectionSize(10)
+        area_hheader.setDefaultSectionSize(70)
+
+        area_hheader.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
+        area_hheader.resizeSection(0, 20)
+        area_hheader.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
+        area_hheader.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
+        area_hheader.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
+
+        # area_hheader.setStretchLastSection(True)
+        self.ui.exclusion_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+
+        self.ui.exclusion_table.setColumnWidth(0, 20)
+
+        self.ui.exclusion_table.setMinimumHeight(self.ui.exclusion_table.getHeight())
+        self.ui.exclusion_table.setMaximumHeight(self.ui.exclusion_table.getHeight())
+
+        # End Build Exclusion Areas
+        # -----------------------------
+
         # 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'])
@@ -340,6 +397,7 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         self.form_fields.update({
             "plot": self.ui.plot_cb,
+            "multicolored": self.ui.multicolored_cb,
             "cutz": self.ui.cutz_entry,
             "vtipdia": self.ui.tipdia_entry,
             "vtipangle": self.ui.tipangle_entry,
@@ -363,7 +421,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             "endxy": self.ui.endxy_entry,
             "cnctooldia": self.ui.addtool_entry,
             "area_exclusion": self.ui.exclusion_cb,
-            "area_shape":self.ui.area_shape_radio,
+            "area_shape": self.ui.area_shape_radio,
             "area_strategy": self.ui.strategy_radio,
             "area_overz": self.ui.over_z_entry,
         })
@@ -535,6 +593,8 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui.extracut_cb.toggled.connect(lambda state: self.ui.e_cut_entry.setDisabled(not state))
 
         self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
+        self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_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.generate_ncc_button.clicked.connect(lambda: self.app.ncclear_tool.run(toggle=False))
@@ -547,8 +607,14 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui.apply_param_to_all.clicked.connect(self.on_apply_param_to_all_clicked)
         self.ui.cutz_entry.returnPressed.connect(self.on_cut_z_changed)
 
+        # Exclusion areas signals
+        self.ui.exclusion_table.horizontalHeader().sectionClicked.connect(self.exclusion_table_toggle_all)
+        self.ui.exclusion_table.lost_focus.connect(self.clear_selection)
+        self.ui.exclusion_table.itemClicked.connect(self.draw_sel_shape)
         self.ui.add_area_button.clicked.connect(self.on_add_area_click)
         self.ui.delete_area_button.clicked.connect(self.on_clear_area_click)
+        self.ui.delete_sel_area_button.clicked.connect(self.on_delete_sel_areas)
+        self.ui.strategy_radio.activated_custom.connect(self.on_strategy)
 
     def on_cut_z_changed(self):
         self.old_cutz = self.ui.cutz_entry.get_value()
@@ -595,8 +661,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                                 self.ui.tool_offset_entry.get_value().replace(',', '.')
                             )
                         except ValueError:
-                            self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                                 _("Wrong value format entered, use a number."))
+                            self.app.inform.emit('[ERROR_NOTCL] %s' % _("Wrong value format entered, use a number."))
                             return
 
     def ui_connect(self):
@@ -604,6 +669,7 @@ class GeometryObject(FlatCAMObj, Geometry):
         # changes in geometry UI
         for i in self.param_fields:
             current_widget = self.param_fields[i]
+
             if isinstance(current_widget, FCCheckBox):
                 current_widget.stateChanged.connect(self.gui_form_to_storage)
             elif isinstance(current_widget, FCComboBox):
@@ -874,7 +940,7 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui_connect()
         self.build_ui()
 
-        # if there is no tool left in the Tools Table, enable the parameters GUI
+        # if there is no tool left in the Tools Table, enable the parameters AppGUI
         if self.ui.geo_tools_table.rowCount() != 0:
             self.ui.geo_param_frame.setDisabled(False)
 
@@ -950,7 +1016,7 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui_connect()
         self.build_ui()
 
-        # if there is no tool left in the Tools Table, enable the parameters GUI
+        # if there is no tool left in the Tools Table, enable the parameters AppGUI
         if self.ui.geo_tools_table.rowCount() != 0:
             self.ui.geo_param_frame.setDisabled(False)
 
@@ -1131,7 +1197,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                 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 there is no tool left in the Tools Table, disable the parameters AppGUI
         if self.ui.geo_tools_table.rowCount() == 0:
             self.ui.geo_param_frame.setDisabled(True)
 
@@ -1149,8 +1215,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                   "- 'V-tip Angle' -> angle at the tip of the tool\n"
                   "- 'V-tip Dia' -> diameter at the tip of the tool \n"
                   "- Tool Dia -> 'Dia' column found in the Tool Table\n"
-                  "NB: a value of zero means that Tool Dia = 'V-tip Dia'"
-                )
+                  "NB: a value of zero means that Tool Dia = 'V-tip Dia'")
             )
             self.ui.cutz_entry.setToolTip(
                 _("Disabled because the tool is V-shape.\n"
@@ -1159,8 +1224,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                   "- 'V-tip Angle' -> angle at the tip of the tool\n"
                   "- 'V-tip Dia' -> diameter at the tip of the tool \n"
                   "- Tool Dia -> 'Dia' column found in the Tool Table\n"
-                  "NB: a value of zero means that Tool Dia = 'V-tip Dia'"
-                  )
+                  "NB: a value of zero means that Tool Dia = 'V-tip Dia'")
             )
 
             self.update_cutz()
@@ -1172,8 +1236,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             self.ui.cutz_entry.setDisabled(False)
             self.ui.cutzlabel.setToolTip(
                 _("Cutting depth (negative)\n"
-                  "below the copper surface."
-                )
+                  "below the copper surface.")
             )
             self.ui.cutz_entry.setToolTip('')
 
@@ -1328,16 +1391,18 @@ class GeometryObject(FlatCAMObj, Geometry):
         self.ui_connect()
 
     def gui_form_to_storage(self):
+        self.ui_disconnect()
+
         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("GeometryObject.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:
+        except Exception as e:
+            log.debug("GeometryObject.gui_form_to_storage() -- wdg index -> %s" % str(e))
             return
 
         # those are the indexes for the V-Tip Dia and V-Tip Angle, if edited calculate the new Cut Z
@@ -1690,15 +1755,11 @@ class GeometryObject(FlatCAMObj, Geometry):
         The actual work is done by the target CNCJobObject object's
         `generate_from_geometry_2()` method.
 
-        :param tools_dict: a dictionary that holds the whole data needed to create the Gcode
-        (including the solid_geometry)
-
-        :param tools_in_use: the tools that are used, needed by some preprocessors
-        :type list of lists, each list in the list is made out of row elements of tools table from GUI
-
         :param outname:
-        :param tools_dict:
-        :param tools_in_use:
+        :param tools_dict:      a dictionary that holds the whole data needed to create the Gcode
+                                (including the solid_geometry)
+        :param tools_in_use:    the tools that are used, needed by some preprocessors
+        :type  tools_in_use     list of lists, each list in the list is made out of row elements of tools table from AppGUI
         :param segx:            number of segments on the X axis, for auto-levelling
         :param segy:            number of segments on the Y axis, for auto-levelling
         :param plot:            if True the generated object will be plotted; if False will not be plotted
@@ -1728,7 +1789,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             self.app.inform.emit(msg)
             return
 
-        # Object initialization function for app.new_object()
+        # Object initialization function for app.app_obj.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")
@@ -1742,7 +1803,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             # count the tools
             tool_cnt = 0
 
-            dia_cnc_dict = {}
+            # dia_cnc_dict = {}
 
             # this turn on the FlatCAMCNCJob plot for multiple tools
             job_obj.multitool = True
@@ -1868,7 +1929,7 @@ class GeometryObject(FlatCAMObj, Geometry):
                 })
                 dia_cnc_dict.clear()
 
-        # Object initialization function for app.new_object()
+        # Object initialization function for app.app_obj.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")
@@ -1882,7 +1943,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             # count the tools
             tool_cnt = 0
 
-            dia_cnc_dict = {}
+            # dia_cnc_dict = {}
 
             # this turn on the FlatCAMCNCJob plot for multiple tools
             job_obj.multitool = True
@@ -2022,15 +2083,15 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         if use_thread:
             # To be run in separate thread
-            def job_thread(app_obj):
+            def job_thread(a_obj):
                 if self.multigeo is False:
                     with self.app.proc_container.new(_("Generating CNC Code")):
-                        if app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot) != 'fail':
-                            app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
+                        if a_obj.app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot) != 'fail':
+                            a_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
                 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] %s: %s' % (_("CNCjob created"), outname))
+                        if a_obj.app_obj.new_object("cncjob", outname, job_init_multi_geometry) != 'fail':
+                            a_obj.inform.emit('[success] %s: %s' % (_("CNCjob created"), outname))
 
             # Create a promise with the name
             self.app.collection.promise(outname)
@@ -2038,25 +2099,17 @@ class GeometryObject(FlatCAMObj, Geometry):
             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, plot=plot)
+                self.app.app_obj.new_object("cncjob", outname, job_init_single_geometry, plot=plot)
             else:
-                self.app.new_object("cncjob", outname, job_init_multi_geometry, plot=plot)
-
-    def generatecncjob(
-            self, outname=None,
-            dia=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, extracut_length=None, startz=None, endz=None,
-            pp=None,
-            segx=None, segy=None,
-            use_thread=True,
-            plot=True):
+                self.app.app_obj.new_object("cncjob", outname, job_init_multi_geometry, plot=plot)
+
+    def generatecncjob(self, outname=None, dia=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, dpp=None, toolchange=None, toolchangez=None, toolchangexy=None,
+            extracut=None, extracut_length=None, startz=None, endz=None, endxy=None, pp=None, segx=None, segy=None,
+            use_thread=True, plot=True):
         """
-        Only used for TCL Command.
+        Only used by the TCL Command Cncjob.
         Creates a CNCJob out of this Geometry object. The actual
         work is done by the target camlib.CNCjob
         `generate_from_geometry_2()` method.
@@ -2073,14 +2126,17 @@ class GeometryObject(FlatCAMObj, Geometry):
         :param dwell:
         :param dwelltime:
         :param multidepth:
-        :param depthperpass:
+        :param dpp:             Depth for each pass when multidepth parameter is True
         :param toolchange:
         :param toolchangez:
-        :param toolchangexy:
+        :param toolchangexy:    A sequence ox X,Y coordinates: a 2-length tuple or a string.
+                                Coordinates in X,Y plane for the Toolchange event
         :param extracut:
         :param extracut_length:
         :param startz:
         :param endz:
+        :param endxy:           A sequence ox X,Y coordinates: a 2-length tuple or a string.
+                                Coordinates in X, Y plane for the last move after ending the job.
         :param pp:              Name of the preprocessor
         :param segx:
         :param segy:
@@ -2100,7 +2156,7 @@ class GeometryObject(FlatCAMObj, Geometry):
         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"])
+        depthperpass = dpp if dpp 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'])
@@ -2110,10 +2166,21 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         startz = startz if startz is not None else self.options["startz"]
         endz = endz if endz is not None else float(self.options["endz"])
-        endxy = self.options["endxy"]
+
+        endxy = endxy if endxy else self.options["endxy"]
+        if isinstance(endxy, str):
+            endxy = re.sub('[()\[\]]', '', endxy)
+            if endxy and endxy != '':
+                endxy = [float(eval(a)) for a in endxy.split(",")]
 
         toolchangez = toolchangez if toolchangez else float(self.options["toolchangez"])
+
         toolchangexy = toolchangexy if toolchangexy else self.options["toolchangexy"]
+        if isinstance(toolchangexy, str):
+            toolchangexy = re.sub('[()\[\]]', '', toolchangexy)
+            if toolchangexy and toolchangexy != '':
+                toolchangexy = [float(eval(a)) for a in toolchangexy.split(",")]
+
         toolchange = toolchange if toolchange else self.options["toolchange"]
 
         offset = offset if offset else 0.0
@@ -2125,7 +2192,7 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         ppname_g = pp if pp else self.options["ppname_g"]
 
-        # Object initialization function for app.new_object()
+        # Object initialization function for app.app_obj.new_object()
         # RUNNING ON SEPARATE THREAD!
         def job_init(job_obj, app_obj):
             assert job_obj.kind == 'cncjob', "Initializer expected a CNCJobObject, got %s" % type(job_obj)
@@ -2174,7 +2241,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             # 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, plot=plot)
+                    app_obj.app_obj.new_object("cncjob", outname, job_init, plot=plot)
                     app_obj.inform.emit('[success] %s: %s' % (_("CNCjob created")), outname)
 
             # Create a promise with the name
@@ -2182,7 +2249,7 @@ class GeometryObject(FlatCAMObj, Geometry):
             # Send to worker
             self.app.worker_task.emit({'fcn': job_thread, 'params': [self.app]})
         else:
-            self.app.new_object("cncjob", outname, job_init, plot=plot)
+            self.app.app_obj.new_object("cncjob", outname, job_init, plot=plot)
 
     # def on_plot_cb_click(self, *args):
     #     if self.muted_ui:
@@ -2469,6 +2536,110 @@ class GeometryObject(FlatCAMObj, Geometry):
 
         return factor
 
+    def on_add_area_click(self):
+        shape_button = self.ui.area_shape_radio
+        overz_button = self.ui.over_z_entry
+        strategy_radio = self.ui.strategy_radio
+        cnc_button = self.ui.generate_cnc_button
+        solid_geo = self.solid_geometry
+        obj_type = self.kind
+
+        self.app.exc_areas.on_add_area_click(
+            shape_button=shape_button, overz_button=overz_button, cnc_button=cnc_button, strategy_radio=strategy_radio,
+            solid_geo=solid_geo, obj_type=obj_type)
+
+    def on_clear_area_click(self):
+        if not self.app.exc_areas.exclusion_areas_storage:
+            self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. There are no exclusion areas to delete."))
+            return
+
+        self.app.exc_areas.on_clear_area_click()
+        self.app.exc_areas.e_shape_modified.emit()
+
+    def on_delete_sel_areas(self):
+        sel_model = self.ui.exclusion_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
+        # so the duplicate rows will not be added
+        sel_rows = set()
+        for idx in sel_indexes:
+            sel_rows.add(idx.row())
+
+        if not sel_rows:
+            self.app.inform.emit("[WARNING_NOTCL] %s" % _("Delete failed. Nothing is selected."))
+            return
+
+        self.app.exc_areas.delete_sel_shapes(idxs=list(sel_rows))
+        self.app.exc_areas.e_shape_modified.emit()
+
+    def draw_sel_shape(self):
+        sel_model = self.ui.exclusion_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())
+
+        self.delete_sel_shape()
+
+        if self.app.is_legacy is False:
+            face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.2 * 255)))[2:]
+            outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(0.8 * 255)))[2:]
+        else:
+            face = self.app.defaults['global_sel_fill'][:-2] + str(hex(int(0.4 * 255)))[2:]
+            outline = self.app.defaults['global_sel_line'][:-2] + str(hex(int(1.0 * 255)))[2:]
+
+        for row in sel_rows:
+            sel_rect = self.app.exc_areas.exclusion_areas_storage[row]['shape']
+            self.app.move_tool.sel_shapes.add(sel_rect, color=outline, face_color=face, update=True, layer=0,
+                                              tolerance=None)
+        if self.app.is_legacy is True:
+            self.app.move_tool.sel_shapes.redraw()
+
+    def clear_selection(self):
+        self.app.delete_selection_shape()
+        # self.ui.exclusion_table.clearSelection()
+
+    def delete_sel_shape(self):
+        self.app.delete_selection_shape()
+
+    def update_exclusion_table(self):
+        self.exclusion_area_cb_is_checked = True if self.ui.exclusion_cb.isChecked() else False
+
+        self.build_ui()
+        self.ui.exclusion_cb.set_value(self.exclusion_area_cb_is_checked)
+
+    def on_strategy(self, val):
+        if val == 'around':
+            self.ui.over_z_label.setDisabled(True)
+            self.ui.over_z_entry.setDisabled(True)
+        else:
+            self.ui.over_z_label.setDisabled(False)
+            self.ui.over_z_entry.setDisabled(False)
+
+    def exclusion_table_toggle_all(self):
+        """
+        will toggle the selection of all rows in Exclusion Areas table
+
+        :return:
+        """
+        sel_model = self.ui.exclusion_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 sel_rows:
+            self.ui.exclusion_table.clearSelection()
+            self.delete_sel_shape()
+        else:
+            self.ui.exclusion_table.selectAll()
+            self.draw_sel_shape()
+
     def plot_element(self, element, color=None, visible=None):
 
         if color is None:
@@ -2498,6 +2669,27 @@ class GeometryObject(FlatCAMObj, Geometry):
         if not FlatCAMObj.plot(self):
             return
 
+        if self.app.is_legacy is False:
+            def random_color():
+                r_color = np.random.rand(4)
+                r_color[3] = 1
+                return r_color
+        else:
+            def random_color():
+                while True:
+                    r_color = np.random.rand(4)
+                    r_color[3] = 1
+
+                    new_color = '#'
+                    for idx in range(len(r_color)):
+                        new_color += '%x' % int(r_color[idx] * 255)
+                    # do it until a valid color is generated
+                    # a valid color has the # symbol, another 6 chars for the color and the last 2 chars for alpha
+                    # for a total of 9 chars
+                    if len(new_color) == 9:
+                        break
+                return new_color
+
         try:
             # plot solid geometries found as members of self.tools attribute dict
             # for MultiGeo
@@ -2505,7 +2697,8 @@ class GeometryObject(FlatCAMObj, Geometry):
                 for tooluid_key in self.tools:
                     solid_geometry = self.tools[tooluid_key]['solid_geometry']
                     self.plot_element(solid_geometry, visible=visible,
-                                      color=self.app.defaults["geometry_plot_line"])
+                                      color=random_color() if self.options['multicolored']
+                                      else self.app.defaults["geometry_plot_line"])
             else:
                 # plot solid geometry that may be an direct attribute of the geometry object
                 # for SingleGeo
@@ -2573,218 +2766,11 @@ class GeometryObject(FlatCAMObj, Geometry):
             self.ui.plot_cb.setChecked(True)
         self.ui_connect()
 
-    def on_add_area_click(self):
-        self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
-        self.app.call_source = 'geometry'
-
-        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):
-        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 = self.ui.area_shape_radio.get_value()
-
-        # do clear area only for left mouse clicks
-        if event.button == 1:
-            if shape_type == "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])
-                    self.exclusion_areas_list.append(new_rectangle)
-
-                    # add a temporary shape on canvas
-                    FlatCAMTool.FlatCAMTool.draw_tool_selection_shape(self, old_coords=(x0, y0), coords=(x1, y1))
-
-                    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.ui.area_shape_radio.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:
-                        FlatCAMTool.FlatCAMTool.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:
-                            self.exclusion_areas_list.append(pol)
-                            FlatCAMTool.FlatCAMTool.draw_selection_shape_polygon(self, points=self.points)
-                            self.app.inform.emit(
-                                _("Zone added. Click to start adding next zone or right click to finish."))
-
-                    self.points = []
-                    self.poly_drawn = False
-                    return
-
-            FlatCAMTool.FlatCAMTool.delete_tool_selection_shape(self)
-
-            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_list) == 0:
-                return
-
-    def area_disconnect(self):
-        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_list = []
-
-        FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
-        FlatCAMTool.FlatCAMTool.delete_tool_selection_shape(self)
-
-        self.app.call_source = "app"
-        self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
-
-    # called on mouse move
-    def on_mouse_move(self, event):
-        shape_type = self.ui.area_shape_radio.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
-        self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp;   "
-                                           "<b>Y</b>: %.4f" % (curr_pos[0], curr_pos[1]))
-        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.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp;  <b>Dy</b>: "
-                                               "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
-
-        # 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]),
-                                                     coords=(curr_pos[0], curr_pos[1]))
-        else:
-            FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
-            FlatCAMTool.FlatCAMTool.draw_moving_selection_shape_poly(
-                self, points=self.points, data=(curr_pos[0], curr_pos[1]))
-
-    def on_clear_area_click(self):
-        self.exclusion_areas_list = []
-        FlatCAMTool.FlatCAMTool.delete_moving_selection_shape(self)
-        self.app.delete_selection_shape()
+    def on_multicolored_cb_click(self, *args):
+        if self.muted_ui:
+            return
+        self.read_form_item('multicolored')
+        self.plot()
 
     @staticmethod
     def merge(geo_list, geo_final, multigeo=None):

+ 27 - 686
flatcamObjects/FlatCAMGerber.py → AppObjects/FlatCAMGerber.py

@@ -14,15 +14,15 @@
 from shapely.geometry import Point, Polygon, MultiPolygon, MultiLineString, LineString, LinearRing
 from shapely.ops import cascaded_union
 
-from flatcamParsers.ParseGerber import Gerber
-from flatcamObjects.FlatCAMObj import *
+from AppParsers.ParseGerber import Gerber
+from AppObjects.FlatCAMObj import *
 
 import math
 import numpy as np
 from copy import deepcopy
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -115,23 +115,12 @@ class GerberObject(FlatCAMObj, Gerber):
             "plot": True,
             "multicolored": False,
             "solid": False,
-            "tool_type": 'circular',
-            "vtipdia": 0.1,
-            "vtipangle": 30,
-            "vcutz": -0.05,
-            "isotooldia": 0.016,
-            "isopasses": 1,
-            "isooverlap": 15,
-            "milling_type": "cl",
-            "combine_passes": True,
             "noncoppermargin": 0.0,
             "noncopperrounded": False,
             "bboxmargin": 0.0,
             "bboxrounded": False,
             "aperture_display": False,
             "follow": False,
-            "iso_scope": 'all',
-            "iso_type": 'full'
         })
 
         # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors)
@@ -197,88 +186,44 @@ class GerberObject(FlatCAMObj, Gerber):
             "plot": self.ui.plot_cb,
             "multicolored": self.ui.multicolored_cb,
             "solid": self.ui.solid_cb,
-            "tool_type": self.ui.tool_type_radio,
-            "vtipdia": self.ui.tipdia_spinner,
-            "vtipangle": self.ui.tipangle_spinner,
-            "vcutz": self.ui.cutz_spinner,
-            "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,
-            "iso_scope": self.ui.iso_scope_radio,
-            "iso_type": self.ui.iso_type_radio
+            "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_iso_button.clicked.connect(self.on_iso_button_click)
+
+        # Tools
+        self.ui.iso_button.clicked.connect(self.app.isolation_tool.run)
         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)
 
-        # set the model for the Area Exception comboboxes
-        self.ui.obj_combo.setModel(self.app.collection)
-        self.ui.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
-        self.ui.obj_combo.is_last = True
-        self.ui.obj_combo.obj_type = {
-            _("Gerber"): "Gerber", _("Geometry"): "Geometry"
-        }[self.ui.type_obj_combo.get_value()]
-        self.on_type_obj_index_changed()
-
-        self.ui.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
-
-        self.ui.tool_type_radio.activated_custom.connect(self.on_tool_type_change)
-        # establish visibility for the GUI elements found in the slot function
-        self.ui.tool_type_radio.activated_custom.emit(self.options['tool_type'])
-
         # Show/Hide Advanced Options
         if self.app.defaults["global_app_level"] == 'b':
             self.ui.level.setText('<span style="color:green;"><b>%s</b></span>' % _('Basic'))
-            self.options['tool_type'] = 'circular'
-
-            self.ui.tool_type_label.hide()
-            self.ui.tool_type_radio.hide()
-
-            # override the Preferences Value; in Basic mode the Tool Type is always Circular ('C1')
-            self.ui.tool_type_radio.set_value('circular')
-
-            self.ui.tipdialabel.hide()
-            self.ui.tipdia_spinner.hide()
-            self.ui.tipanglelabel.hide()
-            self.ui.tipangle_spinner.hide()
-            self.ui.cutzlabel.hide()
-            self.ui.cutz_spinner.hide()
 
             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.iso_type_label.hide()
-            self.ui.iso_type_radio.hide()
 
             self.ui.follow_cb.hide()
-            self.ui.except_cb.setChecked(False)
-            self.ui.except_cb.hide()
+
         else:
             self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
-            self.ui.tipdia_spinner.valueChanged.connect(self.on_calculate_tooldia)
-            self.ui.tipangle_spinner.valueChanged.connect(self.on_calculate_tooldia)
-            self.ui.cutz_spinner.valueChanged.connect(self.on_calculate_tooldia)
 
         if self.app.defaults["gerber_buffering"] == 'no':
             self.ui.create_buffer_button.show()
@@ -296,58 +241,6 @@ class GerberObject(FlatCAMObj, Gerber):
         self.build_ui()
         self.units_found = self.app.defaults['units']
 
-    def on_calculate_tooldia(self):
-        try:
-            tdia = float(self.ui.tipdia_spinner.get_value())
-        except Exception:
-            return
-        try:
-            dang = float(self.ui.tipangle_spinner.get_value())
-        except Exception:
-            return
-        try:
-            cutz = float(self.ui.cutz_spinner.get_value())
-        except Exception:
-            return
-
-        cutz *= -1
-        if cutz < 0:
-            cutz *= -1
-
-        half_tip_angle = dang / 2
-
-        tool_diameter = tdia + (2 * cutz * math.tan(math.radians(half_tip_angle)))
-        self.ui.iso_tool_dia_entry.set_value(tool_diameter)
-
-    def on_type_obj_index_changed(self):
-        val = self.ui.type_obj_combo.get_value()
-        obj_type = {"Gerber": 0, "Geometry": 2}[val]
-        self.ui.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
-        self.ui.obj_combo.setCurrentIndex(0)
-        self.ui.obj_combo.obj_type = {_("Gerber"): "Gerber", _("Geometry"): "Geometry"}[val]
-
-    def on_tool_type_change(self, state):
-        if state == 'circular':
-            self.ui.tipdialabel.hide()
-            self.ui.tipdia_spinner.hide()
-            self.ui.tipanglelabel.hide()
-            self.ui.tipangle_spinner.hide()
-            self.ui.cutzlabel.hide()
-            self.ui.cutz_spinner.hide()
-            self.ui.iso_tool_dia_entry.setDisabled(False)
-            # update the value in the self.iso_tool_dia_entry once this is selected
-            self.ui.iso_tool_dia_entry.set_value(self.options['isotooldia'])
-        else:
-            self.ui.tipdialabel.show()
-            self.ui.tipdia_spinner.show()
-            self.ui.tipanglelabel.show()
-            self.ui.tipangle_spinner.show()
-            self.ui.cutzlabel.show()
-            self.ui.cutz_spinner.show()
-            self.ui.iso_tool_dia_entry.setDisabled(True)
-            # update the value in the self.iso_tool_dia_entry once this is selected
-            self.on_calculate_tooldia()
-
     def build_ui(self):
         FlatCAMObj.build_ui(self)
 
@@ -530,7 +423,7 @@ class GerberObject(FlatCAMObj, Gerber):
                 return "fail"
             geo_obj.solid_geometry = non_copper
 
-        self.app.new_object("geometry", name, geo_init)
+        self.app.app_obj.new_object("geometry", name, geo_init)
 
     def on_generatebb_button_click(self, *args):
         self.app.defaults.report_usage("gerber_on_generatebb_button")
@@ -556,523 +449,7 @@ class GerberObject(FlatCAMObj, Gerber):
                 return "fail"
             geo_obj.solid_geometry = bounding_box
 
-        self.app.new_object("geometry", name, geo_init)
-
-    def on_iso_button_click(self, *args):
-
-        obj = self.app.collection.get_active()
-
-        self.iso_type = 2
-        if self.ui.iso_type_radio.get_value() == 'ext':
-            self.iso_type = 0
-        if self.ui.iso_type_radio.get_value() == 'int':
-            self.iso_type = 1
-
-        def worker_task(iso_obj, app_obj):
-            with self.app.proc_container.new(_("Isolating...")):
-                if self.ui.follow_cb.get_value() is True:
-                    iso_obj.follow_geo()
-                    # in the end toggle the visibility of the origin object so we can see the generated Geometry
-                    iso_obj.ui.plot_cb.toggle()
-                else:
-                    app_obj.defaults.report_usage("gerber_on_iso_button")
-                    self.read_form()
-
-                    iso_scope = 'all' if self.ui.iso_scope_radio.get_value() == 'all' else 'single'
-                    self.isolate_handler(iso_type=self.iso_type, iso_scope=iso_scope)
-
-        self.app.worker_task.emit({'fcn': worker_task, 'params': [obj, self.app]})
-
-    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_handler(self, iso_type, iso_scope):
-
-        if iso_scope == 'all':
-            self.isolate(iso_type=iso_type)
-        else:
-            # disengage the grid snapping since it may be hard to click on polygons with grid snapping on
-            if self.app.ui.grid_snap_btn.isChecked():
-                self.grid_status_memory = True
-                self.app.ui.grid_snap_btn.trigger()
-            else:
-                self.grid_status_memory = False
-
-            self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
-
-            if self.app.is_legacy is False:
-                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.mr)
-
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it."))
-
-    def on_mouse_click_release(self, event):
-        if self.app.is_legacy is False:
-            event_pos = event.pos
-            right_button = 2
-            self.app.event_is_dragging = self.app.event_is_dragging
-        else:
-            event_pos = (event.xdata, event.ydata)
-            right_button = 3
-            self.app.event_is_dragging = self.app.ui.popMenu.mouse_is_panning
-
-        try:
-            x = float(event_pos[0])
-            y = float(event_pos[1])
-        except TypeError:
-            return
-
-        event_pos = (x, y)
-        curr_pos = self.app.plotcanvas.translate_coords(event_pos)
-        if self.app.grid_status():
-            curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
-        else:
-            curr_pos = (curr_pos[0], curr_pos[1])
-
-        if event.button == 1:
-            clicked_poly = self.find_polygon(point=(curr_pos[0], curr_pos[1]))
-
-            if self.app.selection_type is not None:
-                self.selection_area_handler(self.app.pos, curr_pos, self.app.selection_type)
-                self.app.selection_type = None
-            elif clicked_poly:
-                if clicked_poly not in self.poly_dict.values():
-                    shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0, shape=clicked_poly,
-                                                        color=self.app.defaults['global_sel_draw_color'] + 'AF',
-                                                        face_color=self.app.defaults['global_sel_draw_color'] + 'AF',
-                                                        visible=True)
-                    self.poly_dict[shape_id] = clicked_poly
-                    self.app.inform.emit(
-                        '%s: %d. %s' % (_("Added polygon"), int(len(self.poly_dict)),
-                                        _("Click to add next polygon or right click to start isolation."))
-                    )
-                else:
-                    try:
-                        for k, v in list(self.poly_dict.items()):
-                            if v == clicked_poly:
-                                self.app.tool_shapes.remove(k)
-                                self.poly_dict.pop(k)
-                                break
-                    except TypeError:
-                        return
-                    self.app.inform.emit(
-                        '%s. %s' % (_("Removed polygon"),
-                                    _("Click to add/remove next polygon or right click to start isolation."))
-                    )
-
-                self.app.tool_shapes.redraw()
-            else:
-                self.app.inform.emit(_("No polygon detected under click position."))
-        elif event.button == right_button and self.app.event_is_dragging is False:
-            # restore the Grid snapping if it was active before
-            if self.grid_status_memory is True:
-                self.app.ui.grid_snap_btn.trigger()
-
-            if self.app.is_legacy is False:
-                self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
-            else:
-                self.app.plotcanvas.graph_event_disconnect(self.mr)
-
-            self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
-                                                                  self.app.on_mouse_click_release_over_plot)
-
-            self.app.tool_shapes.clear(update=True)
-
-            if self.poly_dict:
-                poly_list = deepcopy(list(self.poly_dict.values()))
-                self.isolate(iso_type=self.iso_type, geometry=poly_list)
-                self.poly_dict.clear()
-            else:
-                self.app.inform.emit('[ERROR_NOTCL] %s' % _("List of single polygons is empty. Aborting."))
-
-    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])])
-
-        # delete previous selection shape
-        self.app.delete_selection_shape()
-
-        added_poly_count = 0
-        try:
-            for geo in self.solid_geometry:
-                if geo not in self.poly_dict.values():
-                    if sel_type is True:
-                        if geo.within(poly_selection):
-                            shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
-                                                                shape=geo,
-                                                                color=self.app.defaults['global_sel_draw_color'] + 'AF',
-                                                                face_color=self.app.defaults[
-                                                                               'global_sel_draw_color'] + 'AF',
-                                                                visible=True)
-                            self.poly_dict[shape_id] = geo
-                            added_poly_count += 1
-                    else:
-                        if poly_selection.intersects(geo):
-                            shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
-                                                                shape=geo,
-                                                                color=self.app.defaults['global_sel_draw_color'] + 'AF',
-                                                                face_color=self.app.defaults[
-                                                                               'global_sel_draw_color'] + 'AF',
-                                                                visible=True)
-                            self.poly_dict[shape_id] = geo
-                            added_poly_count += 1
-        except TypeError:
-            if self.solid_geometry not in self.poly_dict.values():
-                if sel_type is True:
-                    if self.solid_geometry.within(poly_selection):
-                        shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
-                                                            shape=self.solid_geometry,
-                                                            color=self.app.defaults['global_sel_draw_color'] + 'AF',
-                                                            face_color=self.app.defaults[
-                                                                           'global_sel_draw_color'] + 'AF',
-                                                            visible=True)
-                        self.poly_dict[shape_id] = self.solid_geometry
-                        added_poly_count += 1
-                else:
-                    if poly_selection.intersects(self.solid_geometry):
-                        shape_id = self.app.tool_shapes.add(tolerance=self.drawing_tolerance, layer=0,
-                                                            shape=self.solid_geometry,
-                                                            color=self.app.defaults['global_sel_draw_color'] + 'AF',
-                                                            face_color=self.app.defaults[
-                                                                           'global_sel_draw_color'] + 'AF',
-                                                            visible=True)
-                        self.poly_dict[shape_id] = self.solid_geometry
-                        added_poly_count += 1
-
-        if added_poly_count > 0:
-            self.app.tool_shapes.redraw()
-            self.app.inform.emit(
-                '%s: %d. %s' % (_("Added polygon"),
-                                int(added_poly_count),
-                                _("Click to add next polygon or right click to start isolation."))
-            )
-        else:
-            self.app.inform.emit(_("No polygon in selection."))
-
-    def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None,
-                milling_type=None, follow=None, plot=True):
-        """
-        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 geometry: specific geometry to isolate
-        :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
-        :param combine: Boolean: if to combine passes in one resulting object in case of multiple passes
-        :param milling_type: type of milling: conventional or climbing
-        :param follow: Boolean: if to generate a 'follow' geometry
-        :param plot: Boolean: if to plot the resulting geometry object
-        :return: None
-        """
-
-        if geometry is None:
-            work_geo = self.follow_geometry if follow is True else self.solid_geometry
-        else:
-            work_geo = geometry
-
-        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"])
-
-        overlap /= 100.0
-
-        combine = self.options["combine_passes"] if combine is None else bool(combine)
-
-        if milling_type is None:
-            milling_type = self.options["milling_type"]
-
-        if iso_type is None:
-            iso_t = 2
-        else:
-            iso_t = iso_type
-
-        base_name = self.options["name"]
-
-        if combine:
-            if outname is None:
-                if self.iso_type == 0:
-                    iso_name = base_name + "_ext_iso"
-                elif self.iso_type == 1:
-                    iso_name = base_name + "_int_iso"
-                else:
-                    iso_name = base_name + "_iso"
-            else:
-                iso_name = outname
-
-            def iso_init(geo_obj, app_obj):
-                # Propagate options
-                geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-                geo_obj.tool_type = self.ui.tool_type_radio.get_value().upper()
-
-                geo_obj.solid_geometry = []
-
-                # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
-                if self.ui.tool_type_radio.get_value() == 'v':
-                    new_cutz = self.ui.cutz_spinner.get_value()
-                    new_vtipdia = self.ui.tipdia_spinner.get_value()
-                    new_vtipangle = self.ui.tipangle_spinner.get_value()
-                    tool_type = 'V'
-                else:
-                    new_cutz = self.app.defaults['geometry_cutz']
-                    new_vtipdia = self.app.defaults['geometry_vtipdia']
-                    new_vtipangle = self.app.defaults['geometry_vtipangle']
-                    tool_type = 'C1'
-
-                # store here the default data for Geometry Data
-                default_data = {}
-                default_data.update({
-                    "name": iso_name,
-                    "plot": self.app.defaults['geometry_plot'],
-                    "cutz": new_cutz,
-                    "vtipdia": new_vtipdia,
-                    "vtipangle": new_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'],
-                    "extracut_length": self.app.defaults['geometry_extracut_length'],
-                    "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 = {}
-                geo_obj.tools['1'] = {}
-                geo_obj.tools.update({
-                    '1': {
-                        'tooldia': float(self.options["isotooldia"]),
-                        'offset': 'Path',
-                        'offset_value': 0.0,
-                        'type': _('Rough'),
-                        'tool_type': tool_type,
-                        'data': default_data,
-                        'solid_geometry': geo_obj.solid_geometry
-                    }
-                })
-
-                for nr_pass in range(passes):
-                    iso_offset = dia * ((2 * nr_pass + 1) / 2.0) - (nr_pass * overlap * dia)
-
-                    # if milling type is climb then the move is counter-clockwise around features
-                    mill_dir = 1 if milling_type == 'cl' else 0
-                    geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
-                                                  follow=follow, nr_passes=nr_pass)
-
-                    if geom == 'fail':
-                        app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
-                        return 'fail'
-                    geo_obj.solid_geometry.append(geom)
-
-                    # update the geometry in the tools
-                    geo_obj.tools['1']['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) and \
-                        not isinstance(geo_obj.solid_geometry, MultiPolygon):
-                    geo_obj.solid_geometry = [geo_obj.solid_geometry]
-
-                for g in geo_obj.solid_geometry:
-                    if g:
-                        break
-                    else:
-                        empty_cnt += 1
-
-                if empty_cnt == len(geo_obj.solid_geometry):
-                    raise ValidationError("Empty Geometry", None)
-                else:
-                    app_obj.inform.emit('[success] %s" %s' % (_("Isolation geometry created"), geo_obj.options["name"]))
-
-                # even if combine is checked, one pass is still single-geo
-                geo_obj.multigeo = True if passes > 1 else False
-
-                # ############################################################
-                # ########## AREA SUBTRACTION ################################
-                # ############################################################
-                if self.ui.except_cb.get_value():
-                    self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
-                    geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
-
-            # TODO: Do something if this is None. Offer changing name?
-            self.app.new_object("geometry", iso_name, iso_init, plot=plot)
-        else:
-            for i in range(passes):
-
-                offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
-                if passes > 1:
-                    if outname is None:
-                        if self.iso_type == 0:
-                            iso_name = base_name + "_ext_iso" + str(i + 1)
-                        elif self.iso_type == 1:
-                            iso_name = base_name + "_int_iso" + str(i + 1)
-                        else:
-                            iso_name = base_name + "_iso" + str(i + 1)
-                    else:
-                        iso_name = outname
-                else:
-                    if outname is None:
-                        if self.iso_type == 0:
-                            iso_name = base_name + "_ext_iso"
-                        elif self.iso_type == 1:
-                            iso_name = base_name + "_int_iso"
-                        else:
-                            iso_name = base_name + "_iso"
-                    else:
-                        iso_name = outname
-
-                def iso_init(geo_obj, app_obj):
-                    # Propagate options
-                    geo_obj.options["cnctooldia"] = str(self.options["isotooldia"])
-                    if self.ui.tool_type_radio.get_value() == 'v':
-                        geo_obj.tool_type = 'V'
-                    else:
-                        geo_obj.tool_type = 'C1'
-
-                    # if milling type is climb then the move is counter-clockwise around features
-                    mill_dir = 1 if milling_type == 'cl' else 0
-                    geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
-                                                  follow=follow,
-                                                  nr_passes=i)
-
-                    if geom == 'fail':
-                        app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
-                        return 'fail'
-
-                    geo_obj.solid_geometry = geom
-
-                    # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
-                    # even if the resulting geometry is not multigeo we add the tools dict which will hold the data
-                    # required to be transfered to the Geometry object
-                    if self.ui.tool_type_radio.get_value() == 'v':
-                        new_cutz = self.ui.cutz_spinner.get_value()
-                        new_vtipdia = self.ui.tipdia_spinner.get_value()
-                        new_vtipangle = self.ui.tipangle_spinner.get_value()
-                        tool_type = 'V'
-                    else:
-                        new_cutz = self.app.defaults['geometry_cutz']
-                        new_vtipdia = self.app.defaults['geometry_vtipdia']
-                        new_vtipangle = self.app.defaults['geometry_vtipangle']
-                        tool_type = 'C1'
-
-                    # store here the default data for Geometry Data
-                    default_data = {}
-                    default_data.update({
-                        "name": iso_name,
-                        "plot": self.app.defaults['geometry_plot'],
-                        "cutz": new_cutz,
-                        "vtipdia": new_vtipdia,
-                        "vtipangle": new_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'],
-                        "extracut_length": self.app.defaults['geometry_extracut_length'],
-                        "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 = {}
-                    geo_obj.tools['1'] = {}
-                    geo_obj.tools.update({
-                        '1': {
-                            'tooldia': float(self.options["isotooldia"]),
-                            'offset': 'Path',
-                            'offset_value': 0.0,
-                            'type': _('Rough'),
-                            'tool_type': tool_type,
-                            '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:
-                            break
-                        else:
-                            empty_cnt += 1
-
-                    if empty_cnt == len(geo_obj.solid_geometry):
-                        raise ValidationError("Empty Geometry", None)
-                    else:
-                        app_obj.inform.emit('[success] %s: %s' %
-                                            (_("Isolation geometry created"), geo_obj.options["name"]))
-                    geo_obj.multigeo = False
-
-                    # ############################################################
-                    # ########## AREA SUBTRACTION ################################
-                    # ############################################################
-                    if self.ui.except_cb.get_value():
-                        self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
-                        geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
-
-                # TODO: Do something if this is None. Offer changing name?
-                self.app.new_object("geometry", iso_name, iso_init, plot=plot)
+        self.app.app_obj.new_object("geometry", name, geo_init)
 
     def generate_envelope(self, offset, invert, geometry=None, env_iso_type=2, follow=None, nr_passes=0):
         # isolation_geometry produces an envelope that is going on the left of the geometry
@@ -1114,64 +491,28 @@ class GerberObject(FlatCAMObj, Gerber):
                 return 'fail'
         return geom
 
-    def area_subtraction(self, geo, subtractor_geo=None):
+    def follow_geo(self, outname=None):
         """
-        Subtracts the subtractor_geo (if present else self.solid_geometry) from the geo
+        Creates a geometry object "following" the gerber paths.
 
-        :param geo: target geometry from which to subtract
-        :param subtractor_geo: geometry that acts as subtractor
-        :return:
+        :return: None
         """
-        new_geometry = []
-        target_geo = geo
 
-        if subtractor_geo:
-            sub_union = cascaded_union(subtractor_geo)
+        if outname is None:
+            follow_name = self.options["name"] + "_follow"
         else:
-            name = self.ui.obj_combo.currentText()
-            subtractor_obj = self.app.collection.get_by_name(name)
-            sub_union = cascaded_union(subtractor_obj.solid_geometry)
+            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:
-            for geo_elem in target_geo:
-                if isinstance(geo_elem, Polygon):
-                    for ring in self.poly2rings(geo_elem):
-                        new_geo = ring.difference(sub_union)
-                        if new_geo and not new_geo.is_empty:
-                            new_geometry.append(new_geo)
-                elif isinstance(geo_elem, MultiPolygon):
-                    for poly in geo_elem:
-                        for ring in self.poly2rings(poly):
-                            new_geo = ring.difference(sub_union)
-                            if new_geo and not new_geo.is_empty:
-                                new_geometry.append(new_geo)
-                elif isinstance(geo_elem, LineString):
-                    new_geo = geo_elem.difference(sub_union)
-                    if new_geo:
-                        if not new_geo.is_empty:
-                            new_geometry.append(new_geo)
-                elif isinstance(geo_elem, MultiLineString):
-                    for line_elem in geo_elem:
-                        new_geo = line_elem.difference(sub_union)
-                        if new_geo and not new_geo.is_empty:
-                            new_geometry.append(new_geo)
-        except TypeError:
-            if isinstance(target_geo, Polygon):
-                for ring in self.poly2rings(target_geo):
-                    new_geo = ring.difference(sub_union)
-                    if new_geo:
-                        if not new_geo.is_empty:
-                            new_geometry.append(new_geo)
-            elif isinstance(target_geo, LineString):
-                new_geo = target_geo.difference(sub_union)
-                if new_geo and not new_geo.is_empty:
-                    new_geometry.append(new_geo)
-            elif isinstance(target_geo, MultiLineString):
-                for line_elem in target_geo:
-                    new_geo = line_elem.difference(sub_union)
-                    if new_geo and not new_geo.is_empty:
-                        new_geometry.append(new_geo)
-        return new_geometry
+            self.app.app_obj.new_object("geometry", follow_name, follow_init)
+        except Exception as e:
+            return "Operation failed: %s" % str(e)
 
     def on_plot_cb_click(self, *args):
         if self.muted_ui:

+ 27 - 29
flatcamObjects/FlatCAMObj.py → AppObjects/FlatCAMObj.py

@@ -12,15 +12,15 @@
 
 import inspect  # TODO: For debugging only.
 
-from flatcamGUI.ObjectUI import *
+from AppGUI.ObjectUI import *
 
-from FlatCAMCommon import LoudDict
-from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
+from Common import LoudDict
+from AppGUI.PlotCanvasLegacy import ShapeCollectionLegacy
 
 import sys
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -43,7 +43,7 @@ class ValidationError(Exception):
 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
+    in the AppGUI, can be plotted, and their options can be modified
     by the user in their respective forms.
     """
 
@@ -186,11 +186,9 @@ class FlatCAMObj(QtCore.QObject):
 
     def build_ui(self):
         """
-        Sets up the UI/form for this object. Show the UI
-        in the App.
+        Sets up the UI/form for this object. Show the UI in the App.
 
         :return: None
-        :rtype: None
         """
 
         self.muted_ui = True
@@ -247,14 +245,14 @@ class FlatCAMObj(QtCore.QObject):
             self.app.proc_container.update_view_text('')
             with self.app.proc_container.new('%s...' % _("Plotting")):
                 self.plot()
-            self.app.object_changed.emit(self)
+            self.app.app_obj.object_changed.emit(self)
 
         self.app.worker_task.emit({'fcn': worker_task, 'params': []})
 
     def on_scale_button_click(self):
         self.read_form()
         try:
-            factor = float(eval(self.ui.scale_entry.get_value()))
+            factor = float(self.ui.scale_entry.get_value())
         except Exception as e:
             self.app.inform.emit('[ERROR_NOTCL] %s' % _("Scaling could not be executed."))
             log.debug("FlatCAMObj.on_scale_button_click() -- %s" % str(e))
@@ -278,7 +276,7 @@ class FlatCAMObj(QtCore.QObject):
             self.app.proc_container.update_view_text('')
             with self.app.proc_container.new('%s...' % _("Plotting")):
                 self.plot()
-            self.app.object_changed.emit(self)
+            self.app.app_obj.object_changed.emit(self)
 
         self.app.worker_task.emit({'fcn': worker_task, 'params': []})
 
@@ -294,7 +292,7 @@ class FlatCAMObj(QtCore.QObject):
             self.app.proc_container.update_view_text('')
             with self.app.proc_container.new('%s...' % _("Plotting")):
                 self.plot()
-            self.app.object_changed.emit(self)
+            self.app.app_obj.object_changed.emit(self)
 
         self.app.worker_task.emit({'fcn': worker_task, 'params': []})
 
@@ -308,8 +306,8 @@ class FlatCAMObj(QtCore.QObject):
         for option in self.options:
             try:
                 self.set_form_item(option)
-            except Exception:
-                self.app.log.warning("Unexpected error:", sys.exc_info())
+            except Exception as err:
+                self.app.log.warning("Unexpected error: %s" % str(sys.exc_info()), str(err))
 
     def read_form(self):
         """
@@ -323,7 +321,7 @@ class FlatCAMObj(QtCore.QObject):
             try:
                 self.read_form_item(option)
             except Exception:
-                self.app.log.warning("Unexpected error:", sys.exc_info())
+                self.app.log.warning("Unexpected error: %s" % str(sys.exc_info()))
 
     def set_form_item(self, option):
         """
@@ -374,7 +372,7 @@ class FlatCAMObj(QtCore.QObject):
         def plot_task():
             with self.app.proc_container.new('%s...' % _("Plotting")):
                 self.plot()
-            self.app.object_changed.emit(self)
+            self.app.app_obj.object_changed.emit(self)
 
         self.app.worker_task.emit({'fcn': plot_task, 'params': []})
 
@@ -461,20 +459,20 @@ class FlatCAMObj(QtCore.QObject):
     def visible(self, value, threaded=True):
         log.debug("FlatCAMObj.visible()")
 
-        def worker_task(app_obj):
-            self.shapes.visible = value
-
-            if self.app.is_legacy is False:
-                # Not all object types has annotations
-                try:
-                    self.annotation.visible = value
-                except Exception:
-                    pass
-
-        if threaded is False:
-            worker_task(app_obj=self.app)
+        # self.shapes.visible = value   # maybe this is slower in VisPy? use enabled property?
+        if self.shapes.visible is True:
+            if value is False:
+                self.shapes.visible = False
         else:
-            self.app.worker_task.emit({'fcn': worker_task, 'params': [self]})
+            if value is True:
+                self.shapes.visible = True
+
+        if self.app.is_legacy is False:
+            # Not all object types has annotations
+            try:
+                self.annotation.visible = value
+            except Exception:
+                pass
 
     @property
     def drawing_tolerance(self):

+ 33 - 7
flatcamObjects/FlatCAMScript.py → AppObjects/FlatCAMScript.py

@@ -10,16 +10,16 @@
 # File modified by: Marius Stanciu                         #
 # ##########################################################
 
-from flatcamEditors.FlatCAMTextEditor import TextEditor
-from flatcamObjects.FlatCAMObj import *
-from flatcamGUI.ObjectUI import *
+from AppEditors.FlatCAMTextEditor import TextEditor
+from AppObjects.FlatCAMObj import *
+from AppGUI.ObjectUI import *
 
 import tkinter as tk
 import sys
 from copy import deepcopy
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -183,7 +183,13 @@ class ScriptObject(FlatCAMObj):
         if self.app.ui.shell_dock.isHidden():
             self.app.ui.shell_dock.show()
 
-        self.script_code = deepcopy(self.script_editor_tab.code_editor.toPlainText())
+        self.app.shell.open_processing()  # Disables input box.
+
+        # make sure that the pixmaps are not updated when running this as they will crash
+        # TODO find why the pixmaps load crash when run from this object (perhaps another thread?)
+        self.app.ui.fcinfo.lock_pmaps = True
+
+        self.script_code = self.script_editor_tab.code_editor.toPlainText()
 
         old_line = ''
         for tcl_command_line in self.script_code.splitlines():
@@ -202,8 +208,6 @@ class ScriptObject(FlatCAMObj):
 
                 # execute the actual Tcl command
                 try:
-                    self.app.shell.open_processing()  # Disables input box.
-
                     result = self.app.shell.tcl.eval(str(new_command))
                     if result != 'None':
                         self.app.shell.append_output(result + '\n')
@@ -220,6 +224,7 @@ class ScriptObject(FlatCAMObj):
             log.error("Exec command Exception: %s\n" % result)
             self.app.shell.append_error('ERROR: %s\n '% result)
 
+        self.app.ui.fcinfo.lock_pmaps = False
         self.app.shell.close_processing()
 
     def on_autocomplete_changed(self, state):
@@ -228,6 +233,27 @@ class ScriptObject(FlatCAMObj):
         else:
             self.script_editor_tab.code_editor.completer_enable = False
 
+    def mirror(self, axis, point):
+        pass
+
+    def offset(self, vect):
+        pass
+
+    def rotate(self, angle, point):
+        pass
+
+    def scale(self, xfactor, yfactor=None, point=None):
+        pass
+
+    def skew(self, angle_x, angle_y, point):
+        pass
+
+    def buffer(self, distance, join, factor=None):
+        pass
+
+    def bounds(self, flatten=False):
+        return None, None, None, None
+
     def to_dict(self):
         """
         Returns a representation of the object as a dictionary.

+ 216 - 16
flatcamObjects/ObjectCollection.py → AppObjects/ObjectCollection.py

@@ -16,13 +16,13 @@ from PyQt5.QtCore import Qt, QSettings
 from PyQt5.QtGui import QColor
 # from PyQt5.QtCore import QModelIndex
 
-from flatcamObjects.FlatCAMObj import FlatCAMObj
-from flatcamObjects.FlatCAMCNCJob import CNCJobObject
-from flatcamObjects.FlatCAMDocument import DocumentObject
-from flatcamObjects.FlatCAMExcellon import ExcellonObject
-from flatcamObjects.FlatCAMGeometry import GeometryObject
-from flatcamObjects.FlatCAMGerber import GerberObject
-from flatcamObjects.FlatCAMScript import ScriptObject
+from AppObjects.FlatCAMObj import FlatCAMObj
+from AppObjects.FlatCAMCNCJob import CNCJobObject
+from AppObjects.FlatCAMDocument import DocumentObject
+from AppObjects.FlatCAMExcellon import ExcellonObject
+from AppObjects.FlatCAMGeometry import GeometryObject
+from AppObjects.FlatCAMGerber import GerberObject
+from AppObjects.FlatCAMScript import ScriptObject
 
 import inspect  # TODO: Remove
 
@@ -32,7 +32,7 @@ from copy import deepcopy
 from numpy import Inf
 
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')
@@ -50,12 +50,13 @@ class KeySensitiveListView(QtWidgets.QTreeView):
     def __init__(self, app, parent=None):
         super(KeySensitiveListView, self).__init__(parent)
         self.setHeaderHidden(True)
-        self.setEditTriggers(QtWidgets.QTreeView.SelectedClicked)
+        # self.setEditTriggers(QtWidgets.QTreeView.SelectedClicked)     # allow Edit on Tree
+        self.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers)
 
         # self.setRootIsDecorated(False)
         # self.setExpandsOnDoubleClick(False)
 
-        # Enable dragging and dropping onto the GUI
+        # Enable dragging and dropping onto the AppGUI
         self.setAcceptDrops(True)
         self.filename = ""
         self.app = app
@@ -334,6 +335,8 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         self.click_modifier = None
 
         self.update_list_signal.connect(self.on_update_list_signal)
+        self.view.activated.connect(self.on_row_activated)
+        self.item_selected.connect(self.on_row_selected)
 
     def promise(self, obj_name):
         log.debug("Object %s has been promised." % obj_name)
@@ -382,8 +385,9 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
                 if type(obj) != GeometryObject:
                     self.app.ui.menuprojectgeneratecnc.setVisible(False)
-                if type(obj) != GeometryObject and type(obj) != ExcellonObject and type(obj) != GerberObject:
-                    self.app.ui.menuprojectedit.setVisible(False)
+                # if type(obj) != GeometryObject and type(obj) != ExcellonObject and type(obj) != GerberObject or \
+                #         type(obj) != CNCJobObject:
+                #     self.app.ui.menuprojectedit.setVisible(False)
                 if type(obj) != GerberObject and type(obj) != ExcellonObject and type(obj) != CNCJobObject:
                     self.app.ui.menuprojectviewsource.setVisible(False)
                 if type(obj) != GerberObject and type(obj) != GeometryObject and type(obj) != ExcellonObject and \
@@ -550,7 +554,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         while name in self.get_names():
             # ## Create a new name
             # Ends with number?
-            log.debug("new_object(): Object name (%s) exists, changing." % name)
+            log.debug("app_obj.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 ''
@@ -596,8 +600,8 @@ class ObjectCollection(QtCore.QAbstractItemModel):
         """
         Gets a list of the names of all objects in the collection.
 
-        :return: List of names.
-        :rtype: list
+        :return:            List of names.
+        :rtype:             list
         """
 
         # log.debug(str(inspect.stack()[1][3]) + " --> OC.get_names()")
@@ -985,7 +989,7 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
     def get_list(self):
         """
-        Will return a list of all objects currently opened.
+        Will return a list of all objects currently opened. Except FlatCAMScript and FlatCAMDocuments
 
         :return:
         """
@@ -998,3 +1002,199 @@ class ObjectCollection(QtCore.QAbstractItemModel):
 
     def update_view(self):
         self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
+
+    def on_row_activated(self, index):
+        if index.isValid():
+            if index.internalPointer().parent_item != self.root_item:
+                self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+        self.on_item_activated(index)
+
+    def on_row_selected(self, obj_name):
+        """
+        This is a special string; when received it will make all Menu -> Objects entries unchecked
+        It mean we clicked outside of the items and deselected all
+
+        :param obj_name:
+        :return:
+        """
+        if obj_name == 'none':
+            for act in self.app.ui.menuobjects.actions():
+                act.setChecked(False)
+            return
+
+        # get the name of the selected objects and add them to a list
+        name_list = []
+        for obj in self.get_selected():
+            name_list.append(obj.options['name'])
+
+        # set all actions as unchecked but the ones selected make them checked
+        for act in self.app.ui.menuobjects.actions():
+            act.setChecked(False)
+            if act.text() in name_list:
+                act.setChecked(True)
+
+    def on_collection_updated(self, obj, state, old_name):
+        """
+        Create a menu from the object loaded in the collection.
+
+        :param obj:         object that was changed (added, deleted, renamed)
+        :param state:       what was done with the object. Can be: added, deleted, delete_all, renamed
+        :param old_name:    the old name of the object before the action that triggered this slot happened
+        :return:            None
+        """
+        icon_files = {
+            "gerber": self.app.resource_location + "/flatcam_icon16.png",
+            "excellon": self.app.resource_location + "/drill16.png",
+            "cncjob": self.app.resource_location + "/cnc16.png",
+            "geometry": self.app.resource_location + "/geometry16.png",
+            "script": self.app.resource_location + "/script_new16.png",
+            "document": self.app.resource_location + "/notes16_1.png"
+        }
+
+        if state == 'append':
+            for act in self.app.ui.menuobjects.actions():
+                try:
+                    act.triggered.disconnect()
+                except TypeError:
+                    pass
+            self.app.ui.menuobjects.clear()
+
+            gerber_list = []
+            exc_list = []
+            cncjob_list = []
+            geo_list = []
+            script_list = []
+            doc_list = []
+
+            for name in self.get_names():
+                obj_named = self.get_by_name(name)
+                if obj_named.kind == 'gerber':
+                    gerber_list.append(name)
+                elif obj_named.kind == 'excellon':
+                    exc_list.append(name)
+                elif obj_named.kind == 'cncjob':
+                    cncjob_list.append(name)
+                elif obj_named.kind == 'geometry':
+                    geo_list.append(name)
+                elif obj_named.kind == 'script':
+                    script_list.append(name)
+                elif obj_named.kind == 'document':
+                    doc_list.append(name)
+
+            def add_act(o_name):
+                obj_for_icon = self.get_by_name(o_name)
+                add_action = QtWidgets.QAction(parent=self.app.ui.menuobjects)
+                add_action.setCheckable(True)
+                add_action.setText(o_name)
+                add_action.setIcon(QtGui.QIcon(icon_files[obj_for_icon.kind]))
+                add_action.triggered.connect(
+                    lambda: self.set_active(o_name) if add_action.isChecked() is True else
+                    self.set_inactive(o_name))
+                self.app.ui.menuobjects.addAction(add_action)
+
+            for name in gerber_list:
+                add_act(name)
+            self.app.ui.menuobjects.addSeparator()
+
+            for name in exc_list:
+                add_act(name)
+            self.app.ui.menuobjects.addSeparator()
+
+            for name in cncjob_list:
+                add_act(name)
+            self.app.ui.menuobjects.addSeparator()
+
+            for name in geo_list:
+                add_act(name)
+            self.app.ui.menuobjects.addSeparator()
+
+            for name in script_list:
+                add_act(name)
+            self.app.ui.menuobjects.addSeparator()
+
+            for name in doc_list:
+                add_act(name)
+
+            self.app.ui.menuobjects.addSeparator()
+            self.app.ui.menuobjects_selall = self.app.ui.menuobjects.addAction(
+                QtGui.QIcon(self.app.resource_location + '/select_all.png'),
+                _('Select All')
+            )
+            self.app.ui.menuobjects_unselall = self.app.ui.menuobjects.addAction(
+                QtGui.QIcon(self.app.resource_location + '/deselect_all32.png'),
+                _('Deselect All')
+            )
+            self.app.ui.menuobjects_selall.triggered.connect(lambda: self.on_objects_selection(True))
+            self.app.ui.menuobjects_unselall.triggered.connect(lambda: self.on_objects_selection(False))
+
+        elif state == 'delete':
+            for act in self.app.ui.menuobjects.actions():
+                if act.text() == obj.options['name']:
+                    try:
+                        act.triggered.disconnect()
+                    except TypeError:
+                        pass
+                    self.app.ui.menuobjects.removeAction(act)
+                    break
+        elif state == 'rename':
+            for act in self.app.ui.menuobjects.actions():
+                if act.text() == old_name:
+                    add_action = QtWidgets.QAction(parent=self.app.ui.menuobjects)
+                    add_action.setText(obj.options['name'])
+                    add_action.setIcon(QtGui.QIcon(icon_files[obj.kind]))
+                    add_action.triggered.connect(
+                        lambda: self.set_active(obj.options['name']) if add_action.isChecked() is True else
+                        self.set_inactive(obj.options['name']))
+
+                    self.app.ui.menuobjects.insertAction(act, add_action)
+
+                    try:
+                        act.triggered.disconnect()
+                    except TypeError:
+                        pass
+                    self.app.ui.menuobjects.removeAction(act)
+                    break
+        elif state == 'delete_all':
+            for act in self.app.ui.menuobjects.actions():
+                try:
+                    act.triggered.disconnect()
+                except TypeError:
+                    pass
+            self.app.ui.menuobjects.clear()
+
+            self.app.ui.menuobjects.addSeparator()
+            self.app.ui.menuobjects_selall = self.app.ui.menuobjects.addAction(
+                QtGui.QIcon(self.app.resource_location + '/select_all.png'),
+                _('Select All')
+            )
+            self.app.ui.menuobjects_unselall = self.app.ui.menuobjects.addAction(
+                QtGui.QIcon(self.app.resource_location + '/deselect_all32.png'),
+                _('Deselect All')
+            )
+            self.app.ui.menuobjects_selall.triggered.connect(lambda: self.on_objects_selection(True))
+            self.app.ui.menuobjects_unselall.triggered.connect(lambda: self.on_objects_selection(False))
+
+    def on_objects_selection(self, on_off):
+        obj_list = self.get_names()
+
+        if on_off is True:
+            self.set_all_active()
+            for act in self.app.ui.menuobjects.actions():
+                try:
+                    act.setChecked(True)
+                except Exception:
+                    pass
+            if obj_list:
+                self.app.inform[str, bool].emit('[selected] %s' % _("All objects are selected."), False)
+        else:
+            self.set_all_inactive()
+            for act in self.app.ui.menuobjects.actions():
+                try:
+                    act.setChecked(False)
+                except Exception:
+                    pass
+
+            if obj_list:
+                self.app.inform[str, bool].emit('%s' % _("Objects selection is cleared."), False)
+            else:
+                self.app.inform[str, bool].emit('', False)

+ 0 - 0
flatcamObjects/__init__.py → AppObjects/__init__.py


+ 2 - 2
flatcamParsers/ParseDXF.py → AppParsers/ParseDXF.py

@@ -12,8 +12,8 @@ import logging
 
 log = logging.getLogger('base2')
 
-from flatcamParsers.ParseFont import *
-from flatcamParsers.ParseDXF_Spline import *
+from AppParsers.ParseFont import *
+from AppParsers.ParseDXF_Spline import *
 
 
 def distance(pt1, pt2):

+ 0 - 0
flatcamParsers/ParseDXF_Spline.py → AppParsers/ParseDXF_Spline.py


+ 15 - 16
flatcamParsers/ParseExcellon.py → AppParsers/ParseExcellon.py

@@ -6,7 +6,7 @@
 # MIT Licence                                                 #
 # ########################################################## ##
 
-from camlib import Geometry
+from camlib import Geometry, grace
 
 import shapely.affinity as affinity
 from shapely.geometry import Point, LineString
@@ -17,8 +17,7 @@ import logging
 import traceback
 from copy import deepcopy
 
-import FlatCAMTranslation as fcTranslate
-from FlatCAMCommon import GracefulException as grace
+# import AppTranslation as fcTranslate
 
 import gettext
 import builtins
@@ -967,7 +966,7 @@ class Excellon(Geometry):
         :return: None
         """
 
-        log.debug("flatcamParsers.ParseExcellon.Excellon.create_geometry()")
+        log.debug("AppParsers.ParseExcellon.Excellon.create_geometry()")
         self.solid_geometry = []
         try:
             # clear the solid_geometry in self.tools
@@ -982,7 +981,7 @@ class Excellon(Geometry):
                                          _("Excellon.create_geometry() -> a drill location was skipped "
                                            "due of not having a tool associated.\n"
                                            "Check the resulting GCode."))
-                    log.debug("flatcamParsers.ParseExcellon.Excellon.create_geometry() -> a drill location was skipped "
+                    log.debug("AppParsers.ParseExcellon.Excellon.create_geometry() -> a drill location was skipped "
                               "due of not having a tool associated")
                     continue
                 tooldia = self.tools[drill['tool']]['C']
@@ -1006,7 +1005,7 @@ class Excellon(Geometry):
                 self.tools[tool_in_slots]['solid_geometry'].append(poly)
                 self.tools[tool_in_slots]['data'] = deepcopy(self.default_data)
         except Exception as e:
-            log.debug("flatcamParsers.ParseExcellon.Excellon.create_geometry() -> "
+            log.debug("AppParsers.ParseExcellon.Excellon.create_geometry() -> "
                       "Excellon geometry creation failed due of ERROR: %s" % str(e))
             return "fail"
 
@@ -1018,10 +1017,10 @@ class Excellon(Geometry):
         :param flatten:     No used
         """
 
-        log.debug("flatcamParsers.ParseExcellon.Excellon.bounds()")
+        log.debug("AppParsers.ParseExcellon.Excellon.bounds()")
 
         if self.solid_geometry is None or not self.tools:
-            log.debug("flatcamParsers.ParseExcellon.Excellon -> solid_geometry is None")
+            log.debug("AppParsers.ParseExcellon.Excellon -> solid_geometry is None")
             return 0, 0, 0, 0
 
         def bounds_rec(obj):
@@ -1069,7 +1068,7 @@ class Excellon(Geometry):
         This function first convert to the the units found in the Excellon file but it converts tools that
         are not there yet so it has no effect other than it signal that the units are the ones in the file.
 
-        On object creation, in new_object(), true conversion is done because this is done at the end of the
+        On object creation, in app_obj.new_object(), true conversion is done because this is done at the end of the
         Excellon file parsing, the tools are inside and self.tools is really converted from the units found
         inside the file to the FlatCAM units.
 
@@ -1092,7 +1091,7 @@ class Excellon(Geometry):
         else:
             log.error("Unsupported units: %s" % str(obj_units))
             factor = 1.0
-        log.debug("flatcamParsers.ParseExcellon.Excellon.convert_units() --> Factor: %s" % str(factor))
+        log.debug("AppParsers.ParseExcellon.Excellon.convert_units() --> Factor: %s" % str(factor))
 
         self.units = obj_units
         self.scale(factor, factor)
@@ -1118,7 +1117,7 @@ class Excellon(Geometry):
         :return:            None
         :rtype:             None
         """
-        log.debug("flatcamParsers.ParseExcellon.Excellon.scale()")
+        log.debug("AppParsers.ParseExcellon.Excellon.scale()")
 
         if yfactor is None:
             yfactor = xfactor
@@ -1183,7 +1182,7 @@ class Excellon(Geometry):
         :type vect: tuple
         :return: None
         """
-        log.debug("flatcamParsers.ParseExcellon.Excellon.offset()")
+        log.debug("AppParsers.ParseExcellon.Excellon.offset()")
 
         dx, dy = vect
 
@@ -1243,7 +1242,7 @@ class Excellon(Geometry):
         :type point:        list
         :return:            None
         """
-        log.debug("flatcamParsers.ParseExcellon.Excellon.mirror()")
+        log.debug("AppParsers.ParseExcellon.Excellon.mirror()")
 
         px, py = point
         xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
@@ -1309,7 +1308,7 @@ class Excellon(Geometry):
         See shapely manual for more information:
         http://toblerity.org/shapely/manual.html#affine-transformations
         """
-        log.debug("flatcamParsers.ParseExcellon.Excellon.skew()")
+        log.debug("AppParsers.ParseExcellon.Excellon.skew()")
 
         if angle_x is None:
             angle_x = 0.0
@@ -1396,7 +1395,7 @@ class Excellon(Geometry):
         :param point:   tuple of coordinates (x, y)
         :return:        None
         """
-        log.debug("flatcamParsers.ParseExcellon.Excellon.rotate()")
+        log.debug("AppParsers.ParseExcellon.Excellon.rotate()")
 
         if angle == 0:
             return
@@ -1479,7 +1478,7 @@ class Excellon(Geometry):
         :param join:        The type of line joint used by the shapely buffer method: round, square, bevel
         :return:            None
         """
-        log.debug("flatcamParsers.ParseExcellon.Excellon.buffer()")
+        log.debug("AppParsers.ParseExcellon.Excellon.buffer()")
 
         if distance == 0:
             return

+ 1 - 1
flatcamParsers/ParseFont.py → AppParsers/ParseFont.py

@@ -22,7 +22,7 @@ from fontTools import ttLib
 
 import logging
 import gettext
-import FlatCAMTranslation as fcTranslate
+import AppTranslation as fcTranslate
 import builtins
 
 fcTranslate.apply_language('strings')

+ 6 - 7
flatcamParsers/ParseGerber.py → AppParsers/ParseGerber.py

@@ -1,5 +1,5 @@
 from PyQt5 import QtWidgets
-from camlib import Geometry, arc, arc_angle, ApertureMacro
+from camlib import Geometry, arc, arc_angle, ApertureMacro, grace
 
 import numpy as np
 import re
@@ -14,9 +14,8 @@ import shapely.affinity as affinity
 from shapely.geometry import box as shply_box, Polygon, LineString, Point, MultiPolygon
 
 from lxml import etree as ET
-from flatcamParsers.ParseSVG import svgparselength, getsvggeo
-from FlatCAMCommon import GracefulException as grace
-import FlatCAMTranslation as fcTranslate
+from AppParsers.ParseSVG import svgparselength, getsvggeo
+import AppTranslation as fcTranslate
 
 import gettext
 import builtins
@@ -1477,7 +1476,7 @@ class Gerber(Geometry):
                 sol_geo_length = 1
 
             try:
-                if buff_length == 0 and sol_geo_length in [0, 1]:
+                if buff_length == 0 and sol_geo_length in [0, 1] and self.solid_geometry.area == 0:
                     log.error("Object is not Gerber file or empty. Aborting Object creation.")
                     return 'fail'
             except TypeError as e:
@@ -1763,7 +1762,7 @@ class Gerber(Geometry):
         :return: None
         """
 
-        log.debug("flatcamParsers.ParseGerber.Gerber.import_svg()")
+        log.debug("AppParsers.ParseGerber.Gerber.import_svg()")
 
         # Parse into list of shapely objects
         svg_tree = ET.parse(filename)
@@ -2389,7 +2388,7 @@ class Gerber(Geometry):
                                         geo_p = shply_box(minx, miny, maxx, maxy)
                                         new_geo_el['solid'] = geo_p
                                     else:
-                                        log.debug("flatcamParsers.ParseGerber.Gerber.buffer() --> "
+                                        log.debug("AppParsers.ParseGerber.Gerber.buffer() --> "
                                                   "ap type not supported")
                                 else:
                                     new_geo_el['solid'] = geo_el['follow'].buffer(

+ 2 - 3
flatcamParsers/ParseHPGL2.py → AppParsers/ParseHPGL2.py

@@ -6,7 +6,7 @@
 # MIT Licence                                                #
 # ############################################################
 
-from camlib import arc, three_point_circle
+from camlib import arc, three_point_circle, grace
 
 import numpy as np
 import re
@@ -18,8 +18,7 @@ import sys
 from shapely.ops import unary_union
 from shapely.geometry import LineString, Point
 
-from FlatCAMCommon import GracefulException as grace
-import FlatCAMTranslation as fcTranslate
+# import AppTranslation as fcTranslate
 import gettext
 import builtins
 

+ 25 - 356
flatcamTools/ToolPDF.py → AppParsers/ParsePDF.py

@@ -5,50 +5,27 @@
 # MIT Licence                                              #
 # ##########################################################
 
-from PyQt5 import QtWidgets, QtCore
+from PyQt5 import QtCore
 
-from FlatCAMTool import FlatCAMTool
-from FlatCAMCommon import GracefulException as grace
+from Common import GracefulException as grace
 
-from shapely.geometry import Point, Polygon, LineString, MultiPolygon
-from shapely.ops import unary_union
+from shapely.geometry import Polygon, LineString, MultiPolygon
 
 from copy import copy, deepcopy
 import numpy as np
-
-import zlib
 import re
-import time
 import logging
-import traceback
-
-import gettext
-import FlatCAMTranslation as fcTranslate
-import builtins
-
-fcTranslate.apply_language('strings')
-if '_' not in builtins.__dict__:
-    _ = gettext.gettext
 
 log = logging.getLogger('base')
 
 
-class ToolPDF(FlatCAMTool):
-    """
-    Parse a PDF file.
-    Reference here: https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/pdf_reference_archives/PDFReference.pdf
-    Return a list of geometries
-    """
-    toolName = _("PDF Import Tool")
+class PdfParser(QtCore.QObject):
 
     def __init__(self, app):
-        FlatCAMTool.__init__(self, app)
+        super().__init__()
         self.app = app
-        self.decimals = self.app.decimals
         self.step_per_circles = self.app.defaults["gerber_circle_steps"]
 
-        self.stream_re = re.compile(b'.*?FlateDecode.*?stream(.*?)endstream', re.S)
-
         # detect stroke color change; it means a new object to be created
         self.stroke_color_re = re.compile(r'^\s*(\d+\.?\d*) (\d+\.?\d*) (\d+\.?\d*)\s*RG$')
 
@@ -110,75 +87,12 @@ class ToolPDF(FlatCAMTool):
         # (each sublist has 2 lists each having 2 elements: first is offset like:
         # offset_geo = [off_x, off_y], second element is scale list with 2 elements, like: scale_geo = [sc_x, sc_yy])
         self.gs['transform'] = []
-        self.gs['line_width'] = []   # each element is a float
-
-        self.pdf_decompressed = {}
-
-        # key = file name and extension
-        # value is a dict to store the parsed content of the PDF
-        self.pdf_parsed = {}
-
-        # QTimer for periodic check
-        self.check_thread = QtCore.QTimer()
-
-        # Every time a parser is started we add a promise; every time a parser finished we remove a promise
-        # when empty we start the layer rendering
-        self.parsing_promises = []
+        self.gs['line_width'] = []  # each element is a float
 
         # conversion factor to INCH
         self.point_to_unit_factor = 0.01388888888
 
-    def run(self, toggle=True):
-        self.app.defaults.report_usage("ToolPDF()")
-
-        self.set_tool_ui()
-        self.on_open_pdf_click()
-
-    def install(self, icon=None, separator=None, **kwargs):
-        FlatCAMTool.install(self, icon, separator, shortcut='Ctrl+Q', **kwargs)
-
-    def set_tool_ui(self):
-        pass
-
-    def on_open_pdf_click(self):
-        """
-        File menu callback for opening an PDF file.
-
-        :return: None
-        """
-
-        self.app.defaults.report_usage("ToolPDF.on_open_pdf_click()")
-        self.app.log.debug("ToolPDF.on_open_pdf_click()")
-
-        _filter_ = "Adobe PDF Files (*.pdf);;" \
-                   "All Files (*.*)"
-
-        try:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"),
-                                                                   directory=self.app.get_last_folder(),
-                                                                   filter=_filter_)
-        except TypeError:
-            filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open PDF"), filter=_filter_)
-
-        if len(filenames) == 0:
-            self.app.inform.emit('[WARNING_NOTCL] %s.' % _("Open PDF cancelled"))
-        else:
-            # start the parsing timer with a period of 1 second
-            self.periodic_check(1000)
-
-            for filename in filenames:
-                if filename != '':
-                    self.app.worker_task.emit({'fcn': self.open_pdf,
-                                               'params': [filename]})
-
-    def open_pdf(self, filename):
-        short_name = filename.split('/')[-1].split('\\')[-1]
-        self.parsing_promises.append(short_name)
-        self.pdf_parsed[short_name] = {}
-        self.pdf_parsed[short_name]['pdf'] = {}
-        self.pdf_parsed[short_name]['filename'] = filename
-
-        self.pdf_decompressed[short_name] = ''
+    def parse_pdf(self, pdf_content):
 
         # the UNITS in PDF files are points and here we set the factor to convert them to real units (either MM or INCH)
         if self.app.defaults['units'].upper() == 'MM':
@@ -188,260 +102,14 @@ class ToolPDF(FlatCAMTool):
             # 1 inch = 72 points => 1 point = 1 / 72 = 0.01388888888 inch
             self.point_to_unit_factor = 1 / 72
 
-        if self.app.abort_flag:
-            # graceful abort requested by the user
-            raise grace
-
-        with self.app.proc_container.new(_("Parsing PDF file ...")):
-            with open(filename, "rb") as f:
-                pdf = f.read()
-
-            stream_nr = 0
-            for s in re.findall(self.stream_re, pdf):
-                if self.app.abort_flag:
-                    # graceful abort requested by the user
-                    raise grace
-
-                stream_nr += 1
-                log.debug(" PDF STREAM: %d\n" % stream_nr)
-                s = s.strip(b'\r\n')
-                try:
-                    self.pdf_decompressed[short_name] += (zlib.decompress(s).decode('UTF-8') + '\r\n')
-                except Exception as e:
-                    log.debug("ToolPDF.open_pdf().obj_init() --> %s" % str(e))
-
-            self.pdf_parsed[short_name]['pdf'] = self.parse_pdf(pdf_content=self.pdf_decompressed[short_name])
-            # we used it, now we delete it
-            self.pdf_decompressed[short_name] = ''
-
-        # removal from list is done in a multithreaded way therefore not always the removal can be done
-        # try to remove until it's done
-        try:
-            while True:
-                self.parsing_promises.remove(short_name)
-                time.sleep(0.1)
-        except Exception as e:
-            log.debug("ToolPDF.open_pdf() --> %s" % str(e))
-        self.app.inform.emit('[success] %s: %s' % (_("Opened"),  str(filename)))
-
-    def layer_rendering_as_excellon(self, filename, ap_dict, layer_nr):
-        outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
-
-        # store the points here until reconstitution:
-        # keys are diameters and values are list of (x,y) coords
-        points = {}
-
-        def obj_init(exc_obj, app_obj):
-            clear_geo = [geo_el['clear'] for geo_el in ap_dict['0']['geometry']]
-
-            for geo in clear_geo:
-                xmin, ymin, xmax, ymax = geo.bounds
-                center = (((xmax - xmin) / 2) + xmin, ((ymax - ymin) / 2) + ymin)
-
-                # for drill bits, even in INCH, it's enough 3 decimals
-                correction_factor = 0.974
-                dia = (xmax - xmin) * correction_factor
-                dia = round(dia, 3)
-                if dia in points:
-                    points[dia].append(center)
-                else:
-                    points[dia] = [center]
-
-            sorted_dia = sorted(points.keys())
-
-            name_tool = 0
-            for dia in sorted_dia:
-                name_tool += 1
-
-                # create tools dictionary
-                spec = {"C": dia, 'solid_geometry': []}
-                exc_obj.tools[str(name_tool)] = spec
-
-                # create drill list of dictionaries
-                for dia_points in points:
-                    if dia == dia_points:
-                        for pt in points[dia_points]:
-                            exc_obj.drills.append({'point': Point(pt), 'tool': str(name_tool)})
-                        break
-
-            ret = exc_obj.create_geometry()
-            if ret == 'fail':
-                log.debug("Could not create geometry for Excellon object.")
-                return "fail"
-            for tool in exc_obj.tools:
-                if exc_obj.tools[tool]['solid_geometry']:
-                    return
-            app_obj.inform.emit('[ERROR_NOTCL] %s: %s' %
-                                (_("No geometry found in file"), outname))
-            return "fail"
-
-        with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
-
-            ret_val = self.app.new_object("excellon", outname, obj_init, autoselected=False)
-            if ret_val == 'fail':
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _('Open PDF file failed.'))
-                return
-            # Register recent file
-            self.app.file_opened.emit("excellon", filename)
-            # GUI feedback
-            self.app.inform.emit('[success] %s: %s' %
-                                 (_("Rendered"),  outname))
-
-    def layer_rendering_as_gerber(self, filename, ap_dict, layer_nr):
-        outname = filename.split('/')[-1].split('\\')[-1] + "_%s" % str(layer_nr)
-
-        def obj_init(grb_obj):
-
-            grb_obj.apertures = ap_dict
-
-            poly_buff = []
-            follow_buf = []
-            for ap in grb_obj.apertures:
-                for k in grb_obj.apertures[ap]:
-                    if k == 'geometry':
-                        for geo_el in ap_dict[ap][k]:
-                            if 'solid' in geo_el:
-                                poly_buff.append(geo_el['solid'])
-                            if 'follow' in geo_el:
-                                follow_buf.append(geo_el['follow'])
-            poly_buff = unary_union(poly_buff)
-
-            if '0' in grb_obj.apertures:
-                global_clear_geo = []
-                if 'geometry' in grb_obj.apertures['0']:
-                    for geo_el in ap_dict['0']['geometry']:
-                        if 'clear' in geo_el:
-                            global_clear_geo.append(geo_el['clear'])
-
-                if global_clear_geo:
-                    solid = []
-                    for apid in grb_obj.apertures:
-                        if 'geometry' in grb_obj.apertures[apid]:
-                            for elem in grb_obj.apertures[apid]['geometry']:
-                                if 'solid' in elem:
-                                    solid_geo = deepcopy(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)
-                                        if solid_geo.is_empty:
-                                            solid_geo = elem['solid']
-                                    try:
-                                        for poly in solid_geo:
-                                            solid.append(poly)
-                                    except TypeError:
-                                        solid.append(solid_geo)
-                    poly_buff = deepcopy(MultiPolygon(solid))
-
-            follow_buf = unary_union(follow_buf)
-
-            try:
-                poly_buff = poly_buff.buffer(0.0000001)
-            except ValueError:
-                pass
-            try:
-                poly_buff = poly_buff.buffer(-0.0000001)
-            except ValueError:
-                pass
-
-            grb_obj.solid_geometry = deepcopy(poly_buff)
-            grb_obj.follow_geometry = deepcopy(follow_buf)
-
-        with self.app.proc_container.new(_("Rendering PDF layer #%d ...") % int(layer_nr)):
-
-            ret = self.app.new_object('gerber', outname, obj_init, autoselected=False)
-            if ret == 'fail':
-                self.app.inform.emit('[ERROR_NOTCL] %s' %
-                                     _('Open PDF file failed.'))
-                return
-            # Register recent file
-            self.app.file_opened.emit('gerber', filename)
-            # GUI feedback
-            self.app.inform.emit('[success] %s: %s' % (_("Rendered"), outname))
-
-    def periodic_check(self, check_period):
-        """
-        This function starts an QTimer and it will periodically check if parsing was done
-
-        :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("ToolPDF --> Periodic Check started.")
-
-        try:
-            self.check_thread.stop()
-        except TypeError:
-            pass
-
-        self.check_thread.setInterval(check_period)
-        try:
-            self.check_thread.timeout.disconnect(self.periodic_check_handler)
-        except (TypeError, AttributeError):
-            pass
-
-        self.check_thread.timeout.connect(self.periodic_check_handler)
-        self.check_thread.start(QtCore.QThread.HighPriority)
-
-    def periodic_check_handler(self):
-        """
-        If the parsing worker finished then start multithreaded rendering
-        :return:
-        """
-        # log.debug("checking parsing --> %s" % str(self.parsing_promises))
-
-        try:
-            if not self.parsing_promises:
-                self.check_thread.stop()
-                # parsing finished start the layer rendering
-                if self.pdf_parsed:
-                    obj_to_delete = []
-                    for object_name in self.pdf_parsed:
-                        if self.app.abort_flag:
-                            # graceful abort requested by the user
-                            raise grace
-
-                        filename = deepcopy(self.pdf_parsed[object_name]['filename'])
-                        pdf_content = deepcopy(self.pdf_parsed[object_name]['pdf'])
-                        obj_to_delete.append(object_name)
-                        for k in pdf_content:
-                            if self.app.abort_flag:
-                                # graceful abort requested by the user
-                                raise grace
-
-                            ap_dict = pdf_content[k]
-                            if ap_dict:
-                                layer_nr = k
-                                if k == 0:
-                                    self.app.worker_task.emit({'fcn': self.layer_rendering_as_excellon,
-                                                               'params': [filename, ap_dict, layer_nr]})
-                                else:
-                                    self.app.worker_task.emit({'fcn': self.layer_rendering_as_gerber,
-                                                               'params': [filename, ap_dict, layer_nr]})
-                    # delete the object already processed so it will not be processed again for other objects
-                    # that were opened at the same time; like in drag & drop on GUI
-                    for obj_name in obj_to_delete:
-                        if obj_name in self.pdf_parsed:
-                            self.pdf_parsed.pop(obj_name)
-
-                log.debug("ToolPDF --> Periodic check finished.")
-        except Exception:
-            traceback.print_exc()
-
-    def parse_pdf(self, pdf_content):
         path = {}
-        path['lines'] = []      # it's a list of lines subpaths
-        path['bezier'] = []     # it's a list of bezier arcs subpaths
+        path['lines'] = []  # it's a list of lines subpaths
+        path['bezier'] = []  # it's a list of bezier arcs subpaths
         path['rectangle'] = []  # it's a list of rectangle subpaths
 
         subpath = {}
-        subpath['lines'] = []      # it's a list of points
-        subpath['bezier'] = []     # it's a list of sublists each like this [start, c1, c2, stop]
+        subpath['lines'] = []  # it's a list of points
+        subpath['bezier'] = []  # it's a list of sublists each like this [start, c1, c2, stop]
         subpath['rectangle'] = []  # it's a list of sublists of points
 
         # store the start point (when 'm' command is encountered)
@@ -503,7 +171,7 @@ class ToolPDF(FlatCAMTool):
             if match:
                 color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
                 log.debug(
-                    "ToolPDF.parse_pdf() --> STROKE Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
+                    "parse_pdf() --> STROKE Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
                     (line_nr, color[0], color[1], color[2]))
 
                 if color[0] == old_color[0] and color[1] == old_color[1] and color[2] == old_color[2]:
@@ -526,7 +194,7 @@ class ToolPDF(FlatCAMTool):
             if match:
                 fill_color = [float(match.group(1)), float(match.group(2)), float(match.group(3))]
                 log.debug(
-                    "ToolPDF.parse_pdf() --> FILL Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
+                    "parse_pdf() --> FILL Color change on line: %s --> RED=%f GREEN=%f BLUE=%f" %
                     (line_nr, fill_color[0], fill_color[1], fill_color[2]))
                 # if the color is white we are seeing 'clear_geometry' that can't be seen. It may be that those
                 # geometries are actually holes from which we can make an Excellon file
@@ -545,7 +213,7 @@ class ToolPDF(FlatCAMTool):
                 # sometimes they combine save_to_graphics_stack with the transformation on the same line
                 if match.group(1) == 'q':
                     log.debug(
-                        "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
+                        "parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
                         (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
 
                     self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
@@ -555,7 +223,7 @@ class ToolPDF(FlatCAMTool):
                 if (float(match.group(3)) == 0 and float(match.group(4)) == 0) and \
                         (float(match.group(6)) != 0 or float(match.group(7)) != 0):
                     log.debug(
-                        "ToolPDF.parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline))
+                        "parse_pdf() --> OFFSET transformation found on line: %s --> %s" % (line_nr, pline))
 
                     offset_geo[0] += float(match.group(6))
                     offset_geo[1] += float(match.group(7))
@@ -564,7 +232,7 @@ class ToolPDF(FlatCAMTool):
                 # transformation = SCALING
                 if float(match.group(2)) != 1 and float(match.group(5)) != 1:
                     log.debug(
-                        "ToolPDF.parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline))
+                        "parse_pdf() --> SCALE transformation found on line: %s --> %s" % (line_nr, pline))
 
                     scale_geo[0] *= float(match.group(2))
                     scale_geo[1] *= float(match.group(5))
@@ -576,7 +244,7 @@ class ToolPDF(FlatCAMTool):
             match = self.save_gs_re.search(pline)
             if match:
                 log.debug(
-                    "ToolPDF.parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
+                    "parse_pdf() --> Save to GS found on line: %s --> offset=[%f, %f] ||| scale=[%f, %f]" %
                     (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
                 self.gs['transform'].append(deepcopy([offset_geo, scale_geo]))
                 self.gs['line_width'].append(deepcopy(size))
@@ -590,18 +258,18 @@ class ToolPDF(FlatCAMTool):
                     scale_geo = restored_transform[1]
                 except IndexError:
                     # nothing to remove
-                    log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
+                    log.debug("parse_pdf() --> Nothing to restore")
                     pass
 
                 try:
                     size = self.gs['line_width'].pop(-1)
                 except IndexError:
-                    log.debug("ToolPDF.parse_pdf() --> Nothing to restore")
+                    log.debug("parse_pdf() --> Nothing to restore")
                     # nothing to remove
                     pass
 
                 log.debug(
-                    "ToolPDF.parse_pdf() --> Restore from GS found on line: %s --> "
+                    "parse_pdf() --> Restore from GS found on line: %s --> "
                     "restored_offset=[%f, %f] ||| restored_scale=[%f, %f]" %
                     (line_nr, offset_geo[0], offset_geo[1], scale_geo[0], scale_geo[1]))
                 # log.debug("Restored Offset= [%f, %f]" % (offset_geo[0], offset_geo[1]))
@@ -712,9 +380,9 @@ class ToolPDF(FlatCAMTool):
                 width = (float(match.group(3)) + offset_geo[0]) * self.point_to_unit_factor * scale_geo[0]
                 height = (float(match.group(4)) + offset_geo[1]) * self.point_to_unit_factor * scale_geo[1]
                 pt1 = (x, y)
-                pt2 = (x+width, y)
-                pt3 = (x+width, y+height)
-                pt4 = (x, y+height)
+                pt2 = (x + width, y)
+                pt3 = (x + width, y + height)
+                pt4 = (x, y + height)
                 subpath['rectangle'] += [pt1, pt2, pt3, pt4, pt1]
                 current_point = pt1
                 continue
@@ -948,7 +616,8 @@ class ToolPDF(FlatCAMTool):
                                 geo += self.bezier_to_points(start=b[0], c1=b[1], c2=b[2], stop=b[3])
                                 # close the subpath if it was not closed already
                                 if close_subpath is False:
-                                    geo.append(geo[0])
+                                    new_g = geo[0]
+                                    geo.append(new_g)
                                 try:
                                     geo_el = Polygon(geo).buffer(0.0000001, resolution=self.step_per_circles)
                                     path_geo.append(geo_el)

+ 1 - 1
flatcamParsers/ParseSVG.py → AppParsers/ParseSVG.py

@@ -27,7 +27,7 @@ from shapely.geometry import LineString, LinearRing, MultiLineString
 from shapely.affinity import skew, affine_transform, rotate
 import numpy as np
 
-from flatcamParsers.ParseFont import *
+from AppParsers.ParseFont import *
 
 log = logging.getLogger('base2')
 

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません