Skip to content

Commit 086b299

Browse files
committed
feat: limit the vertical stack of widgets with collapsible sections
Add a SectionStack widget that keeps at most two sections open, saves the state, and shows a summary in the title of a collapsed section. Use it in steps 1 to 3 and in the feature details panel. Each page has one scroll area, at page level. The stratigraphic layers are a section of the column page, the problems list of step 4 is one line, and the dock footer is one line.
1 parent 05a7407 commit 086b299

15 files changed

Lines changed: 658 additions & 113 deletions

File tree

‎docs/development/usability-plan.md‎

Lines changed: 266 additions & 6 deletions
Large diffs are not rendered by default.

‎loopstructural/gui/modelling/base_tab.py‎

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,16 @@
11
from qgis.gui import QgsCollapsibleGroupBox
2-
from qgis.PyQt.QtCore import Qt
3-
from qgis.PyQt.QtWidgets import QScrollArea, QSizePolicy, QVBoxLayout, QWidget
2+
from qgis.PyQt.QtWidgets import QVBoxLayout, QWidget
43

54

65
class BaseTab(QWidget):
7-
def __init__(self, parent=None, data_manager=None, scrollable=False):
6+
"""A tab with a vertical layout. The page that holds the tab has the scroll area."""
7+
8+
def __init__(self, parent=None, data_manager=None):
89
super().__init__(parent)
910
self.data_manager = data_manager
10-
# Initialize a default layout for all tabs
11-
if scrollable:
12-
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
13-
self.scroll_area = QScrollArea(self)
14-
self.scroll_area.setWidgetResizable(True)
15-
self.scroll_area.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
16-
# Create a container widget for the scroll area
17-
self.container_widget = QWidget()
18-
self.scroll_area.setWidget(self.container_widget)
19-
# Ensure the scroll area and its container widget can handle focus and mouse events
20-
self.scroll_area.setFocusPolicy(Qt.FocusPolicy.NoFocus)
21-
self.scroll_area.setFrameShape(
22-
QScrollArea.Shape.NoFrame
23-
) # Remove any unnecessary frame
24-
25-
# Explicitly set size policies to ensure proper interaction
26-
self.scroll_area.setSizePolicy(
27-
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
28-
)
29-
# Set up a layout for the container widget
30-
self.container_layout = QVBoxLayout(self.container_widget)
31-
# Set the main layout for the BaseTab
32-
self.main_layout = QVBoxLayout(self)
33-
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, False)
34-
self.main_layout.addWidget(self.scroll_area)
35-
36-
self.container_widget.setSizePolicy(
37-
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred
38-
)
39-
40-
# Ensure the container widget propagates mouse events properly
41-
self.container_widget.setAttribute(
42-
Qt.WidgetAttribute.WA_TransparentForMouseEvents, False
43-
)
44-
45-
self.setLayout(self.main_layout)
46-
else:
47-
# If not scrollable, use a simple layout
48-
self.container_layout = QVBoxLayout(self)
49-
self.setLayout(self.container_layout)
50-
51-
# Set the layout for the tab
11+
self.container_layout = QVBoxLayout(self)
12+
self.container_layout.setContentsMargins(0, 0, 0, 0)
13+
self.setLayout(self.container_layout)
5214

5315
def add_widget(self, widget, name=None, group_box=True):
5416
"""Add a widget to the tab."""

‎loopstructural/gui/modelling/geological_history_tab.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
class GeologialHistoryTab(BaseTab):
66
def __init__(self, parent=None, data_manager=None):
7-
super().__init__(parent, data_manager, scrollable=False)
7+
super().__init__(parent, data_manager)
88
# Load the UI file for Tab 1
99
self.stratigraphic_column_widget = StratColumnWidget(self, data_manager=data_manager)
1010
# Add the loaded UI widget to the container layout

‎loopstructural/gui/modelling/geological_model_tab/feature_details_panel/_base.py‎

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
QLabel,
1212
QMessageBox,
1313
QPushButton,
14-
QScrollArea,
1514
QSpinBox,
1615
QVBoxLayout,
1716
QWidget,
@@ -23,6 +22,7 @@
2322

2423
from ....background_task import finish_background_task, start_background_task
2524
from ....messages import push_info, push_warning
25+
from ...steps.section_stack import SectionStack
2626
from ..bounding_box_widget import BoundingBoxWidget
2727
from ..layer_selection_table import LayerSelectionTable
2828

