Kaynağa Gözat

Merged in preferences_changes (pull request #12)
Preferences changes

Marius Stanciu 5 yıl önce
ebeveyn
işleme
5624596828

+ 28 - 11
AppDatabase.py

@@ -119,7 +119,7 @@ class ToolsDB(QtWidgets.QWidget):
         )
         )
         self.buttons_box.addWidget(import_db_btn)
         self.buttons_box.addWidget(import_db_btn)
 
 
-        self.add_tool_from_db = FCButton(_("Add Tool from Tools DB"))
+        self.add_tool_from_db = FCButton(_("Transfer Tool"))
         self.add_tool_from_db.setToolTip(
         self.add_tool_from_db.setToolTip(
             _("Add a new tool in the Tools Table of the\n"
             _("Add a new tool in the Tools Table of the\n"
               "active Geometry object after selecting a tool\n"
               "active Geometry object after selecting a tool\n"
@@ -315,7 +315,7 @@ class ToolsDB(QtWidgets.QWidget):
             self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
             self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
             return
             return
 
 
-        self.app.inform.emit('[success] %s: %s' % (_("Loaded FlatCAM Tools DB from"), filename))
+        self.app.inform.emit('[success] %s: %s' % (_("Loaded Tools DB from"), filename))
 
 
         self.build_db_ui()
         self.build_db_ui()
 
 
@@ -726,7 +726,7 @@ class ToolsDB(QtWidgets.QWidget):
                 self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
                 self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
                 return
                 return
 
 
-            self.app.inform.emit('[success] %s: %s' % (_("Loaded FlatCAM Tools DB from"), filename))
+            self.app.inform.emit('[success] %s: %s' % (_("Loaded Tools DB from"), filename))
             self.build_db_ui()
             self.build_db_ui()
             self.callback_on_edited()
             self.callback_on_edited()
 
 
@@ -1792,12 +1792,19 @@ class ToolsDB2(QtWidgets.QWidget):
         )
         )
         self.buttons_box.addWidget(self.save_db_btn)
         self.buttons_box.addWidget(self.save_db_btn)
 
 
-        self.add_tool_from_db = FCButton(_("Add Tool from Tools DB"))
+        self.add_tool_from_db = FCButton(_("Transfer Tool"))
         self.add_tool_from_db.setToolTip(
         self.add_tool_from_db.setToolTip(
-            _("Add a new tool in the Tools Table of the\n"
-              "active Geometry object after selecting a tool\n"
+            _("Insert a new tool in the Tools Table of the\n"
+              "object/application tool after selecting a tool\n"
               "in the Tools Database.")
               "in the Tools Database.")
         )
         )
+        self.add_tool_from_db.setStyleSheet("""
+                                            QPushButton
+                                            {
+                                                font-weight: bold;
+                                                color: green;
+                                            }
+                                            """)
         self.add_tool_from_db.hide()
         self.add_tool_from_db.hide()
 
 
         self.cancel_tool_from_db = FCButton(_("Cancel"))
         self.cancel_tool_from_db = FCButton(_("Cancel"))
@@ -1807,7 +1814,7 @@ class ToolsDB2(QtWidgets.QWidget):
         tree_layout.addLayout(hlay)
         tree_layout.addLayout(hlay)
         hlay.addWidget(self.add_tool_from_db)
         hlay.addWidget(self.add_tool_from_db)
         hlay.addWidget(self.cancel_tool_from_db)
         hlay.addWidget(self.cancel_tool_from_db)
-        hlay.addStretch()
+        # hlay.addStretch()
 
 
         # ##############################################################################
         # ##############################################################################
         # ##############################################################################
         # ##############################################################################
@@ -2015,7 +2022,7 @@ class ToolsDB2(QtWidgets.QWidget):
         self.blockSignals(False)
         self.blockSignals(False)
 
 
     def setup_db_ui(self):
     def setup_db_ui(self):
-        filename = self.app.data_path + '/geo_tools_db.FlatDB'
+        filename = self.app.data_path + '\geo_tools_db.FlatDB'
 
 
         # load the database tools from the file
         # load the database tools from the file
         try:
         try:
@@ -2034,7 +2041,7 @@ class ToolsDB2(QtWidgets.QWidget):
             self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
             self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
             return
             return
 
 
-        self.app.inform.emit('[success] %s: %s' % (_("Loaded FlatCAM Tools DB from"), filename))
+        self.app.inform.emit('[success] %s: %s' % (_("Loaded Tools DB from"), filename))
 
 
         self.build_db_ui()
         self.build_db_ui()
 
 
@@ -2145,8 +2152,18 @@ class ToolsDB2(QtWidgets.QWidget):
             "tools_iso_isotype":        self.app.defaults["tools_iso_isotype"],
             "tools_iso_isotype":        self.app.defaults["tools_iso_isotype"],
         })
         })
 
 
+        temp = []
+        for k, v in self.db_tool_dict.items():
+            if "new_tool_" in v['name']:
+                temp.append(float(v['name'].rpartition('_')[2]))
+
+        if temp:
+            new_name = "new_tool_%d" % int(max(temp) + 1)
+        else:
+            new_name = "new_tool_1"
+
         dict_elem = {}
         dict_elem = {}
-        dict_elem['name'] = 'new_tool'
+        dict_elem['name'] = new_name
         if type(self.app.defaults["geometry_cnctooldia"]) == float:
         if type(self.app.defaults["geometry_cnctooldia"]) == float:
             dict_elem['tooldia'] = self.app.defaults["geometry_cnctooldia"]
             dict_elem['tooldia'] = self.app.defaults["geometry_cnctooldia"]
         else:
         else:
@@ -2323,7 +2340,7 @@ class ToolsDB2(QtWidgets.QWidget):
                 self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
                 self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
                 return
                 return
 
 
-            self.app.inform.emit('[success] %s: %s' % (_("Loaded FlatCAM Tools DB from"), filename))
+            self.app.inform.emit('[success] %s: %s' % (_("Loaded Tools DB from"), filename))
             self.build_db_ui()
             self.build_db_ui()
             self.update_storage()
             self.update_storage()
 
 

+ 1 - 1
AppEditors/FlatCAMGrbEditor.py

@@ -3435,7 +3435,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             else:
             else:
                 # deleted_tool_dia = float(self.apertures_table.item(self.apertures_table.currentRow(), 1).text())
                 # deleted_tool_dia = float(self.apertures_table.item(self.apertures_table.currentRow(), 1).text())
                 if len(self.apertures_table.selectionModel().selectedRows()) == 0:
                 if len(self.apertures_table.selectionModel().selectedRows()) == 0:
-                    self.app.inform.emit('[WARNING_NOTCL]%s' % _(" Select an aperture in Aperture Table"))
+                    self.app.inform.emit('[WARNING_NOTCL] %s' % _(" Select an aperture in Aperture Table"))
                     return
                     return
 
 
                 deleted_apcode_list = []
                 deleted_apcode_list = []

+ 181 - 0
AppGUI/ColumnarFlowLayout.py

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

+ 94 - 0
AppGUI/GUIElements.py

