From 99510be43b9f3cacb4439877b90fcdc0a9ea750a Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Wed, 4 Feb 2026 16:15:41 +0000 Subject: [PATCH 1/2] Custom decimal for nsTolerance --- rascal2/widgets/controls.py | 3 +++ rascal2/widgets/inputs.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rascal2/widgets/controls.py b/rascal2/widgets/controls.py index 7fb4ffef..5affe3b6 100644 --- a/rascal2/widgets/controls.py +++ b/rascal2/widgets/controls.py @@ -191,6 +191,7 @@ def __init__(self, parent, settings, presenter): self.rows = {} self.datasetter = {} self.val_labels = {} + adjusted_decimals = {"nsTolerance": 3} settings_grid = QtWidgets.QGridLayout() settings_grid.setContentsMargins(10, 10, 10, 10) @@ -198,6 +199,8 @@ def __init__(self, parent, settings, presenter): for i, setting in enumerate(settings): field_info = controls_fields[setting] self.rows[setting] = get_validated_input(field_info) + if setting in adjusted_decimals: + self.rows[setting].editor.setDecimals(adjusted_decimals[setting]) self.rows[setting].layout().setContentsMargins(5, 0, 0, 0) self.datasetter[setting] = self.create_model_data_setter(setting) self.rows[setting].edited_signal.connect(self.datasetter[setting]) diff --git a/rascal2/widgets/inputs.py b/rascal2/widgets/inputs.py index f5aeeaa6..092e887a 100644 --- a/rascal2/widgets/inputs.py +++ b/rascal2/widgets/inputs.py @@ -224,8 +224,6 @@ def create_editor(self, field_info: FieldInfo) -> QtWidgets.QWidget: class AdaptiveDoubleSpinBox(QtWidgets.QDoubleSpinBox): """A double spinbox which adapts to given numbers of decimals.""" - MIN_DECIMALS = 2 - def __init__(self, parent=None): super().__init__(parent) From 6930b2d518a573268ef5df1765fa1de8801e32c2 Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Fri, 6 Feb 2026 15:23:45 +0000 Subject: [PATCH 2/2] Add custom tile layout and adjust plot padding --- pyproject.toml | 7 ++ rascal2/ui/view.py | 24 +++++- rascal2/widgets/controls.py | 4 +- rascal2/widgets/plot.py | 6 +- rascal2/widgets/project/project.py | 108 ++++++++++++-------------- rascal2/widgets/utils.py | 90 +++++++++++++++++++++ tests/widgets/project/test_project.py | 6 -- 7 files changed, 173 insertions(+), 72 deletions(-) create mode 100644 rascal2/widgets/utils.py diff --git a/pyproject.toml b/pyproject.toml index 3baa7120..4a2b7ffa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,10 +51,15 @@ extend-ignore-names = ['allKeys', 'closeEvent', 'columnCount', 'createEditor', + 'expandingDirections', 'eventFilter', + 'hasHeightForWidth', 'headerData', + 'heightForWidth', 'isClean', + 'itemAt', 'mergeWith', + 'minimumSize', 'mouseDoubleClickEvent', 'paintEvent', 'resizeEvent', @@ -62,6 +67,7 @@ extend-ignore-names = ['allKeys', 'setClean', 'setData', 'setEditorData', + 'setGeometry', 'setModel', 'setModelData', 'setText', @@ -70,6 +76,7 @@ extend-ignore-names = ['allKeys', 'sizeHint', 'stepBy', 'supportedDropActions', + 'takeAt', 'textFromValue', 'timerEvent', 'valueFromText',] diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 2a02cb0f..d08e6b4f 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -47,7 +47,7 @@ def __init__(self): self.create_toolbar() self.create_status_bar() - self.setMinimumSize(1024, 800) + self.setMinimumSize(1360, 800) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) self.settings = Settings() @@ -182,7 +182,7 @@ def create_actions(self): self.tile_windows_action = QtGui.QAction("Tile Windows", self) self.tile_windows_action.setStatusTip("Arrange windows in the default grid.") self.tile_windows_action.setIcon(QtGui.QIcon(path_for("tile.png"))) - self.tile_windows_action.triggered.connect(self.mdi.tileSubWindows) + self.tile_windows_action.triggered.connect(self.custom_tile_layout) self.tile_windows_action.setEnabled(False) self.disabled_elements.append(self.tile_windows_action) @@ -338,7 +338,7 @@ def reset_mdi_layout(self): if mdi_defaults is None: for window in self.mdi.subWindowList(): window.showNormal() - self.mdi.tileSubWindows() + self.custom_tile_layout() else: for window in self.mdi.subWindowList(): # get corresponding MDIGeometries entry for the widget @@ -364,6 +364,24 @@ def save_mdi_layout(self): global_setting.setValue("window_geometry", self.saveGeometry()) global_setting.sync() + def custom_tile_layout(self): + """Tile the MDI windows using user recommended sizes.""" + # The percentages are estimated from provided screenshot in + # https://github.com/RascalSoftware/RasCAL-2/issues/188 + rect = self.centralWidget().contentsRect() + plot_width = round(0.6 * rect.width()) + plot_height = round(0.65 * rect.height()) + project_width = rect.width() - plot_width + project_height = round(0.5 * rect.height()) + + plot_geom = (0, 0, plot_width, plot_height) + project_geom = (plot_width, 0, project_width, project_height) + terminal_geom = (0, plot_height, plot_width, rect.height() - plot_height) + controls_geom = (plot_width, project_height, project_width, rect.height() - project_height) + geoms = [controls_geom, terminal_geom, project_geom, plot_geom] # windows in reverse order of creation + for geom, windows in zip(geoms, self.mdi.subWindowList(), strict=False): + windows.setGeometry(*geom) + def enable_elements(self): """Enable the elements that are disabled on startup.""" for element in self.disabled_elements: diff --git a/rascal2/widgets/controls.py b/rascal2/widgets/controls.py index 5affe3b6..080e162a 100644 --- a/rascal2/widgets/controls.py +++ b/rascal2/widgets/controls.py @@ -76,9 +76,9 @@ def __init__(self, parent): widget_layout = QtWidgets.QHBoxLayout() widget_layout.addStretch(1) - widget_layout.addLayout(procedure_box, 4) + widget_layout.addLayout(procedure_box, 8) widget_layout.addSpacing(20) - widget_layout.addWidget(self.fit_settings, 4) + widget_layout.addWidget(self.fit_settings, 12) widget_layout.addStretch(1) self.setLayout(widget_layout) diff --git a/rascal2/widgets/plot.py b/rascal2/widgets/plot.py index ff9ba413..241c480a 100644 --- a/rascal2/widgets/plot.py +++ b/rascal2/widgets/plot.py @@ -215,7 +215,7 @@ def __init__(self, parent): sub_layout.addWidget(slider) sub_layout.addStretch(1) plot_toolbar.addLayout(sub_layout) - plot_toolbar.addSpacing(15) + plot_toolbar.addSpacing(5) sidebar = QtWidgets.QHBoxLayout() sidebar.addWidget(self.plot_controls) @@ -237,6 +237,7 @@ def __init__(self, parent): scroll_area.setWidgetResizable(True) central_layout = QtWidgets.QVBoxLayout() + central_layout.setSpacing(0) central_layout.setContentsMargins(0, 0, 0, 0) self.interaction_layout = self.make_interaction_layout() if self.interaction_layout is not None: @@ -399,6 +400,8 @@ def make_figure(self) -> matplotlib.figure.Figure: self.resize_timer = 0 figure = matplotlib.figure.Figure() figure.subplots(1, 2) + figure.set_tight_layout(True) + figure.set_tight_layout({"pad": 0, "w_pad": 0.5}) return figure @@ -465,7 +468,6 @@ def plot_event(self, data: ratapi.events.PlotEventData | None = None): show_legend = self.show_legend.isChecked() if self.current_plot_data.contrastNames else False self.figure.clear() self.update_figure_size() - self.figure.tight_layout() ratapi.plotting.plot_ref_sld_helper( self.current_plot_data, self.figure, diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index 038f4478..7316d5ea 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -21,6 +21,7 @@ ProjectFieldWidget, ResolutionsFieldWidget, ) +from rascal2.widgets.utils import FlowLayout class ProjectWidget(QtWidgets.QWidget): @@ -77,16 +78,43 @@ def __init__(self, parent): layout.addWidget(self.stacked_widget) self.setLayout(layout) + @staticmethod + def make_labelled_widget(label_text, form_widget): + """Create widget containing a label and the given widget. + + Parameters + ---------- + label_text: str + The label text for the form widget. + form_widget + The widget to add. + + Returns + ------- + label_form_widget: + A widget with label and the given widget. + """ + layout = QtWidgets.QHBoxLayout() + layout.setSpacing(0) + layout.setContentsMargins(2, 5, 2, 5) + layout.addWidget(QtWidgets.QLabel(f"{label_text}: ", objectName="BoldLabel")) + layout.addWidget(form_widget) + + widget = QtWidgets.QWidget() + widget.setLayout(layout) + widget.setSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) + return widget + def create_project_view(self) -> QtWidgets.QWidget: """Create the project (non-edit) view.""" project_widget = QtWidgets.QWidget() main_layout = QtWidgets.QVBoxLayout() main_layout.setSpacing(20) - show_sliders_button = QtWidgets.QPushButton("Show sliders", self) + show_sliders_button = QtWidgets.QPushButton("Show sliders") show_sliders_button.clicked.connect(self.parent.toggle_sliders) - self.edit_project_button = QtWidgets.QPushButton("Edit Project", self, icon=QtGui.QIcon(path_for("edit.png"))) + self.edit_project_button = QtWidgets.QPushButton("Edit Project", icon=QtGui.QIcon(path_for("edit.png"))) self.edit_project_button.clicked.connect(self.show_edit_view) button_layout = QtWidgets.QHBoxLayout() button_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) @@ -95,42 +123,27 @@ def create_project_view(self) -> QtWidgets.QWidget: main_layout.addLayout(button_layout) - settings_layout = QtWidgets.QHBoxLayout() - settings_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) + settings_layout = FlowLayout(spacing=2) + settings_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) - absorption_label = QtWidgets.QLabel("Absorption:", self, objectName="BoldLabel") self.absorption_checkbox = QtWidgets.QCheckBox() self.absorption_checkbox.setDisabled(True) + settings_layout.addWidget(self.make_labelled_widget("Absorption", self.absorption_checkbox)) - settings_layout.addWidget(absorption_label) - settings_layout.addWidget(self.absorption_checkbox) - - self.calculation_label = QtWidgets.QLabel("Calculation:", self, objectName="BoldLabel") - - self.calculation_type = QtWidgets.QLineEdit(self) + self.calculation_type = QtWidgets.QLineEdit() self.calculation_type.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.calculation_type.setReadOnly(True) + settings_layout.addWidget(self.make_labelled_widget("Calculation", self.calculation_type)) - settings_layout.addWidget(self.calculation_label) - settings_layout.addWidget(self.calculation_type) - - self.model_type_label = QtWidgets.QLabel("Model Type:", self, objectName="BoldLabel") - - self.model_type = QtWidgets.QLineEdit(self) + self.model_type = QtWidgets.QLineEdit() self.model_type.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.model_type.setReadOnly(True) + settings_layout.addWidget(self.make_labelled_widget("Model Type", self.model_type)) - settings_layout.addWidget(self.model_type_label) - settings_layout.addWidget(self.model_type) - - self.geometry_label = QtWidgets.QLabel("Geometry:", self, objectName="BoldLabel") - - self.geometry_type = QtWidgets.QLineEdit(self) + self.geometry_type = QtWidgets.QLineEdit() self.geometry_type.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.geometry_type.setReadOnly(True) - - settings_layout.addWidget(self.geometry_label) - settings_layout.addWidget(self.geometry_type) + settings_layout.addWidget(self.make_labelled_widget("Geometry", self.geometry_type)) main_layout.addLayout(settings_layout) @@ -152,11 +165,11 @@ def create_edit_view(self) -> QtWidgets.QWidget: main_layout.setSpacing(20) self.save_project_button = QtWidgets.QPushButton( - "Accept Changes", self, icon=QtGui.QIcon(path_for("save-project.png")) + "Accept Changes", icon=QtGui.QIcon(path_for("save-project.png")) ) self.save_project_button.clicked.connect(self.save_changes) - self.cancel_button = QtWidgets.QPushButton("Cancel", self, icon=QtGui.QIcon(path_for("cancel-dark.png"))) + self.cancel_button = QtWidgets.QPushButton("Cancel", icon=QtGui.QIcon(path_for("cancel-dark.png"))) self.cancel_button.clicked.connect(self.show_project_view) buttons_layout = QtWidgets.QHBoxLayout() @@ -165,47 +178,24 @@ def create_edit_view(self) -> QtWidgets.QWidget: buttons_layout.addWidget(self.cancel_button) main_layout.addLayout(buttons_layout) - settings_layout = QtWidgets.QHBoxLayout() + settings_layout = FlowLayout(spacing=2) settings_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignHCenter) - absorption_label = QtWidgets.QLabel("Absorption:", self, objectName="BoldLabel") self.edit_absorption_checkbox = QtWidgets.QCheckBox() + settings_layout.addWidget(self.make_labelled_widget("Absorption", self.edit_absorption_checkbox)) - settings_layout.addWidget(absorption_label) - settings_layout.addWidget(self.edit_absorption_checkbox) - - self.edit_calculation_label = QtWidgets.QLabel("Calculation:", self, objectName="BoldLabel") - - self.calculation_combobox = QtWidgets.QComboBox(self) - self.calculation_combobox.setSizePolicy( - QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed - ) + self.calculation_combobox = QtWidgets.QComboBox() self.calculation_combobox.addItems([calc for calc in Calculations]) + settings_layout.addWidget(self.make_labelled_widget("Calculation", self.calculation_combobox)) - settings_layout.addWidget(self.edit_calculation_label) - settings_layout.addWidget(self.calculation_combobox) - - self.edit_model_type_label = QtWidgets.QLabel("Model Type:", self, objectName="BoldLabel") - - self.model_combobox = QtWidgets.QComboBox(self) - self.model_combobox.setSizePolicy( - QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed - ) + self.model_combobox = QtWidgets.QComboBox() self.model_combobox.addItems([model for model in LayerModels]) + settings_layout.addWidget(self.make_labelled_widget("Model Type", self.model_combobox)) - settings_layout.addWidget(self.edit_model_type_label) - settings_layout.addWidget(self.model_combobox) - - self.edit_geometry_label = QtWidgets.QLabel("Geometry:", self, objectName="BoldLabel") - - self.geometry_combobox = QtWidgets.QComboBox(self) - self.geometry_combobox.setSizePolicy( - QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed - ) + self.geometry_combobox = QtWidgets.QComboBox() self.geometry_combobox.addItems([geo for geo in Geometries]) + settings_layout.addWidget(self.make_labelled_widget("Geometry", self.geometry_combobox)) - settings_layout.addWidget(self.edit_geometry_label) - settings_layout.addWidget(self.geometry_combobox) main_layout.addLayout(settings_layout) self.edit_absorption_checkbox.checkStateChanged.connect( diff --git a/rascal2/widgets/utils.py b/rascal2/widgets/utils.py new file mode 100644 index 00000000..1b5c73be --- /dev/null +++ b/rascal2/widgets/utils.py @@ -0,0 +1,90 @@ +from PyQt6 import QtCore, QtWidgets + + +class FlowLayout(QtWidgets.QLayout): + """A layout that rearranges contents when resized. + + Adapted from PyQt port of QFlowLayout example https://doc.qt.io/qt-6/qtwidgets-layouts-flowlayout-example.html + """ + + def __init__(self, parent=None, margin=0, spacing=5): + super().__init__(parent) + + if parent is not None: + self.setContentsMargins(margin, margin, margin, margin) + + self.setSpacing(spacing) + self.margin = margin + + self.item_list = [] + + def __del__(self): + item = self.takeAt(0) + while item: + item = self.takeAt(0) + + def addItem(self, item): + self.item_list.append(item) + + def count(self): + return len(self.item_list) + + def itemAt(self, index): + if 0 <= index < len(self.item_list): + return self.item_list[index] + + return None + + def takeAt(self, index): + if 0 <= index < len(self.item_list): + return self.item_list.pop(index) + + return None + + def expandingDirections(self): + return QtCore.Qt.Orientation.Horizontal + + def hasHeightForWidth(self): + return True + + def heightForWidth(self, width): + height = self.do_layout(QtCore.QRect(0, 0, width, 0), True) + return height + + def setGeometry(self, rect): + super().setGeometry(rect) + self.do_layout(rect, False) + + def sizeHint(self): + return self.minimumSize() + + def minimumSize(self): + size = QtCore.QSize() + + for item in self.item_list: + size = size.expandedTo(item.minimumSize()) + + size += QtCore.QSize(2 * self.margin, 2 * self.margin) + return size + + def do_layout(self, rect, test_only): + x = rect.x() + y = rect.y() + space_x = space_y = self.spacing() + line_height = 0 + + for item in self.item_list: + next_x = x + item.sizeHint().width() + space_x + if next_x - space_x > rect.right() and line_height > 0: + x = rect.x() + y = y + line_height + space_y + next_x = x + item.sizeHint().width() + space_x + line_height = 0 + + if not test_only: + item.setGeometry(QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint())) + + x = next_x + line_height = max(line_height, item.sizeHint().height()) + + return y + line_height - rect.y() diff --git a/tests/widgets/project/test_project.py b/tests/widgets/project/test_project.py index 645b2bbb..493e4b43 100644 --- a/tests/widgets/project/test_project.py +++ b/tests/widgets/project/test_project.py @@ -110,15 +110,12 @@ def test_project_widget_initial_state(setup_project_widget): assert project_widget.edit_project_button.isEnabled() assert project_widget.edit_project_button.text() == "Edit Project" - assert project_widget.calculation_label.text() == "Calculation:" assert project_widget.calculation_type.text() == Calculations.Normal assert project_widget.calculation_type.isReadOnly() - assert project_widget.model_type_label.text() == "Model Type:" assert project_widget.model_type.text() == LayerModels.StandardLayers assert project_widget.model_type.isReadOnly() - assert project_widget.geometry_label.text() == "Geometry:" assert project_widget.geometry_type.text() == Geometries.AirSubstrate assert project_widget.geometry_type.isReadOnly() @@ -129,17 +126,14 @@ def test_project_widget_initial_state(setup_project_widget): assert project_widget.cancel_button.isEnabled() assert project_widget.cancel_button.text() == "Cancel" - assert project_widget.edit_calculation_label.text() == "Calculation:" assert project_widget.calculation_combobox.currentText() == Calculations.Normal for ix, calc in enumerate(Calculations): assert project_widget.calculation_combobox.itemText(ix) == calc - assert project_widget.edit_model_type_label.text() == "Model Type:" assert project_widget.model_combobox.currentText() == LayerModels.StandardLayers for ix, model in enumerate(LayerModels): assert project_widget.model_combobox.itemText(ix) == model - assert project_widget.edit_geometry_label.text() == "Geometry:" assert project_widget.geometry_combobox.currentText() == Geometries.AirSubstrate for ix, geometry in enumerate(Geometries): assert project_widget.geometry_combobox.itemText(ix) == geometry