Просмотр исходного кода

Merged in preferences_changes (pull request #12)
Preferences changes

Marius Stanciu 5 лет назад
Родитель
Сommit
5624596828

+ 28 - 11
AppDatabase.py

@@ -119,7 +119,7 @@ class ToolsDB(QtWidgets.QWidget):
         )
         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(
             _("Add a new tool in the Tools Table of the\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."))
             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()
 
@@ -726,7 +726,7 @@ class ToolsDB(QtWidgets.QWidget):
                 self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
                 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.callback_on_edited()
 
@@ -1792,12 +1792,19 @@ class ToolsDB2(QtWidgets.QWidget):
         )
         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(
-            _("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.")
         )
+        self.add_tool_from_db.setStyleSheet("""
+                                            QPushButton
+                                            {
+                                                font-weight: bold;
+                                                color: green;
+                                            }
+                                            """)
         self.add_tool_from_db.hide()
 
         self.cancel_tool_from_db = FCButton(_("Cancel"))
@@ -1807,7 +1814,7 @@ class ToolsDB2(QtWidgets.QWidget):
         tree_layout.addLayout(hlay)
         hlay.addWidget(self.add_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)
 
     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
         try:
@@ -2034,7 +2041,7 @@ class ToolsDB2(QtWidgets.QWidget):
             self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
             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()
 
@@ -2145,8 +2152,18 @@ class ToolsDB2(QtWidgets.QWidget):
             "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['name'] = 'new_tool'
+        dict_elem['name'] = new_name
         if type(self.app.defaults["geometry_cnctooldia"]) == float:
             dict_elem['tooldia'] = self.app.defaults["geometry_cnctooldia"]
         else:
@@ -2323,7 +2340,7 @@ class ToolsDB2(QtWidgets.QWidget):
                 self.app.inform.emit('[ERROR] %s' % _("Failed to parse Tools DB file."))
                 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.update_storage()
 

+ 1 - 1
AppEditors/FlatCAMGrbEditor.py

@@ -3435,7 +3435,7 @@ class FlatCAMGrbEditor(QtCore.QObject):
             else:
                 # deleted_tool_dia = float(self.apertures_table.item(self.apertures_table.currentRow(), 1).text())
                 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
 
                 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)
 
 
+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):
 
     returnPressed = QtCore.pyqtSignal()

+ 1 - 1
AppGUI/MainGUI.py

@@ -1124,7 +1124,7 @@ class MainGUI(QtWidgets.QMainWindow):
         # #######################################################################
         # ####################### 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.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas)
         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).
     """
 
-    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):
         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.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()
     
     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.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):
@@ -16,4 +42,36 @@ class OptionsGroupUI(QtWidgets.QGroupBox):
         """)
 
         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
         self.theme_label = QtWidgets.QLabel('%s:' % _('Theme'))
         self.theme_label.setToolTip(
-            _("Select a theme for FlatCAM.\n"
+            _("Select a theme for the application.\n"
               "It will theme the plot area.")
         )
 
@@ -72,7 +72,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Layout selection
         self.layout_label = QtWidgets.QLabel('%s:' % _('Layout'))
         self.layout_label.setToolTip(
-            _("Select an layout for FlatCAM.\n"
+            _("Select an layout for the application.\n"
               "It is applied immediately.")
         )
         self.layout_combo = FCComboBox()
@@ -94,7 +94,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Style selection
         self.style_label = QtWidgets.QLabel('%s:' % _('Style'))
         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.")
         )
         self.style_combo = FCComboBox()
@@ -110,7 +110,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Enable High DPI Support
         self.hdpi_cb = FCCheckBox('%s' % _('Activate HDPI Support'))
         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.")
         )
 
@@ -126,7 +126,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Enable Hover box
         self.hover_cb = FCCheckBox('%s' % _('Display Hover Shape'))
         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"
               "over any kind of not-selected object.")
         )
@@ -135,7 +135,7 @@ class GeneralGUIPrefGroupUI(OptionsGroupUI):
         # Enable Selection box
         self.selection_cb = FCCheckBox('%s' % _('Display Selection Shape'))
         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"
               "either by clicking or dragging mouse from left to right or\n"
               "right to left.")

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