@@ -683,6 +683,100 @@ class NumericalEvalTupleEntry(FCEntry):
         self.setValidator(validator)
         self.setValidator(validator)
 
 
 
 
+class FCColorEntry(QtWidgets.QFrame):
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+
+        self.entry = FCEntry()
+
+        self.button = QtWidgets.QPushButton()
+        self.button.setFixedSize(15, 15)
+        self.button.setStyleSheet("border-color: dimgray;")
+
+        self.layout = QtWidgets.QHBoxLayout()
+        self.layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.layout.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.entry)
+        self.layout.addWidget(self.button)
+        self.setLayout(self.layout)
+
+        self.entry.editingFinished.connect(self._sync_button_color)
+        self.button.clicked.connect(self._on_button_clicked)
+
+    def get_value(self) -> str:
+        return self.entry.get_value()
+
+    def set_value(self, value: str):
+        self.entry.set_value(value)
+        self._sync_button_color()
+
+    def _sync_button_color(self):
+        value = self.get_value()
+        self.button.setStyleSheet("background-color:%s;" % self._extract_color(value))
+
+    def _on_button_clicked(self):
+        value = self.entry.get_value()
+        current_color = QtGui.QColor(self._extract_color(value))
+
+        color_dialog = QtWidgets.QColorDialog()
+        selected_color = color_dialog.getColor(initial=current_color, options=QtWidgets.QColorDialog.ShowAlphaChannel)
+
+        if selected_color.isValid() is False:
+            return
+
+        new_value = str(selected_color.name()) + self._extract_alpha(value)
+        self.set_value(new_value)
+
+    def _extract_color(self, value: str) -> str:
+        return value[:7]
+
+    def _extract_alpha(self, value: str) -> str:
+        return value[7:9]
+
+
+class FCSliderWithSpinner(QtWidgets.QFrame):
+
+    def __init__(self, min=0, max=100, step=1, **kwargs):
+        super().__init__(**kwargs)
+
+        self.slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
+        self.slider.setMinimum(min)
+        self.slider.setMaximum(max)
+        self.slider.setSingleStep(step)
+
+        self.spinner = FCSpinner()
+        self.spinner.set_range(min, max)
+        self.spinner.set_step(step)
+        self.spinner.setMinimumWidth(70)
+
+        self.layout = QtWidgets.QHBoxLayout()
+        self.layout.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
+        self.layout.setContentsMargins(0, 0, 0, 0)
+        self.layout.addWidget(self.slider)
+        self.layout.addWidget(self.spinner)
+        self.setLayout(self.layout)
+
+        self.slider.valueChanged.connect(self._on_slider)
+        self.spinner.valueChanged.connect(self._on_spinner)
+
+        self.valueChanged = self.spinner.valueChanged
+
+    def get_value(self) -> int:
+        return self.spinner.get_value()
+
+    def set_value(self, value: int):
+        self.spinner.set_value(value)
+
+    def _on_spinner(self):
+        spinner_value = self.spinner.value()
+        self.slider.setValue(spinner_value)
+
+    def _on_slider(self):
+        slider_value = self.slider.value()
+        self.spinner.set_value(slider_value)
+
+
 class FCSpinner(QtWidgets.QSpinBox):
 class FCSpinner(QtWidgets.QSpinBox):
 
 
     returnPressed = QtCore.pyqtSignal()
     returnPressed = QtCore.pyqtSignal()

+ 1 - 1
AppGUI/MainGUI.py

@@ -1124,7 +1124,7 @@ class MainGUI(QtWidgets.QMainWindow):
         # #######################################################################
         # #######################################################################
         # ####################### TCL Shell DOCK ################################
         # ####################### TCL Shell DOCK ################################
         # #######################################################################
         # #######################################################################
-        self.shell_dock = FCDock("FlatCAM TCL Shell", close_callback=self.toggle_shell_ui)
+        self.shell_dock = FCDock("TCL Shell", close_callback=self.toggle_shell_ui)
         self.shell_dock.setObjectName('Shell_DockWidget')
         self.shell_dock.setObjectName('Shell_DockWidget')
         self.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas)
         self.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas)
         self.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
         self.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |

+ 7 - 1
AppGUI/ObjectUI.py

@@ -35,7 +35,7 @@ class ObjectUI(QtWidgets.QWidget):
     put UI elements in ObjectUI.custom_box (QtWidgets.QLayout).
     put UI elements in ObjectUI.custom_box (QtWidgets.QLayout).
     """
     """
 
 
-    def __init__(self, app, icon_file='assets/resources/flatcam_icon32.png', title=_('FlatCAM Object'),
+    def __init__(self, app, icon_file='assets/resources/flatcam_icon32.png', title=_('App Object'),
                  parent=None, common=True):
                  parent=None, common=True):
         QtWidgets.QWidget.__init__(self, parent=parent)
         QtWidgets.QWidget.__init__(self, parent=parent)
 
 
@@ -149,6 +149,12 @@ class ObjectUI(QtWidgets.QWidget):
             self.common_grid.addWidget(self.offsetvector_entry, 4, 0)
             self.common_grid.addWidget(self.offsetvector_entry, 4, 0)
             self.common_grid.addWidget(self.offset_button, 4, 1)
             self.common_grid.addWidget(self.offset_button, 4, 1)
 
 
+            self.transformations_button = QtWidgets.QPushButton(_('Transformations'))
+            self.transformations_button.setToolTip(
+                _("Geometrical transformations of the current object.")
+            )
+            self.common_grid.addWidget(self.transformations_button, 5, 0, 1, 2)
+
         layout.addStretch()
         layout.addStretch()
     
     
     def confirmation_message(self, accepted, minval, maxval):
     def confirmation_message(self, accepted, minval, maxval):

+ 327 - 0
AppGUI/preferences/OptionUI.py

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

+ 59 - 1
AppGUI/preferences/OptionsGroupUI.py

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

+ 41 - 0
AppGUI/preferences/PreferencesSectionUI.py

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

+ 302 - 0
AppGUI/preferences/general/GeneralAppSettingsGroupUI.py

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

+ 6 - 6
AppGUI/preferences/general/GeneralGUIPrefGroupUI.py

@@ -35,7 +35,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Theme selection
         # Theme selection
         self.theme_label = QtWidgets.QLabel('%s:' % _('Theme'))
         self.theme_label = QtWidgets.QLabel('%s:' % _('Theme'))
         self.theme_label.setToolTip(
         self.theme_label.setToolTip(
-            _("Select a theme for FlatCAM.\n"
+            _("Select a theme for the application.\n"
               "It will theme the plot area.")
               "It will theme the plot area.")
         )
         )
 
 
@@ -72,7 +72,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Layout selection
         # Layout selection
         self.layout_label = QtWidgets.QLabel('%s:' % _('Layout'))
         self.layout_label = QtWidgets.QLabel('%s:' % _('Layout'))
         self.layout_label.setToolTip(
         self.layout_label.setToolTip(
-            _("Select an layout for FlatCAM.\n"
+            _("Select an layout for the application.\n"
               "It is applied immediately.")
               "It is applied immediately.")
         )
         )
         self.layout_combo = FCComboBox()
         self.layout_combo = FCComboBox()
