diff --git a/AppGUI/ColumnarFlowLayout.py b/AppGUI/ColumnarFlowLayout.py
new file mode 100644
index 00000000..3c92efd0
--- /dev/null
+++ b/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)
diff --git a/AppGUI/GUIElements.py b/AppGUI/GUIElements.py
index a7829719..5d6d4c07 100644
--- a/AppGUI/GUIElements.py
+++ b/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()
diff --git a/AppGUI/preferences/OptionUI.py b/AppGUI/preferences/OptionUI.py
new file mode 100644
index 00000000..d05691d5
--- /dev/null
+++ b/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 = "%s" % fmt
+ if self.label_color:
+ fmt = "%s" % (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('%s' % _(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
diff --git a/AppGUI/preferences/OptionsGroupUI.py b/AppGUI/preferences/OptionsGroupUI.py
index 98780004..079b84cc 100644
--- a/AppGUI/preferences/OptionsGroupUI.py
+++ b/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)
\ No newline at end of file
+ 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
diff --git a/AppGUI/preferences/PreferencesSectionUI.py b/AppGUI/preferences/PreferencesSectionUI.py
new file mode 100644
index 00000000..6daae820
--- /dev/null
+++ b/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
diff --git a/AppGUI/preferences/general/GeneralAppSettingsGroupUI.py b/AppGUI/preferences/general/GeneralAppSettingsGroupUI.py
new file mode 100644
index 00000000..7a7467d0
--- /dev/null
+++ b/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"
+ "<>: 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"]
diff --git a/AppObjects/FlatCAMCNCJob.py b/AppObjects/FlatCAMCNCJob.py
index f0fe0765..cad98460 100644
--- a/AppObjects/FlatCAMCNCJob.py
+++ b/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):
"""
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1dac8877..c30e4c7b 100644
--- a/CHANGELOG.md
+++ b/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