@@ -85,24 +85,13 @@ def __init__(self, parent=None, *, feature=None, model_manager=None, data_manage
8585
self.feature = feature
8686
self.model_manager = model_manager
8787
self.data_manager = data_manager
88-
# Create a scroll area for horizontal scrolling
89-
scroll = QScrollArea(self)
90-
scroll.setWidgetResizable(True)
91-
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
92-
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
93-
94-
# Create content widget to hold the form layout
95-
content = QWidget()
96-
self.layout = QVBoxLayout(content)
97-
# Set the content widget as the scroll area's widget
98-
scroll.setWidget(content)
99-
100-
# Add scroll area to main layout
88+
# The sections of the panel. At most two are open. The page that holds
89+
# the panel has the scroll area.
10190
mainLayout = QVBoxLayout(self)
102-
mainLayout.addWidget(scroll)
103-
104-
# Set the main layout
105-
self.setLayout(mainLayout)
91+
mainLayout.setContentsMargins(0, 0, 0, 0)
92+
self.layout = SectionStack(self, 'feature_details', data_manager)
93+
mainLayout.addWidget(self.layout)
94+
mainLayout.addStretch(1)
10695

10796
## define interpolator parameters
10897
# Regularisation spin box
@@ -176,9 +165,17 @@ def __init__(self, parent=None, *, feature=None, model_manager=None, data_manage
176165
form_layout.addRow('Orientation point weight', self.npw_spin_box)
177166
group_box = QgsCollapsibleGroupBox('Interpolator Settings')
178167
group_box.setLayout(form_layout)
179-
self.layout.addWidget(group_box)
180-
self.layout.addWidget(self._build_preview_widget())
181-
self.layout.addWidget(table_group_box)
168+
self.layout.add_section(
169+
group_box,
170+
'interpolator',
171+
'Interpolator Settings',
172+
summary=lambda: (
173+
f"{self.interpolator_type_combo.currentText()}, "
174+
f"{int(self.n_elements_spinbox.value())} elements"
175+
),
176+
)
177+
self.layout.add_section(self._build_preview_widget(), 'preview', 'Preview', collapsed=True)
178+
self.layout.add_section(table_group_box, 'data_layers', 'Data Layers')
182179
# this will call the addMidBlock and addExportBlock methods
183180
self.addMidBlock()
184181
self.addExportBlock()

‎loopstructural/gui/modelling/geological_model_tab/feature_details_panel/fault_panel.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,3 @@ def update_dip(value):
115115

116116
self.layout.addWidget(orientation_group)
117117
self.layout.addWidget(ellipsoid_group)
118-
self.setLayout(self.layout)

‎loopstructural/gui/modelling/geological_model_tab/feature_details_panel/folded_panel.py‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ def addMidBlock(self):
123123
group_box.setLayout(form_layout)
124124
self.layout.addWidget(group_box)
125125
# Remove redundant layout setting
126-
self.setLayout(self.layout)
127126

128127
def open_splot_dialog(self):
129128
dialog = SPlotDialog(

‎loopstructural/gui/modelling/geological_model_tab/feature_details_panel/foliation_panel.py‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,5 @@ def addMidBlock(self):
2929
group_box.setLayout(form_layout)
3030
self.layout.addWidget(group_box)
3131

32-
# Remove redundant layout setting
33-
self.setLayout(self.layout)
34-
3532
def on_fold_frame_changed(self, text):
3633
self.model_manager.add_fold_to_feature(self.feature.name, fold_frame_name=text)

‎loopstructural/gui/modelling/geological_model_tab/geological_model_tab.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from ....main.workflow_mode import DEFAULT_WORKFLOW_MODE, WORKFLOW_MODE_CONSTRAINTS
2424
from ...messages import push_info
2525
from ..steps import build_plan
26+
from ..steps.section_stack import page_scroll_area
2627
from .add_fault_dialog import AddFaultDialog
2728
from .add_foliation_dialog import AddFoliationDialog
2829
from .add_unconformity_dialog import AddUnconformityDialog
@@ -187,7 +188,7 @@ def __init__(self, parent=None, *, model_manager=None, data_manager=None):
187188
# The problems of all steps. They show before the build, so the user
188189
# sees why a build can fail or can give a poor model.
189190
self.problemsLabel = QLabel()
190-
self.problemsLabel.setWordWrap(True)
191+
self.problemsLabel.setWordWrap(False)
191192
self.problemsLabel.setTextFormat(Qt.TextFormat.RichText)
192193
self.problemsLabel.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed)
193194
mainLayout.insertWidget(1, self.problemsLabel, 0)
@@ -316,9 +317,11 @@ def refresh_primary_action(self, *args, **kwargs):
316317
problems = self._problems_provider() if self._problems_provider is not None else []
317318
if problems:
318319
lines = "".join(f"<li>{html.escape(message)}</li>" for _key, message in problems)
320+
count = len(problems)
319321
self.problemsLabel.setText(
320-
f"<b>Check before the build:</b><ul style='margin:0'>{lines}</ul>"
322+
f"<b>Check before the build:</b> {count} problem{'s' if count != 1 else ''}"
321323
)
324+
self.problemsLabel.setToolTip(f"<ul style='margin:0'>{lines}</ul>")
322325
self.problemsLabel.show()
323326
else:
324327
self.problemsLabel.hide()
@@ -756,7 +759,8 @@ def _set_details_panel(self, panel, feature_name, feature):
756759
# Dynamically replace the featureDetailsPanel widget
757760
splitter = self._splitter
758761
splitter.widget(1).deleteLater() # Remove the existing widget
759-
splitter.addWidget(panel) # Add the new widget
762+
# The page has one scroll area, here. The panel does not have its own.
763+
splitter.addWidget(page_scroll_area(panel))
760764

761765
def _on_model_update_started(self):
762766
"""Show a non-blocking indeterminate progress dialog for model updates.

‎loopstructural/gui/modelling/model_definition/model_definition_tab.py‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from qgis.PyQt.QtWidgets import QSizePolicy
22

33
from loopstructural.gui.modelling.base_tab import BaseTab
4+
from loopstructural.gui.modelling.steps.section_stack import SectionStack
45

56
from .bounding_box import BoundingBoxWidget
67
from .dem import DEMWidget
@@ -14,14 +15,34 @@ class ModelDefinitionTab(BaseTab):
1415
"""
1516

1617
def __init__(self, parent=None, data_manager=None):
17-
super().__init__(parent, data_manager, scrollable=True)
18-
# Add widgets to the QToolBox
18+
super().__init__(parent, data_manager)
1919
self.bounding_box = BoundingBoxWidget(self, data_manager)
2020
self.dem = DEMWidget(self, data_manager)
2121

2222
# Set uniform size policy for all widgets
2323
for widget in [self.bounding_box, self.dem]:
2424
widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
2525

26-
self.add_widget(self.bounding_box, 'Bounding Box')
27-
self.add_widget(self.dem, 'DEM')
26+
self.sections = SectionStack(self, 'data', data_manager)
27+
self.sections.add_section(
28+
self.bounding_box,
29+
'bounding_box',
30+
'Bounding Box',
31+
summary=self._bounding_box_summary,
32+
)
33+
self.sections.add_section(self.dem, 'dem', 'DEM', summary=self._dem_summary)
34+
self.container_layout.addWidget(self.sections)
35+
self.container_layout.addStretch(1)
36+
37+
def _bounding_box_summary(self):
38+
if self.data_manager is None or not self.data_manager.is_bounding_box_set():
39+
return "not set"
40+
return "set"
41+
42+
def _dem_summary(self):
43+
if self.data_manager is None:
44+
return ""
45+
layer = getattr(self.data_manager, 'dem_layer', None)
46+
if getattr(self.data_manager, 'use_dem', False) and layer is not None:
47+
return layer.name()
48+
return "flat elevation"

‎loopstructural/gui/modelling/modelling_widget.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
QHBoxLayout,
44
QLabel,
55
QPushButton,
6+
QSizePolicy,
67
QStackedWidget,
78
QVBoxLayout,
89
QWidget,
@@ -18,6 +19,7 @@
1819
StratigraphyStep,
1920
ViewStep,
2021
)
22+
from loopstructural.gui.modelling.steps.section_stack import SectionStack
2123
from loopstructural.gui.modelling.steps.step_bar import StepBar
2224

2325
# How often the checks of the steps run again, in milliseconds. Several
@@ -88,7 +90,10 @@ def __init__(
8890
self.stack.addWidget(page)
8991

9092
self.footer_label = QLabel(self)
91-
self.footer_label.setWordWrap(True)
93+
# One line. The tooltip has all messages. The label can be shorter than
94+
# its text, so a long message does not make the dock wider or taller.
95+
self.footer_label.setWordWrap(False)
96+
self.footer_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred)
9297
self.back_button = QPushButton("< Back", self)
9398
self.next_button = QPushButton("Next >", self)
9499
self.back_button.clicked.connect(lambda _checked=False: self.go_to(self._neighbour(-1)))
@@ -99,6 +104,8 @@ def __init__(
99104
footer.addWidget(self.next_button)
100105

101106
mainLayout = QVBoxLayout(self)
107+
mainLayout.setContentsMargins(4, 2, 4, 2)
108+
mainLayout.setSpacing(2)
102109
mainLayout.addWidget(self.header)
103110
mainLayout.addWidget(self.step_bar)
104111
mainLayout.addWidget(self.stack, 1)
@@ -203,6 +210,8 @@ def refresh_status(self):
203210
self.model_step.tab.refresh_primary_action()
204211
self.view_step.refresh()
205212
self.next_button.setText(self._next_text(self.current_index))
213+
for section_stack in self.findChildren(SectionStack):
214+
section_stack.refresh_summaries()
206215
page = self.pages[self.current_index]
207216
check = results[page.key]
208217
if check.messages:

0 commit comments

Comments
 (0)