@@ -94,7 +94,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Style selection
         # Style selection
         self.style_label = QtWidgets.QLabel('%s:' % _('Style'))
         self.style_label = QtWidgets.QLabel('%s:' % _('Style'))
         self.style_label.setToolTip(
         self.style_label.setToolTip(
-            _("Select an style for FlatCAM.\n"
+            _("Select an style for the application.\n"
               "It will be applied at the next app start.")
               "It will be applied at the next app start.")
         )
         )
         self.style_combo = FCComboBox()
         self.style_combo = FCComboBox()
@@ -110,7 +110,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Enable High DPI Support
         # Enable High DPI Support
         self.hdpi_cb = FCCheckBox('%s' % _('Activate HDPI Support'))
         self.hdpi_cb = FCCheckBox('%s' % _('Activate HDPI Support'))
         self.hdpi_cb.setToolTip(
         self.hdpi_cb.setToolTip(
-            _("Enable High DPI support for FlatCAM.\n"
+            _("Enable High DPI support for the application.\n"
               "It will be applied at the next app start.")
               "It will be applied at the next app start.")
         )
         )
 
 
@@ -126,7 +126,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Enable Hover box
         # Enable Hover box
         self.hover_cb = FCCheckBox('%s' % _('Display Hover Shape'))
         self.hover_cb = FCCheckBox('%s' % _('Display Hover Shape'))
         self.hover_cb.setToolTip(
         self.hover_cb.setToolTip(
-            _("Enable display of a hover shape for FlatCAM objects.\n"
+            _("Enable display of a hover shape for the application objects.\n"
               "It is displayed whenever the mouse cursor is hovering\n"
               "It is displayed whenever the mouse cursor is hovering\n"
               "over any kind of not-selected object.")
               "over any kind of not-selected object.")
         )
         )
@@ -135,7 +135,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Enable Selection box
         # Enable Selection box
         self.selection_cb = FCCheckBox('%s' % _('Display Selection Shape'))
         self.selection_cb = FCCheckBox('%s' % _('Display Selection Shape'))
         self.selection_cb.setToolTip(
         self.selection_cb.setToolTip(
-            _("Enable the display of a selection shape for FlatCAM objects.\n"
+            _("Enable the display of a selection shape for the application objects.\n"
               "It is displayed whenever the mouse selects an object\n"
               "It is displayed whenever the mouse selects an object\n"
               "either by clicking or dragging mouse from left to right or\n"
               "either by clicking or dragging mouse from left to right or\n"
               "right to left.")
               "right to left.")

+ 1 - 2
AppGUI/preferences/tools/ToolsFilmPrefGroupUI.py

@@ -30,8 +30,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         # ## Parameters
         # ## Parameters
         self.film_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.film_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.film_label.setToolTip(
         self.film_label.setToolTip(
-            _("Create a PCB film from a Gerber or Geometry\n"
-              "FlatCAM object.\n"
+            _("Create a PCB film from a Gerber or Geometry object.\n"
               "The file is saved in SVG format.")
               "The file is saved in SVG format.")
         )
         )
         self.layout.addWidget(self.film_label)
         self.layout.addWidget(self.film_label)

+ 2 - 2
AppGUI/preferences/tools/ToolsPaintPrefGroupUI.py

@@ -105,7 +105,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
         cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
         cutzlabel = QtWidgets.QLabel('%s:' % _('Cut Z'))
         cutzlabel.setToolTip(
         cutzlabel.setToolTip(
             _("Depth of cut into material. Negative value.\n"
             _("Depth of cut into material. Negative value.\n"
-              "In FlatCAM units.")
+              "In application units.")
         )
         )
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry.set_precision(self.decimals)
         self.cutz_entry.set_precision(self.decimals)
@@ -114,7 +114,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
 
 
         self.cutz_entry.setToolTip(
         self.cutz_entry.setToolTip(
             _("Depth of cut into material. Negative value.\n"
             _("Depth of cut into material. Negative value.\n"
-              "In FlatCAM units.")
+              "In application units.")
         )
         )
         grid0.addWidget(cutzlabel, 4, 0)
         grid0.addWidget(cutzlabel, 4, 0)
         grid0.addWidget(self.cutz_entry, 4, 1)
         grid0.addWidget(self.cutz_entry, 4, 1)

+ 1 - 1
AppGUI/preferences/tools/ToolsTransformPrefGroupUI.py

@@ -31,7 +31,7 @@ class ToolsTransformPrefGroupUI(OptionsGroupUI):
         self.transform_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.transform_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.transform_label.setToolTip(
         self.transform_label.setToolTip(
             _("Various transformations that can be applied\n"
             _("Various transformations that can be applied\n"
-              "on a FlatCAM object.")
+              "on a application object.")
         )
         )
         self.layout.addWidget(self.transform_label)
         self.layout.addWidget(self.transform_label)
 
 

+ 4 - 4
AppObjects/FlatCAMCNCJob.py

@@ -504,17 +504,17 @@ class CNCJobObject(FlatCAMObj, CNCjob):
         try:
         try:
             dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
             dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
             filename, _f = FCFileSaveDialog.get_saved_filename(
             filename, _f = FCFileSaveDialog.get_saved_filename(
-                caption=_("Export Machine Code ..."),
+                caption=_("Export Code ..."),
                 directory=dir_file_to_save,
                 directory=dir_file_to_save,
                 ext_filter=_filter_
                 ext_filter=_filter_
             )
             )
         except TypeError:
         except TypeError:
-            filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Machine Code ..."), ext_filter=_filter_)
+            filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Code ..."), ext_filter=_filter_)
 
 
         filename = str(filename)
         filename = str(filename)
 
 
         if filename == '':
         if filename == '':
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export Machine Code cancelled ..."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
             return
             return
         else:
         else:
             if save_gcode is True:
             if save_gcode is True:
@@ -535,7 +535,7 @@ class CNCJobObject(FlatCAMObj, CNCjob):
         if self.app.defaults["global_open_style"] is False:
         if self.app.defaults["global_open_style"] is False:
             self.app.file_opened.emit("gcode", filename)
             self.app.file_opened.emit("gcode", filename)
         self.app.file_saved.emit("gcode", filename)
         self.app.file_saved.emit("gcode", filename)
-        self.app.inform.emit('[success] %s: %s' % (_("Machine Code file saved to"), filename))
+        self.app.inform.emit('[success] %s: %s' % (_("File saved to"), filename))
 
 
     def on_edit_code_click(self, *args):
     def on_edit_code_click(self, *args):
         """
         """

+ 6 - 1
AppObjects/FlatCAMObj.py

@@ -139,7 +139,7 @@ class FlatCAMObj(QtCore.QObject):
                 except KeyError:
                 except KeyError:
                     log.debug("FlatCAMObj.from_dict() --> KeyError: %s. "
                     log.debug("FlatCAMObj.from_dict() --> KeyError: %s. "
                               "Means that we are loading an old project that don't"
                               "Means that we are loading an old project that don't"
-                              "have all attributes in the latest FlatCAM." % str(attr))
+                              "have all attributes in the latest application version." % str(attr))
                     pass
                     pass
 
 
     def on_options_change(self, key):
     def on_options_change(self, key):
@@ -182,6 +182,11 @@ class FlatCAMObj(QtCore.QObject):
         except (TypeError, AttributeError):
         except (TypeError, AttributeError):
             pass
             pass
 
 
+        try:
+            self.ui.transformations_button.clicked.connect(self.app.transform_tool.run)
+        except (TypeError, AttributeError):
+            pass
+
         # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
         # self.ui.skew_button.clicked.connect(self.on_skew_button_click)
 
 
     def build_ui(self):
     def build_ui(self):

+ 8 - 8
AppTools/ToolAlignObjects.py

@@ -308,13 +308,6 @@ class AlignObjects(AppTool):
         else:
         else:
             self.grid_status_memory = False
             self.grid_status_memory = False
 
 
-        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
-
-        if self.app.is_legacy is False:
-            self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
-        else:
-            self.canvas.graph_event_disconnect(self.app.mr)
-
         self.local_connected = True
         self.local_connected = True
 
 
         self.aligner_old_fill_color = self.aligner_obj.fill_color
         self.aligner_old_fill_color = self.aligner_obj.fill_color
@@ -322,10 +315,17 @@ class AlignObjects(AppTool):
         self.aligned_old_fill_color = self.aligned_obj.fill_color
         self.aligned_old_fill_color = self.aligned_obj.fill_color
         self.aligned_old_line_color = self.aligned_obj.outline_color
         self.aligned_old_line_color = self.aligned_obj.outline_color
 
 
-        self.app.inform.emit('%s: %s' % (_("First Point"), _("Click on the START point.")))
         self.target_obj = self.aligned_obj
         self.target_obj = self.aligned_obj
         self.set_color()
         self.set_color()
 
 
+        self.app.inform.emit('%s: %s' % (_("First Point"), _("Click on the START point.")))
+        self.mr = self.canvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
+
+        if self.app.is_legacy is False:
+            self.canvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
+        else:
+            self.canvas.graph_event_disconnect(self.app.mr)
+
     def on_mouse_click_release(self, event):
     def on_mouse_click_release(self, event):
         if self.app.is_legacy is False:
         if self.app.is_legacy is False:
             event_pos = event.pos
             event_pos = event.pos

+ 1 - 1
AppTools/ToolCalibration.py

@@ -64,7 +64,7 @@ class ToolCalibration(AppTool):
         grid_lay.setColumnStretch(1, 1)
         grid_lay.setColumnStretch(1, 1)
         grid_lay.setColumnStretch(2, 0)
         grid_lay.setColumnStretch(2, 0)
 
 
-        self.gcode_title_label = QtWidgets.QLabel('<b>%s</b>' % _('GCode Parameters'))
+        self.gcode_title_label = QtWidgets.QLabel('<b>%s:</b>' % _('Parameters'))
         self.gcode_title_label.setToolTip(
         self.gcode_title_label.setToolTip(
             _("Parameters used when creating the GCode in this tool.")
             _("Parameters used when creating the GCode in this tool.")
         )
         )

+ 6 - 2
AppTools/ToolCopperThieving.py

@@ -77,8 +77,12 @@ class ToolCopperThieving(AppTool):
         )
         )
 
 
         i_grid_lay.addWidget(self.grbobj_label, 0, 0)
         i_grid_lay.addWidget(self.grbobj_label, 0, 0)
-        i_grid_lay.addWidget(self.grb_object_combo, 0, 1, 1, 2)
-        i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0)
+        i_grid_lay.addWidget(self.grb_object_combo, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        i_grid_lay.addWidget(separator_line, 2, 0, 1, 2)
 
 
         # ## Grid Layout
         # ## Grid Layout
         grid_lay = QtWidgets.QGridLayout()
         grid_lay = QtWidgets.QGridLayout()

+ 15 - 13
AppTools/ToolDistance.py

@@ -511,12 +511,13 @@ class Distance(AppTool):
             self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
             self.distance_x_entry.set_value('%.*f' % (self.decimals, abs(dx)))
             self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
             self.distance_y_entry.set_value('%.*f' % (self.decimals, abs(dy)))
 
 
-            if dx != 0.0:
-                try:
-                    angle = math.degrees(math.atan(dy / dx))
-                    self.angle_entry.set_value('%.*f' % (self.decimals, angle))
-                except Exception:
-                    pass
+            try:
+                angle = math.degrees(math.atan2(dy, dx))
+                if angle < 0:
+                    angle += 360
+                self.angle_entry.set_value('%.*f' % (self.decimals, angle))
+            except Exception:
+                pass
 
 
             self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
             self.total_distance_entry.set_value('%.*f' % (self.decimals, abs(d)))
             # self.app.ui.rel_position_label.setText(
             # self.app.ui.rel_position_label.setText(
@@ -582,13 +583,14 @@ class Distance(AppTool):
             if len(self.points) == 1:
             if len(self.points) == 1:
                 self.utility_geometry(pos=pos)
                 self.utility_geometry(pos=pos)
                 # and display the temporary angle
                 # and display the temporary angle
-                if dx != 0.0:
-                    try:
-                        angle = math.degrees(math.atan(dy / dx))
-                        self.angle_entry.set_value('%.*f' % (self.decimals, angle))
-                    except Exception as e:
-                        log.debug("Distance.on_mouse_move_meas() -> update utility geometry -> %s" % str(e))
-                        pass
+                try:
+                    angle = math.degrees(math.atan2(dy, dx))
+                    if angle < 0:
+                        angle += 360
+                    self.angle_entry.set_value('%.*f' % (self.decimals, angle))
+                except Exception as e:
+                    log.debug("Distance.on_mouse_move_meas() -> update utility geometry -> %s" % str(e))
+                    pass
 
 
         except Exception as e:
         except Exception as e:
             log.debug("Distance.on_mouse_move_meas() --> %s" % str(e))
             log.debug("Distance.on_mouse_move_meas() --> %s" % str(e))

+ 8 - 2
AppTools/ToolEtchCompensation.py

@@ -78,7 +78,10 @@ class ToolEtchCompensation(AppTool):
         grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
         grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
         grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
         grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
 
 
-        grid0.addWidget(QtWidgets.QLabel(""), 3, 0, 1, 2)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 3, 0, 1, 2)
 
 
         self.util_label = QtWidgets.QLabel("<b>%s:</b>" % _("Utilities"))
         self.util_label = QtWidgets.QLabel("<b>%s:</b>" % _("Utilities"))
         self.util_label.setToolTip('%s.' % _("Conversion utilities"))
         self.util_label.setToolTip('%s.' % _("Conversion utilities"))
@@ -127,7 +130,10 @@ class ToolEtchCompensation(AppTool):
         hlay_2.addWidget(self.mils_to_um_entry)
         hlay_2.addWidget(self.mils_to_um_entry)
         grid0.addLayout(hlay_2, 8, 0, 1, 2)
         grid0.addLayout(hlay_2, 8, 0, 1, 2)
 
 