@@ -30,8 +30,7 @@ class ToolsFilmPrefGroupUI(OptionsGroupUI):
         # ## Parameters
         self.film_label = QtWidgets.QLabel("<b>%s:</b>" % _("Parameters"))
         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.")
         )
         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.setToolTip(
             _("Depth of cut into material. Negative value.\n"
-              "In FlatCAM units.")
+              "In application units.")
         )
         self.cutz_entry = FCDoubleSpinner()
         self.cutz_entry.set_precision(self.decimals)
@@ -114,7 +114,7 @@ class ToolsPaintPrefGroupUI(OptionsGroupUI):
 
         self.cutz_entry.setToolTip(
             _("Depth of cut into material. Negative value.\n"
-              "In FlatCAM units.")
+              "In application units.")
         )
         grid0.addWidget(cutzlabel, 4, 0)
         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.setToolTip(
             _("Various transformations that can be applied\n"
-              "on a FlatCAM object.")
+              "on a application object.")
         )
         self.layout.addWidget(self.transform_label)
 

+ 4 - 4
AppObjects/FlatCAMCNCJob.py

@@ -504,17 +504,17 @@ class CNCJobObject(FlatCAMObj, CNCjob):
         try:
             dir_file_to_save = self.app.get_last_save_folder() + '/' + str(name)
             filename, _f = FCFileSaveDialog.get_saved_filename(
-                caption=_("Export Machine Code ..."),
+                caption=_("Export Code ..."),
                 directory=dir_file_to_save,
                 ext_filter=_filter_
             )
         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)
 
         if filename == '':
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export Machine Code cancelled ..."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
             return
         else:
             if save_gcode is True:
@@ -535,7 +535,7 @@ class CNCJobObject(FlatCAMObj, CNCjob):
         if self.app.defaults["global_open_style"] is False:
             self.app.file_opened.emit("gcode", filename)
         self.app.file_saved.emit("gcode", filename)
-        self.app.inform.emit('[success] %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):
         """

+ 6 - 1
AppObjects/FlatCAMObj.py

@@ -139,7 +139,7 @@ class FlatCAMObj(QtCore.QObject):
                 except KeyError:
                     log.debug("FlatCAMObj.from_dict() --> KeyError: %s. "
                               "Means that we are loading an old project that don't"
-                              "have all attributes in the latest FlatCAM." % str(attr))
+                              "have all attributes in the latest application version." % str(attr))
                     pass
 
     def on_options_change(self, key):
@@ -182,6 +182,11 @@ class FlatCAMObj(QtCore.QObject):
         except (TypeError, AttributeError):
             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)
 
     def build_ui(self):

+ 8 - 8
AppTools/ToolAlignObjects.py

@@ -308,13 +308,6 @@ class AlignObjects(AppTool):
         else:
             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.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_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.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):
         if self.app.is_legacy is False:
             event_pos = event.pos

+ 1 - 1
AppTools/ToolCalibration.py

@@ -64,7 +64,7 @@ class ToolCalibration(AppTool):
         grid_lay.setColumnStretch(1, 1)
         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(
             _("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.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_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_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.app.ui.rel_position_label.setText(
@@ -582,13 +583,14 @@ class Distance(AppTool):
             if len(self.points) == 1:
                 self.utility_geometry(pos=pos)
                 # 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:
             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_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.setToolTip('%s.' % _("Conversion utilities"))
@@ -127,7 +130,10 @@ class ToolEtchCompensation(AppTool):
         hlay_2.addWidget(self.mils_to_um_entry)
         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.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.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
 
-        self.layout.addWidget(self.points_table)
-        self.layout.addWidget(QtWidgets.QLabel(''))
-
         self.points_table.setColumnCount(3)
         self.points_table.setHorizontalHeaderLabels(
             [
@@ -76,7 +73,6 @@ class ToolFiducials(AppTool):
         )
         self.points_table.setRowCount(3)
         row = 0
-
         flags = QtCore.Qt.ItemIsEnabled
 
         # BOTTOM LEFT
@@ -140,6 +136,13 @@ class ToolFiducials(AppTool):
         for row in range(self.points_table.rowCount()):
             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_lay = QtWidgets.QGridLayout()
         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_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.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 AppGUI.GUIElements import FCCheckBox, FCDoubleSpinner, RadioSet, FCTable, FCInputDialog, FCButton, \
-    FCComboBox, OptionalHideInputSection, FCSpinner
+    FCComboBox, OptionalInputSection, FCSpinner
 from AppParsers.ParseGerber import Gerber
 
 from copy import deepcopy
@@ -484,11 +484,11 @@ class ToolIsolation(AppTool, Gerber):
 
         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
         self.select_label = QtWidgets.QLabel('%s:' % _("Selection"))
@@ -1691,6 +1691,7 @@ class ToolIsolation(AppTool, Gerber):
             else:
                 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.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
 
@@ -1703,7 +1704,6 @@ class ToolIsolation(AppTool, Gerber):
             # disconnect flags
             self.poly_sel_disconnect_flag = True
 
-            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click on a polygon to isolate it."))
         elif selection == _("Reference Object"):
             ref_obj = self.app.collection.get_by_name(self.reference_combo.get_value())
             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)
 
+        # 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):
         """
 

+ 7 - 1
AppTools/ToolOptimal.py

@@ -72,7 +72,13 @@ class ToolOptimal(AppTool):
         self.gerber_object_label.setToolTip(
             "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
         self.precision_label = QtWidgets.QLabel('%s:' % _("Precision"))

+ 2 - 2
AppTools/ToolPaint.py

@@ -1429,8 +1429,6 @@ class ToolPaint(AppTool, Gerber):
                                 outname=self.o_name)
 
         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
             if self.app.ui.grid_snap_btn.isChecked():
                 self.grid_status_memory = True
@@ -1438,6 +1436,8 @@ class ToolPaint(AppTool, Gerber):
             else:
                 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.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
 
         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')
 
         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.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(
             _("Gerber Object to which the QRCode will be added.")
         )
 
         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_lay = QtWidgets.QGridLayout()
@@ -90,7 +111,7 @@ class QRCode(AppTool):
         grid_lay.setColumnStretch(0, 0)
         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(
             _("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_entry, 4, 1)
 
-        # Text box
-        self.text_label = QtWidgets.QLabel('%s:' % _("QRCode Data"))
-        self.text_label.setToolTip(
-            _("QRCode Data. Alphanumeric text to be encoded in the QRCode.")
-        )
-        self.text_data = FCTextArea()
-        self.text_data.setPlaceholderText(
-            _("Add here the text to be included in the QRCode...")
-        )
-        grid_lay.addWidget(self.text_label, 5, 0)
-        grid_lay.addWidget(self.text_data, 6, 0, 1, 2)
-
         # POLARITY CHOICE #
         self.pol_label = QtWidgets.QLabel('%s:' % _("Polarity"))
         self.pol_label.setToolTip(
@@ -788,7 +797,7 @@ class QRCode(AppTool):
         filename = str(filename)
 
         if filename == "":
-            self.app.inform.emit('[WARNING_NOTCL]%s' % _("Cancelled."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
             return
         else:
             self.app.worker_task.emit({'fcn': job_thread_qr_png, 'params': [self.app, filename]})
@@ -835,7 +844,7 @@ class QRCode(AppTool):
         filename = str(filename)
 
         if filename == "":
-            self.app.inform.emit('[WARNING_NOTCL]%s' % _("Cancelled."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
             return
         else:
             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.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 ## ##
         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.")
         )
 
-        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)
-        # grid2.addWidget(self.copytool_btn, 0, 1)
         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
         grid0_1 = QtWidgets.QGridLayout()
@@ -373,6 +369,18 @@ class SolderPaste(AppTool):
             _("Second step is to create a solder paste dispensing\n"
               "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(self.soldergeo_btn, 0, 2)
 
@@ -1497,11 +1505,10 @@ class SolderPaste(AppTool):
             )
         except TypeError:
             filename, _f = FCFileSaveDialog.get_saved_filename(
-                caption=_("Export Machine Code ..."), ext_filter=_filter_)
+                caption=_("Export Code ..."), ext_filter=_filter_)
 
         if filename == '':
-            self.app.inform.emit('[WARNING_NOTCL] %s' %
-                                 _("Export Machine Code cancelled ..."))
+            self.app.inform.emit('[WARNING_NOTCL] %s' % _("Export cancelled ..."))
             return
 
         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()
 
         # 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
         try:
@@ -1488,6 +1488,11 @@ class App(QtCore.QObject):
         self.prj_list = ['flatprj']
         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
         # flag for polygons not cleared
         self.poly_not_cleared = False
@@ -4088,7 +4093,7 @@ class App(QtCore.QObject):
             val_x = float(self.defaults['global_gridx'])
             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()
 
@@ -5735,7 +5740,7 @@ class App(QtCore.QObject):
             name = obj.options["name"]
         except AttributeError:
             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"))
             return
 
@@ -6559,11 +6564,13 @@ class App(QtCore.QObject):
             try:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"),
                                                                        directory=self.get_last_folder(),
-                                                                       filter=_filter_)
+                                                                       filter=_filter_,
+                                                                       initialFilter=self.last_op_gerber_filter)
             except TypeError:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Gerber"), filter=_filter_)
 
             filenames = [str(filename) for filename in filenames]
+            self.last_op_gerber_filter = _f
         else:
             filenames = [name]
             self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
@@ -6597,10 +6604,12 @@ class App(QtCore.QObject):
             try:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"),
                                                                        directory=self.get_last_folder(),
-                                                                       filter=_filter_)
+                                                                       filter=_filter_,
+                                                                       initialFilter=self.last_op_excellon_filter)
             except TypeError:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open Excellon"), filter=_filter_)
             filenames = [str(filename) for filename in filenames]
+            self.last_op_excellon_filter = _f
         else:
             filenames = [str(name)]
             self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
@@ -6610,7 +6619,7 @@ class App(QtCore.QObject):
                                     color=QtGui.QColor("gray"))
 
         if len(filenames) == 0:
-            self.inform.emit('[WARNING_NOTCL]%s' % _("Cancelled."))
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
         else:
             for filename in filenames:
                 if filename != '':
@@ -6638,11 +6647,13 @@ class App(QtCore.QObject):
             try:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"),
                                                                        directory=self.get_last_folder(),
-                                                                       filter=_filter_)
+                                                                       filter=_filter_,
+                                                                       initialFilter=self.last_op_gcode_filter)
             except TypeError:
                 filenames, _f = QtWidgets.QFileDialog.getOpenFileNames(caption=_("Open G-Code"), filter=_filter_)
 
             filenames = [str(filename) for filename in filenames]
+            self.last_op_gcode_filter = _f
         else:
             filenames = [name]
             self.splash.showMessage('%s: %ssec\n%s' % (_("Canvas initialization started.\n"
@@ -6803,7 +6814,7 @@ class App(QtCore.QObject):
         filename = str(filename)
 
         if filename == "":
-            self.inform.emit('[WARNING_NOTCL]%s' % _("Cancelled."))
+            self.inform.emit('[WARNING_NOTCL] %s' % _("Cancelled."))
             return
         else:
             self.export_svg(name, filename)
@@ -6821,7 +6832,7 @@ class App(QtCore.QObject):
 
         data = None
         if self.is_legacy is False:
-            image = _screenshot(alpha=None)
+            image = _screenshot(alpha=False)
             data = np.asarray(image)
             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'))
@@ -8895,7 +8906,7 @@ class App(QtCore.QObject):
         """
         self.log.debug("Plot_all()")
         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():
             def worker_task(obj):

+ 3 - 3
Bookmark.py

@@ -287,8 +287,8 @@ class BookmarkManager(QtWidgets.QWidget):
         date = date.replace(' ', '_')
 
         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()),
                                                                 n=_("Bookmarks"),
                                                                 date=date),
@@ -334,7 +334,7 @@ class BookmarkManager(QtWidgets.QWidget):
         self.app.log.debug("on_import_bookmarks()")
 
         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)
 

+ 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
 
 - 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
 
-        # 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.strategy_button = strategy_radio
         self.cnc_button = cnc_button

+ 5 - 3
defaults.py

@@ -697,13 +697,15 @@ class FlatCAMDefaults:
     }
 
     @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."""
         # Delete any existing factory defaults file
         if os.path.isfile(file_path):
             os.chmod(file_path, stat.S_IRWXO | stat.S_IWRITE | stat.S_IWGRP)
             os.remove(file_path)
 
+        cls.factory_defaults['version'] = version
+
         try:
             # recreate a new factory defaults file and save the factory defaults data into it
             f_f_def_s = open(file_path, "w")
@@ -784,8 +786,8 @@ class FlatCAMDefaults:
         if defaults is None:
             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
             defaults = self.__migrate_old_defaults(defaults=defaults)
         else: