Jelajahi Sumber

- structural changes in Preferences from David Robertson

Marius Stanciu 5 tahun lalu
induk
melakukan
ede90d6775

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

+ 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

+ 60 - 2
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):
@@ -6,7 +32,7 @@ class OptionsGroupUI(QtWidgets.QGroupBox):
 
     def __init__(self, title, parent=None):
         # QtGui.QGroupBox.__init__(self, title, parent=parent)
-        super(OptionsGroupUI, self).__init__()
+        super(OptionsGroupUI, self).__init__(title=title, parent=parent)
         self.setStyleSheet("""
         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"]

+ 1 - 1
AppObjects/FlatCAMCNCJob.py

@@ -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):
         """

+ 4 - 0
CHANGELOG.md

@@ -7,6 +7,10 @@ CHANGELOG for FlatCAM beta
 
 =================================================
 
+31.05.2020
+
+- structural changes in Preferences from David Robertson
+
 30.05.2020
 
 - made confirmation messages for the values that are modified not to be printed in the Shell