-        grid0.addWidget(QtWidgets.QLabel(""), 9, 0, 1, 2)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 9, 0, 1, 2)
 
 
         self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.param_label.setToolTip('%s.' % _("Parameters for this tool"))
         self.param_label.setToolTip('%s.' % _("Parameters for this tool"))

+ 7 - 4
AppTools/ToolFiducials.py

@@ -63,9 +63,6 @@ class ToolFiducials(AppTool):
         self.points_table = FCTable()
         self.points_table = FCTable()
         self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
         self.points_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
 
 
-        self.layout.addWidget(self.points_table)
-        self.layout.addWidget(QtWidgets.QLabel(''))
-
         self.points_table.setColumnCount(3)
         self.points_table.setColumnCount(3)
         self.points_table.setHorizontalHeaderLabels(
         self.points_table.setHorizontalHeaderLabels(
             [
             [
@@ -76,7 +73,6 @@ class ToolFiducials(AppTool):
         )
         )
         self.points_table.setRowCount(3)
         self.points_table.setRowCount(3)
         row = 0
         row = 0
-
         flags = QtCore.Qt.ItemIsEnabled
         flags = QtCore.Qt.ItemIsEnabled
 
 
         # BOTTOM LEFT
         # BOTTOM LEFT
@@ -140,6 +136,13 @@ class ToolFiducials(AppTool):
         for row in range(self.points_table.rowCount()):
         for row in range(self.points_table.rowCount()):
             self.points_table.cellWidget(row, 2).setFrame(False)
             self.points_table.cellWidget(row, 2).setFrame(False)
 
 
+        self.layout.addWidget(self.points_table)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        self.layout.addWidget(separator_line)
+
         # ## Grid Layout
         # ## Grid Layout
         grid_lay = QtWidgets.QGridLayout()
         grid_lay = QtWidgets.QGridLayout()
         self.layout.addLayout(grid_lay)
         self.layout.addLayout(grid_lay)

+ 4 - 1
AppTools/ToolInvertGerber.py

@@ -77,7 +77,10 @@ class ToolInvertGerber(AppTool):
         grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
         grid0.addWidget(self.gerber_label, 1, 0, 1, 2)
         grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
         grid0.addWidget(self.gerber_combo, 2, 0, 1, 2)
 
 
-        grid0.addWidget(QtWidgets.QLabel(""), 3, 0, 1, 2)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 3, 0, 1, 2)
 
 
         self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         self.param_label.setToolTip('%s.' % _("Parameters for this tool"))
         self.param_label.setToolTip('%s.' % _("Parameters for this tool"))

+ 10 - 7
AppTools/ToolIsolation.py

@@ -9,7 +9,7 @@ from PyQt5 import QtWidgets, QtCore, QtGui
 
 
 from AppTool import AppTool
 from AppTool import AppTool
 from AppGUI.GUIElements import FCCheckBox, FCDoubleSpinner, RadioSet, FCTable, FCInputDialog, FCButton, \
 from AppGUI.GUIElements import FCCheckBox, FCDoubleSpinner, RadioSet, FCTable, FCInputDialog, FCButton, \
-    FCComboBox, OptionalHideInputSection, FCSpinner
+    FCComboBox, OptionalInputSection, FCSpinner
 from AppParsers.ParseGerber import Gerber
 from AppParsers.ParseGerber import Gerber
 
 
 from copy import deepcopy
 from copy import deepcopy
@@ -484,11 +484,11 @@ class ToolIsolation(AppTool, Gerber):
 
 
         self.grid3.addWidget(self.exc_obj_combo, 29, 0, 1, 2)
         self.grid3.addWidget(self.exc_obj_combo, 29, 0, 1, 2)
 
 
-        self.e_ois = OptionalHideInputSection(self.except_cb,
-                                              [
-                                                  self.type_excobj_radio,
-                                                  self.exc_obj_combo
-                                              ])
+        self.e_ois = OptionalInputSection(self.except_cb,
+                                          [
+                                              self.type_excobj_radio,
+                                              self.exc_obj_combo
+                                          ])
 
 
         # Isolation Scope
         # Isolation Scope
         self.select_label = QtWidgets.QLabel('%s:' % _("Selection"))
         self.select_label = QtWidgets.QLabel('%s:' % _("Selection"))
@@ -1691,6 +1691,7 @@ class ToolIsolation(AppTool, Gerber):
             else:
             else:
                 self.grid_status_memory = False
                 self.grid_status_memory = False
 
 
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it."))
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_poly_mouse_click_release)
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_poly_mouse_click_release)
             self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
             self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
 
 
@@ -1703,7 +1704,6 @@ class ToolIsolation(AppTool, Gerber):
             # disconnect flags
             # disconnect flags
             self.poly_sel_disconnect_flag = True
             self.poly_sel_disconnect_flag = True
 
 
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it."))
         elif selection == _("Reference Object"):
         elif selection == _("Reference Object"):
             ref_obj = self.app.collection.get_by_name(self.reference_combo.get_value())
             ref_obj = self.app.collection.get_by_name(self.reference_combo.get_value())
             ref_geo = cascaded_union(ref_obj.solid_geometry)
             ref_geo = cascaded_union(ref_obj.solid_geometry)
@@ -1865,6 +1865,9 @@ class ToolIsolation(AppTool, Gerber):
 
 
                     self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
                     self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
 
 
+        # Switch notebook to Selected page
+        self.app.ui.notebook.setCurrentWidget(self.app.ui.selected_tab)
+
     def combined_rest(self, iso_obj, iso2geo, tools_storage, lim_area, plot=True):
     def combined_rest(self, iso_obj, iso2geo, tools_storage, lim_area, plot=True):
         """
         """
 
 

+ 7 - 1
AppTools/ToolOptimal.py

@@ -72,7 +72,13 @@ class ToolOptimal(AppTool):
         self.gerber_object_label.setToolTip(
         self.gerber_object_label.setToolTip(
             "Gerber object for which to find the minimum distance between copper features."
             "Gerber object for which to find the minimum distance between copper features."
         )
         )
-        form_lay.addRow(self.gerber_object_label, self.gerber_object_combo)
+        form_lay.addRow(self.gerber_object_label)
+        form_lay.addRow(self.gerber_object_combo)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        form_lay.addRow(separator_line)
 
 
         # Precision = nr of decimals
         # Precision = nr of decimals
         self.precision_label = QtWidgets.QLabel('%s:' % _("Precision"))
         self.precision_label = QtWidgets.QLabel('%s:' % _("Precision"))

+ 2 - 2
AppTools/ToolPaint.py

@@ -1429,8 +1429,6 @@ class ToolPaint(AppTool, Gerber):
                                 outname=self.o_name)
                                 outname=self.o_name)
 
 
         elif self.select_method == _("Polygon Selection"):
         elif self.select_method == _("Polygon Selection"):
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to paint it."))
-
             # disengage the grid snapping since it may be hard to click on polygons with grid snapping on
             # 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():
             if self.app.ui.grid_snap_btn.isChecked():
                 self.grid_status_memory = True
                 self.grid_status_memory = True
@@ -1438,6 +1436,8 @@ class ToolPaint(AppTool, Gerber):
             else:
             else:
                 self.grid_status_memory = False
                 self.grid_status_memory = False
 
 
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to paint it."))
+
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_single_poly_mouse_release)
             self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_single_poly_mouse_release)
             self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
             self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
 
 

+ 1 - 1
AppTools/ToolPanelize.py

@@ -438,7 +438,7 @@ class Panelize(AppTool):
             return "Could not retrieve object: %s" % boxname
             return "Could not retrieve object: %s" % boxname
 
 
         if box is None:
         if box is None:
-            self.app.inform.emit('[WARNING_NOTCL]%s: %s' % (_("No object Box. Using instead"), panel_source_obj))
+            self.app.inform.emit('[WARNING_NOTCL] %s: %s' % (_("No object Box. Using instead"), panel_source_obj))
             self.reference_radio.set_value('bbox')
             self.reference_radio.set_value('bbox')
 
 
         if self.reference_radio.get_value() == 'bbox':
         if self.reference_radio.get_value() == 'bbox':

+ 27 - 18
AppTools/ToolQRCode.py

@@ -75,14 +75,35 @@ class QRCode(AppTool):
         self.grb_object_combo.is_last = True
         self.grb_object_combo.is_last = True
         self.grb_object_combo.obj_type = "Gerber"
         self.grb_object_combo.obj_type = "Gerber"
 
 
-        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("Object"))
+        self.grbobj_label = QtWidgets.QLabel("<b>%s:</b>" % _("GERBER"))
         self.grbobj_label.setToolTip(
         self.grbobj_label.setToolTip(
             _("Gerber Object to which the QRCode will be added.")
             _("Gerber Object to which the QRCode will be added.")
         )
         )
 
 
         i_grid_lay.addWidget(self.grbobj_label, 0, 0)
         i_grid_lay.addWidget(self.grbobj_label, 0, 0)
-        i_grid_lay.addWidget(self.grb_object_combo, 0, 1, 1, 2)
-        i_grid_lay.addWidget(QtWidgets.QLabel(''), 1, 0)
+        i_grid_lay.addWidget(self.grb_object_combo, 1, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        i_grid_lay.addWidget(separator_line, 2, 0, 1, 2)
+
+        # Text box
+        self.text_label = QtWidgets.QLabel('<b>%s</b>:' % _("QRCode Data"))
+        self.text_label.setToolTip(
+            _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
+        )
+        self.text_data = FCTextArea()
+        self.text_data.setPlaceholderText(
+            _("Add here the text to be included in the QRCode...")
+        )
+        i_grid_lay.addWidget(self.text_label, 5, 0)
+        i_grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        i_grid_lay.addWidget(separator_line, 7, 0, 1, 2)
 
 
         # ## Grid Layout
         # ## Grid Layout
         grid_lay = QtWidgets.QGridLayout()
         grid_lay = QtWidgets.QGridLayout()
@@ -90,7 +111,7 @@ class QRCode(AppTool):
         grid_lay.setColumnStretch(0, 0)
         grid_lay.setColumnStretch(0, 0)
         grid_lay.setColumnStretch(1, 1)
         grid_lay.setColumnStretch(1, 1)
 
 
-        self.qrcode_label = QtWidgets.QLabel('<b>%s</b>' % _('QRCode Parameters'))
+        self.qrcode_label = QtWidgets.QLabel('<b>%s</b>' % _('Parameters'))
         self.qrcode_label.setToolTip(
         self.qrcode_label.setToolTip(
             _("The parameters used to shape the QRCode.")
             _("The parameters used to shape the QRCode.")
         )
         )
@@ -158,18 +179,6 @@ class QRCode(AppTool):
         grid_lay.addWidget(self.border_size_label, 4, 0)
         grid_lay.addWidget(self.border_size_label, 4, 0)
         grid_lay.addWidget(self.border_size_entry, 4, 1)
         grid_lay.addWidget(self.border_size_entry, 4, 1)
 
 
-        # Text box
-        self.text_label = QtWidgets.QLabel('%s:' % _("QRCode Data"))
-        self.text_label.setToolTip(
-            _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
-        )
-        self.text_data = FCTextArea()
-        self.text_data.setPlaceholderText(
-            _("Add here the text to be included in the QRCode...")
-        )
-        grid_lay.addWidget(self.text_label, 5, 0)
-        grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
-
         # POLARITY CHOICE #
         # POLARITY CHOICE #
         self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
         self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
         self.pol_label.setToolTip(
         self.pol_label.setToolTip(
@@ -788,7 +797,7 @@ class QRCode(AppTool):
         filename = str(filename)
         filename = str(filename)
 
 
         if filename == "":
         if filename == "":
-            self.app.inform.emit('[WARNING_NOTCL]%s' % _("Cancelled."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
             return
             return
         else:
         else:
             self.app.worker_task.emit({'fcn': job_thread_qr_png, 'params': [self.app, filename]})
             self.app.worker_task.emit({'fcn': job_thread_qr_png, 'params': [self.app, filename]})
@@ -835,7 +844,7 @@ class QRCode(AppTool):
         filename = str(filename)
         filename = str(filename)
 
 
         if filename == "":
         if filename == "":
-            self.app.inform.emit('[WARNING_NOTCL]%s' % _("Cancelled."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
             return
             return
         else:
         else:
             self.app.worker_task.emit({'fcn': job_thread_qr_svg, 'params': [self.app, filename]})
             self.app.worker_task.emit({'fcn': job_thread_qr_svg, 'params': [self.app, filename]})

+ 27 - 20
AppTools/ToolSolderPaste.py

@@ -64,11 +64,16 @@ class SolderPaste(AppTool):
         self.obj_combo.is_last = True
         self.obj_combo.is_last = True
         self.obj_combo.obj_type = "Gerber"
         self.obj_combo.obj_type = "Gerber"
 
 
-        self.object_label = QtWidgets.QLabel("Gerber:   ")
-        self.object_label.setToolTip(
-            _("Gerber Solder paste object.                        ")
+        self.object_label = QtWidgets.QLabel('<b>%s</b>:'% _("GERBER"))
+        self.object_label.setToolTip(_("Gerber Solder paste object.")
         )
         )
-        obj_form_layout.addRow(self.object_label, self.obj_combo)
+        obj_form_layout.addRow(self.object_label)
+        obj_form_layout.addRow(self.obj_combo)
+
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        obj_form_layout.addRow(separator_line)
 
 
         # ### Tools ## ##
         # ### Tools ## ##
         self.tools_table_label = QtWidgets.QLabel('<b>%s</b>' % _('Tools Table'))
         self.tools_table_label = QtWidgets.QLabel('<b>%s</b>' % _('Tools Table'))
@@ -131,22 +136,13 @@ class SolderPaste(AppTool):
              "by first selecting a row(s) in the Tool Table.")
              "by first selecting a row(s) in the Tool Table.")
         )
         )
 
 
-        self.soldergeo_btn = QtWidgets.QPushButton(_("Generate Geo"))
-        self.soldergeo_btn.setToolTip(
-            _("Generate solder paste dispensing geometry.")
-        )
-        self.soldergeo_btn.setStyleSheet("""
-                        QPushButton
-                        {
-                            font-weight: bold;
-                        }
-                        """)
-
         grid0.addWidget(self.addtool_btn, 0, 0)
         grid0.addWidget(self.addtool_btn, 0, 0)
-        # grid2.addWidget(self.copytool_btn, 0, 1)
         grid0.addWidget(self.deltool_btn, 0, 2)
         grid0.addWidget(self.deltool_btn, 0, 2)
 
 
-        self.layout.addSpacing(10)
+        separator_line = QtWidgets.QFrame()
+        separator_line.setFrameShape(QtWidgets.QFrame.HLine)
+        separator_line.setFrameShadow(QtWidgets.QFrame.Sunken)
+        grid0.addWidget(separator_line, 1, 0, 1, 3)
 
 
         # ## Buttons
         # ## Buttons
         grid0_1 = QtWidgets.QGridLayout()
         grid0_1 = QtWidgets.QGridLayout()
@@ -373,6 +369,18 @@ class SolderPaste(AppTool):
             _("Second step is to create a solder paste dispensing\n"
             _("Second step is to create a solder paste dispensing\n"
               "geometry out of an Solder Paste Mask Gerber file.")
               "geometry out of an Solder Paste Mask Gerber file.")
         )
         )
+
+        self.soldergeo_btn = QtWidgets.QPushButton(_("Generate Geo"))
+        self.soldergeo_btn.setToolTip(
+            _("Generate solder paste dispensing geometry.")
+        )
+        self.soldergeo_btn.setStyleSheet("""
+                                        QPushButton
+                                        {
+                                            font-weight: bold;
+                                        }
+                                        """)
+
         grid2.addWidget(step2_lbl, 0, 0)
         grid2.addWidget(step2_lbl, 0, 0)
         grid2.addWidget(self.soldergeo_btn, 0, 2)
         grid2.addWidget(self.soldergeo_btn, 0, 2)
 
 
@@ -1497,11 +1505,10 @@ class SolderPaste(AppTool):
             )
             )
         except TypeError:
         except TypeError:
             filename, _f = FCFileSaveDialog.get_saved_filename(
             filename, _f = FCFileSaveDialog.get_saved_filename(
-                caption=_("Export Machine Code ..."), ext_filter=_filter_)
+                caption=_("Export Code ..."), ext_filter=_filter_)
 
 
         if filename == '':
         if filename == '':
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Export Machine Code cancelled ..."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
             return
             return
 
 
         gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \
         gcode = '(G-CODE GENERATED BY FLATCAM v%s - www.flatcam.org - Version Date: %s)\n' % \

+ 21 - 10
App_Main.py

@@ -381,7 +381,7 @@ class App(QtCore.QObject):
             f.close()
             f.close()
 
 
         # Write factory_defaults.FlatConfig file to disk
         # Write factory_defaults.FlatConfig file to disk
-        FlatCAMDefaults.save_factory_defaults(os.path.join(self.data_path, "factory_defaults.FlatConfig"))
+        FlatCAMDefaults.save_factory_defaults(os.path.join(self.data_path, "factory_defaults.FlatConfig"), self.version)
 
 
         # create a recent files json file if there is none
         # create a recent files json file if there is none
         try:
         try:
@@ -1488,6 +1488,11 @@ class App(QtCore.QObject):
         self.prj_list = ['flatprj']
         self.prj_list = ['flatprj']
         self.conf_list = ['flatconfig']
         self.conf_list = ['flatconfig']
 
 
+        # last used filters
+        self.last_op_gerber_filter = None
+        self.last_op_excellon_filter = None
+        self.last_op_gcode_filter = None
+
         # global variable used by NCC Tool to signal that some polygons could not be cleared, if True
         # global variable used by NCC Tool to signal that some polygons could not be cleared, if True
         # flag for polygons not cleared
         # flag for polygons not cleared
         self.poly_not_cleared = False
         self.poly_not_cleared = False
@@ -4088,7 +4093,7 @@ class App(QtCore.QObject):
             val_x = float(self.defaults['global_gridx'])
             val_x = float(self.defaults['global_gridx'])
             val_y = float(self.defaults['global_gridy'])
             val_y = float(self.defaults['global_gridy'])
 
 
-            self.inform.emit('[WARNING_NOTCL]%s' % _("Cancelled."))
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
 
 
         self.preferencesUiManager.defaults_read_form()
         self.preferencesUiManager.defaults_read_form()
 
 
@@ -5735,7 +5740,7 @@ class App(QtCore.QObject):
             name = obj.options["name"]
             name = obj.options["name"]
         except AttributeError:
         except AttributeError:
             log.debug("on_copy_name() --> No object selected to copy it's name")
             log.debug("on_copy_name() --> No object selected to copy it's name")
-            self.inform.emit('[WARNING_NOTCL]%s' %
+            self.inform.emit('[WARNING_NOTCL] %s' %
                              _(" No object selected to copy it's name"))
                              _(" No object selected to copy it's name"))
             return
             return
 
 
@@ -6559,11 +6564,13 @@ class App(QtCore.QObject):
             try:
             try:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"),
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"),
                                                                        directory=self.get_last_folder(),
                                                                        directory=self.get_last_folder(),
-                                                                       filter=_filter_)
+                                                                       filter=_filter_,
+                                                                       initialFilter=self.last_op_gerber_filter)
             except TypeError:
             except TypeError:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"), filter=_filter_)
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"), filter=_filter_)
 
 
             filenames = [str(filename) for filename in filenames]
             filenames = [str(filename) for filename in filenames]
+            self.last_op_gerber_filter = _f
         else:
         else:
             filenames = [name]
             filenames = [name]
             self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
             self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
@@ -6597,10 +6604,12 @@ class App(QtCore.QObject):
             try:
             try:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"),
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"),
                                                                        directory=self.get_last_folder(),
                                                                        directory=self.get_last_folder(),
-                                                                       filter=_filter_)
+                                                                       filter=_filter_,
+                                                                       initialFilter=self.last_op_excellon_filter)
             except TypeError:
             except TypeError:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"), filter=_filter_)
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"), filter=_filter_)
             filenames = [str(filename) for filename in filenames]
             filenames = [str(filename) for filename in filenames]
+            self.last_op_excellon_filter = _f
         else:
         else:
             filenames = [str(name)]
             filenames = [str(name)]
             self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
             self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
@@ -6610,7 +6619,7 @@ class App(QtCore.QObject):
                                     color=QtGui.QColor("gray"))
                                     color=QtGui.QColor("gray"))
 
 
         if len(filenames) == 0:
         if len(filenames) == 0:
-            self.inform.emit('[WARNING_NOTCL]%s' % _("Cancelled."))
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
         else:
         else:
             for filename in filenames:
             for filename in filenames:
                 if filename != '':
                 if filename != '':
@@ -6638,11 +6647,13 @@ class App(QtCore.QObject):
             try:
             try:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"),
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"),
                                                                        directory=self.get_last_folder(),
                                                                        directory=self.get_last_folder(),
-                                                                       filter=_filter_)
+                                                                       filter=_filter_,
+                                                                       initialFilter=self.last_op_gcode_filter)
             except TypeError:
             except TypeError:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"), filter=_filter_)
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"), filter=_filter_)
 
 
             filenames = [str(filename) for filename in filenames]
             filenames = [str(filename) for filename in filenames]
+            self.last_op_gcode_filter = _f
         else:
         else:
             filenames = [name]
             filenames = [name]
             self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
             self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
@@ -6803,7 +6814,7 @@ class App(QtCore.QObject):
         filename = str(filename)
         filename = str(filename)
 
 
         if filename == "":
         if filename == "":
-            self.inform.emit('[WARNING_NOTCL]%s' % _("Cancelled."))
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
             return
             return
         else:
         else:
             self.export_svg(name, filename)
             self.export_svg(name, filename)
@@ -6821,7 +6832,7 @@ class App(QtCore.QObject):
 
 
         data = None
         data = None
         if self.is_legacy is False:
         if self.is_legacy is False:
-            image = _screenshot(alpha=None)
+            image = _screenshot(alpha=False)
             data = np.asarray(image)
             data = np.asarray(image)
             if not data.ndim == 3 and data.shape[-1] in (3, 4):
             if not data.ndim == 3 and data.shape[-1] in (3, 4):
                 self.inform.emit('[[WARNING_NOTCL]] %s' % _('Data must be a 3D array with last dimension 3 or 4'))
                 self.inform.emit('[[WARNING_NOTCL]] %s' % _('Data must be a 3D array with last dimension 3 or 4'))
@@ -8895,7 +8906,7 @@ class App(QtCore.QObject):
         """
         """
         self.log.debug("Plot_all()")
         self.log.debug("Plot_all()")
         if muted is not True:
         if muted is not True:
-            self.inform.emit('[success] %s...' % _("Redrawing all objects"))
+            self.inform[str, bool].emit('[success] %s...' % _("Redrawing all objects"), False)
 
 
         for plot_obj in self.collection.get_list():
         for plot_obj in self.collection.get_list():
             def worker_task(obj):
             def worker_task(obj):

+ 3 - 3
Bookmark.py

@@ -287,8 +287,8 @@ class BookmarkManager(QtWidgets.QWidget):
         date = date.replace(' ', '_')
         date = date.replace(' ', '_')
 
 
         filter__ = "Text File (*.TXT);;All Files (*.*)"
         filter__ = "Text File (*.TXT);;All Files (*.*)"
-        filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export FlatCAM Bookmarks"),
-                                                           directory='{l_save}/FlatCAM_{n}_{date}'.format(
+        filename, _f = FCFileSaveDialog.get_saved_filename(caption=_("Export Bookmarks"),
+                                                           directory='{l_save}/{n}_{date}'.format(
                                                                 l_save=str(self.app.get_last_save_folder()),
                                                                 l_save=str(self.app.get_last_save_folder()),
                                                                 n=_("Bookmarks"),
                                                                 n=_("Bookmarks"),
                                                                 date=date),
                                                                 date=date),
@@ -334,7 +334,7 @@ class BookmarkManager(QtWidgets.QWidget):
         self.app.log.debug("on_import_bookmarks()")
         self.app.log.debug("on_import_bookmarks()")
 
 
         filter_ = "Text File (*.txt);;All Files (*.*)"
         filter_ = "Text File (*.txt);;All Files (*.*)"
-        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import FlatCAM Bookmarks"), filter=filter_)
+        filename, _f = QtWidgets.QFileDialog.getOpenFileName(caption=_("Import Bookmarks"), filter=filter_)
 
 
         filename = str(filename)
         filename = str(filename)
 
 

+ 20 - 0
CHANGELOG.md

@@ -7,6 +7,26 @@ CHANGELOG for FlatCAM beta
 
 
 =================================================
 =================================================
 
 
+1.06.2020
+
+- made the Distance Tool display the angle in values between 0 and 359.9999 degrees
+- changed some strings
+- fixed the warning that old preferences found even for new installation
+- in Paint Tool fixed the message to select a polygon when using the Selection: Single Polygon being overwritten by the "Grid disabled" message
+- more changes in strings throughout the app
+- made some minor changes in the GUI of the FlatCAM Tools
+- in Tools Database made sure that each new tool added has a unique name
+- in AppTool made some methods to be class methods
+- reverted the class methods in AppTool
+- added a button for Transformations Tool in the lower side (common) of the Object UI
+- some other UI changes
+- after using Isolation Tool it will switch automatically to the Geometry UI
+
+31.05.2020
+
+- structural changes in Preferences from David Robertson
+- made last filter selected for open file to be used next time when opening files (for Excellon, GCode and Gerber files, for now)
+
 30.05.2020
 30.05.2020
 
 
 - made confirmation messages for the values that are modified not to be printed in the Shell
 - made confirmation messages for the values that are modified not to be printed in the Shell

+ 0 - 3
Common.py

@@ -212,9 +212,6 @@ class ExclusionAreas(QtCore.QObject):
 
 
         self.shape_type_button = shape_button
         self.shape_type_button = shape_button
 
 
-        # TODO use the self.app.defaults when made general (not in Geo object Pref UI)
-        # self.shape_type_button.set_value('square')
-
         self.over_z_button = overz_button
         self.over_z_button = overz_button
         self.strategy_button = strategy_radio
         self.strategy_button = strategy_radio
         self.cnc_button = cnc_button
         self.cnc_button = cnc_button

+ 5 - 3
defaults.py

@@ -697,13 +697,15 @@ class FlatCAMDefaults:
     }
     }
 
 
     @classmethod
     @classmethod
-    def save_factory_defaults(cls, file_path: str):
+    def save_factory_defaults(cls, file_path: str, version: float):
         """Writes the factory defaults to a file at the given path, overwriting any existing file."""
         """Writes the factory defaults to a file at the given path, overwriting any existing file."""
         # Delete any existing factory defaults file
         # Delete any existing factory defaults file
         if os.path.isfile(file_path):
         if os.path.isfile(file_path):
             os.chmod(file_path, stat.S_IRWXO | stat.S_IWRITE | stat.S_IWGRP)
             os.chmod(file_path, stat.S_IRWXO | stat.S_IWRITE | stat.S_IWGRP)
             os.remove(file_path)
             os.remove(file_path)
 
 
+        cls.factory_defaults['version'] = version
+
         try:
         try:
             # recreate a new factory defaults file and save the factory defaults data into it
             # recreate a new factory defaults file and save the factory defaults data into it
             f_f_def_s = open(file_path, "w")
             f_f_def_s = open(file_path, "w")
@@ -784,8 +786,8 @@ class FlatCAMDefaults:
         if defaults is None:
         if defaults is None:
             return
             return
 
 
-        # Perform migration if necessary
-        if self.__is_old_defaults(defaults):
+        # Perform migration if necessary but only if the defaults dict is not empty
+        if self.__is_old_defaults(defaults) and defaults:
             self.old_defaults_found = True
             self.old_defaults_found = True
             defaults = self.__migrate_old_defaults(defaults=defaults)
             defaults = self.__migrate_old_defaults(defaults=defaults)
         else:
         else: