From e1565fd655406cbb40cde133ed670485c5b3a224 Mon Sep 17 00:00:00 2001 From: lachlangrose Date: Tue, 6 Oct 2026 15:16:16 -0300 Subject: [PATCH 1/3] docs: add a plan for a guided modelling workflow Describe the current usability problems, a step-based dock, a direct interpolation entry, and derived data that is updated when its inputs change. Split the work into phases. --- docs/development/usability-plan.md | 370 +++++++++++++++++++++++++++++ docs/index.md | 1 + 2 files changed, 371 insertions(+) create mode 100644 docs/development/usability-plan.md diff --git a/docs/development/usability-plan.md b/docs/development/usability-plan.md new file mode 100644 index 00000000..e6de4dde --- /dev/null +++ b/docs/development/usability-plan.md @@ -0,0 +1,370 @@ +# Usability plan: guided modelling workflow + +## Purpose + +This plan makes the plugin easier to use. It changes the plugin from a set of +separate tools into one guided workflow. It also adds a second entry, so that +users can interpolate surfaces directly from their own constraints. + +Do the work in phases. Each phase is a separate pull request. Each phase must +leave the plugin in a usable state. + +## Current problems + +### Entry points + +- The plugin has three separate entry points: the toolbar, the Plugins menu, + and the dock. None of them shows the order of the workflow. +- The toolbar has approximately 10 actions. Many actions have no icon, so the + toolbar shows long text labels. +- `action_fault_topology` is added to the toolbar two times + (`plugin_main.py`, `initGui`). +- `initProcessing()` is called two times in `initGui`. +- The Modelling and Visualisation tabs are at the bottom of the dock + (`TabPosition.South`). Users can easily miss them. + +### Workflow order + +The real dependency order is: + +``` +Convert data -> Define column -> Extract basal contacts -> Calculate thickness + -> Sample -> Area / DEM / layers -> Fault topology -> Fault adjacency + -> Initialise -> Solve -> View in 3D -> Export +``` + +The UI does not follow this order: + +- "Load Data" is the first tab. It asks for a basal contacts layer, but that + layer does not exist until the user defines the column and runs a dialog. +- The map2loop tools are modal dialogs outside the dock. Each tool adds its + output to the project as a new layer. The user must then go back to + "Load Data" and select that layer. +- Each dialog has its own geology, fault and structure layer pickers. The user + selects the same layers many times. +- There are five ways to build the stratigraphic column, in three places: + - the dock buttons "Initialise from map" and "Initialise from Layer Field" + - the Automatic Sorter dialog + - the User-Defined Stratigraphic Column dialog + - Paint Stratigraphic Order +- The Fault Topology Calculator is a dialog, but Fault Adjacency is a tab. +- Save, Load and Reset State are on the "Load Data" tab, but they apply to all + of the plugin. +- Only the Geological Model tab shows a status. The user cannot see which + earlier steps are done. + +### Feedback and consistency + +- Many actions show a modal `QMessageBox` for a successful result. +- Basal Contacts shows "No Contacts Found" and then also "Successfully + extracted" for the same run (`basal_contacts_widget.py`, + `_on_extractor_finished`). +- The Fault Adjacency instruction labels are created, but they are never added + to a layout. Users do not see the colour key. +- The Fault Adjacency tables use red and green only. This is a problem for + colour-blind users. +- `BaseTab(scrollable=True)` puts lists that scroll inside a scroll area. +- Rows use emoji (🗑️) as icons, but toolbars use QGIS theme icons. +- `export_tab.ui`, `topology_tab.ui`, `viewer_tab.ui` and + `geological_history_tab.ui` are not used. + +### Derived data becomes out of date + +- `extract_basal_contacts` gives each contact the name of the younger unit in + the column at the time of the run (see `get_stratigraphic_unit_names` in + `data_manager.py`). Thus the `basal_unit` values record the column order of + that run. +- When the user changes the column order, the model is marked stale + (`GeologicalModelManager._on_stratigraphic_column_changed`). But the next + initialisation reads the old contacts layer. Some contacts then have the + wrong unit name. The plugin gives no warning. +- The calculated thicknesses come from the contacts. They can also become + incorrect, with no warning. +- The `strat_order` and `strat_thickness` fields that are written onto a map + layer also become out of date. + +### Direct interpolation + +- "Add Feature -> Add Foliation" already lets the user add Value, Form Line, + Orientation and Inequality constraints from layers + (`layer_selection_table.py`). But it is at the end of the stratigraphic + workflow, so users do not easily find it. +- "Add Fault" in the Geological Model tab is "not yet implemented". + +## Target design + +### One dock with steps + +``` ++ LoopStructural ---------------------------- [Save][Open][...] + +| (1) Data ok (2) Stratigraphy ! (3) Faults ok (4) Model - (5) View - | ++-------------------------------------------------------------+ +| contents of the current step | ++-------------------------------------------------------------+ +| ! 2 units have no basal contacts. [Back] [Next >] | ++-------------------------------------------------------------+ +``` + +- Each step has a status: done, problem, or not started. +- The footer shows the most important problem and the next action. +- Save, Open, Reset and Settings go in the dock header. + +### Steps + +| Step | Contents | Moves in from | +|---|---|---| +| 1 Data | Bounding box, CRS, DEM. Source layer roles: geology, fault traces, structural points, contacts. Link to "Convert data". | Load Data tab, Data Conversion dialog | +| 2 Stratigraphy | Column list. "Build column" menu. "Derive from map" menu: basal contacts, thickness, sampler. | Stratigraphic Column tab, Sorter, User-Defined Sorter, Paint Order, Basal Contacts, Thickness, Sampler dialogs | +| 3 Faults | Fault list and fault properties. "Calculate topology". Adjacency tables. | Load Data -> Fault Layers, Fault Topology dialog, Fault Adjacency tab | +| 4 Model | Feature list, constraints for each feature, interpolator settings, one primary build button, list of problems. | Geological Model tab | +| 5 View and export | "Open 3D view". Export of surfaces, block model, cross-sections. | Visualisation dock, unused `export_tab.ui` | + +### Two entries, one structure + +At the start, the user selects one of: + +- **Build from a geological map.** All steps are visible. +- **Interpolate surfaces from constraints.** Steps 2 and 3 are hidden. The + checks for unit names, thickness and adjacency do not apply. + +The user can show the hidden steps later. Steps 2 and 3 are generators: they +make features and constraints for step 4. In step 4, generated constraints are +read-only rows ("from stratigraphic column"). The user can add constraints to a +generated feature, or detach it and edit it. + +Thus both entries use the same area setup, the same build path and the same +viewer. A user can mix the two methods in one model, for example a column-based +model with one more surface for a dyke. + +### Derived data + +Some data is calculated from other inputs. The plugin must keep this data +correct when the inputs change. + +**Contacts source.** In step 1, the user selects one of: + +- **Calculate from geology polygons** (default). The plugin extracts the basal + contacts from the geology layer and the stratigraphic column when it needs + them. A layer is added to the project only so that the user can see or + export the contacts. The model does not read that layer. +- **Use a contacts layer.** The user's own layer is an input. The plugin does + not change it. + +The thickness calculator can already make basal contacts from the geology +layer and the column when no contacts layer is selected. Use the same code. + +**Recorded inputs.** For each derived result, keep the inputs of the run: + +| Derived result | Inputs to record | +|---|---| +| Basal contacts | Column unit order, geology layer, unit name field, override and ignored units | +| Calculated thickness | Basal contacts inputs, calculator type, structure layer, cross-section layer | +| `strat_order` / `strat_thickness` fields on a map layer | Column unit order, thickness values | + +When one of these inputs changes, mark the result as out of date. Use a hash +of the inputs, so that a change and its undo (for example, two reorders) do not +mark the result as out of date. + +**When to calculate again.** + +- Do not calculate again after each change in the column list. Extraction is + too slow, and the user often moves many rows. +- In step 2, show the out-of-date results, for example "Basal contacts are out + of date (the column changed)", with an **Update** button. +- In step 4, the build button calculates all out-of-date derived data before it + initialises the model. A build cannot use out-of-date contacts. + +**Thickness values.** Record the source of each unit thickness: "typed" or +"calculated". Do not overwrite a typed value. Mark a calculated value as out +of date with its contacts, and update it at build time. + +### Toolbar and menu + +- Toolbar: LoopStructural (opens the dock), 3D View, Help. +- Plugins menu: the same actions, plus a "Tools" submenu with the map2loop + dialogs for advanced users. +- The Processing algorithms do not change. + +### General rules + +1. Select a layer one time. All tools read the layer roles from the data + manager. +2. Use `iface.messageBar()` for results. Use `QMessageBox` only for errors that + stop an action, and for confirmations before destructive actions. +3. Each step has a check function. The step header shows its result. +4. Use QGIS widgets and theme icons: `QgsColorButton`, `QgsCollapsibleGroupBox`, + `QgsApplication.getThemeIcon`. Do not use emoji as icons. +5. Do not put a scroll area around a widget that scrolls. +6. Derived data records its inputs. The UI shows when it is out of date. A + model build never uses out-of-date derived data. + +## Phases + +### Phase 0: Quick fixes + +No change to the layout. + +- [ ] Remove the second `toolbar.addAction(self.action_fault_topology)`. +- [ ] Remove the second `initProcessing()` call. +- [ ] Add the Fault Adjacency instruction labels to their group layouts. +- [ ] Basal Contacts: show one message only. Do not show "Success" after "No + Contacts Found". +- [ ] Replace success `QMessageBox.information` calls with message bar + messages. Start with the stratigraphic column, basal contacts, thickness + and save state. +- [ ] Ask for confirmation before "Clear Stratigraphic Column". +- [ ] Delete the unused `.ui` files, or keep `export_tab.ui` for phase 5. + +Files: `plugin_main.py`, `fault_adjacency_tab.py`, `basal_contacts_widget.py`, +`stratigraphic_column.py`, `model_definition_tab.py`. + +Acceptance: the toolbar shows each action one time. The adjacency legend is +visible. No modal dialog opens for a successful action. + +### Phase 1: Shared layer roles + +- [ ] Add layer roles to the data manager: `geology`, `geology_unit_field`, + `fault_traces`, `structure`, `basal_contacts`, `dem`. Save them with the + application state. +- [ ] Add a signal or observer event when a role changes. +- [ ] Make the "Load Data" widgets write to the roles. +- [ ] Make each map2loop dialog read the roles as its default values. The user + can still change the value in the dialog. +- [ ] When Basal Contacts makes a new layer, set it as the `basal_contacts` + role. +- [ ] The stratigraphic column "geology layer" pickers use the `geology` role. +- [ ] Add the contacts source setting: "Calculate from geology polygons" or + "Use a contacts layer". Save it with the application state. +- [ ] Add a derived-data record to the data manager. For each result (basal + contacts, calculated thickness, styled fields), keep a hash of its inputs + and a status: current or out of date. +- [ ] Compare the hash when the column, a layer role or a tool setting changes. + Send an event when a status changes. +- [ ] Record the source of each unit thickness: "typed" or "calculated". A + thickness that the user types in the column list is "typed". +- [ ] In step 2 (until phase 3, in the Stratigraphic Column tab), show the + out-of-date results and an **Update** button. + +Files: `main/data_manager.py`, +`model_definition/*.py`, `map2loop_tools/*_widget.py`, `stratigraphic_column.py`. + +Acceptance: the user selects the geology layer one time. All dialogs show it. +After Basal Contacts runs, the model uses the new contacts layer without more +selections. After the user changes the column order, the UI shows that the +basal contacts and the calculated thicknesses are out of date. If the user +moves a unit and then moves it back, the contacts stay current. + +Tests: unit tests for the role storage, the save/load of roles, and the change +event. Unit tests for the input hash: a reorder marks the contacts out of date; +a change to a unit colour does not; a reorder and its undo give the same hash. +A typed thickness is not overwritten by a calculated thickness. + +### Phase 2: Stratigraphic column changes + +Depends on: the `feat/highlight-strat-unit` branch (map highlight, thickness +styling) is merged. + +- [ ] Put the geology layer group at the top, with labels. Show a summary of + the unit names that have no match. +- [ ] Toolbar: "+ Unit" and "+ Unconformity" as text buttons. A "Build column" + menu for the import actions. An overflow menu for Reverse and Clear. +- [ ] Add "Youngest" and "Oldest" labels above and below the list. +- [ ] Replace the three apply buttons with one "Style map layer" group: a + "Style by" combo (unit colour, stratigraphic order, thickness), a ramp + combo and one Apply button. +- [ ] Rows: use `QgsColorButton`, a theme delete icon, and no repeated + "Thickness:" label. Give unconformity rows a different style. +- [ ] Show text in the empty list. +- [ ] Remove the outer scroll area from the tab. + +Files: `stratigraphic_column/*.py`, `stratigraphic_column/*.ui`, +`geological_history_tab.py`. + +### Phase 3: Step-based dock + +- [ ] Add a step navigation widget with a status for each step. +- [ ] Add a `check()` function for each step. It returns a status and a list of + problems. +- [ ] Move the existing tabs into the steps, in the order of the target design. +- [ ] Move Save, Open, Reset and Settings into the dock header. +- [ ] Move the dialogs into their steps as buttons or menus. Keep the dialogs. + Do not rewrite them in this phase. +- [ ] Move the Fault Topology Calculator into step 3. +- [ ] Reduce the toolbar to three actions. Put the dialogs in a "Tools" + submenu. +- [ ] Keep the `separate_dock_widgets` setting. + +Files: `loop_widget.py`, `modelling/modelling_widget.py`, `plugin_main.py`, +new `gui/modelling/steps/` module. + +Acceptance: a new user can go from an empty project to a solved model with the +"Next" button only. Each step shows why it is not done. + +### Phase 4: Model step and direct interpolation + +- [ ] Replace "Initialize Model", "Solve Model" and "Update Model Data" with one + primary button. Its text and action come from the model state. +- [ ] Show the problems from all steps before the build. +- [ ] Before the build, calculate all out-of-date derived data again: basal + contacts first, then calculated thicknesses. Run this in a background + task with progress. If the calculation fails, stop the build and show + the error. +- [ ] With "Calculate from geology polygons", give the extracted contacts + directly to the model. Update the project contacts layer for display + only. +- [ ] Add the start choice: "Build from a geological map" or "Interpolate + surfaces from constraints". Save the choice with the state. +- [ ] Constraint list for each feature: source layer, constraint type, field + mapping, weight, Z source (layer Z, DEM or constant). +- [ ] Add the constraint types: value, interface, gradient/normal, tangent, + inequality, pairwise inequality. +- [ ] Show generated constraints as read-only rows. Add "Detach" to make a + generated feature editable. +- [ ] Build and preview one feature: an isoline on the map canvas, or a surface + in the 3D view. +- [ ] Implement "Add Fault" in the model step. + +Files: `geological_model_tab/*.py`, `layer_selection_table.py`, +`feature_details_panel/*.py`, `main/model_manager.py`. + +Acceptance: a user can make a model from constraint layers only, without a +stratigraphic column. A user can add a direct-constraint feature to a +column-based model. After the user changes the column order and builds the +model, the model uses basal contacts and thicknesses that match the new order. + +### Phase 5: View and export + +- [ ] Show "Open 3D view" as the next action after a successful build. +- [ ] Add export of surfaces, block model and cross-sections to step 5. + +## Risks + +- **Large UI change.** Users of the current version must learn the new layout. + Update `docs/usage` in the same pull request as each phase. +- **Saved state files.** Phases 1 and 4 add new data to the state. Load old + state files without errors, and use default values for new keys. +- **Dialogs and Processing algorithms share code.** Phase 1 must not change the + Processing algorithm parameters. +- **Two dock modes.** The `separate_dock_widgets` setting must work after + phase 3. +- **Build time.** Extraction and thickness calculation at build time make the + build slower. Calculate only the results that are out of date. Show the + progress of each part. +- **User edits to a derived layer.** A user can edit the contacts layer that + the plugin made. With "Calculate from geology polygons", the next update + overwrites these edits. Show a warning before the overwrite, and offer to + change the contacts source to "Use a contacts layer". + +## Open questions + +1. Must the steps be strict (the user cannot go to step 4 before step 2 is + done), or only a guide? Recommendation: only a guide. Show problems, but do + not lock steps. +2. Is the Data Conversion dialog part of step 1, or a separate tool? +3. In direct mode, which constraint types does each interpolator (FDI, PLI, + surfe) support? The UI must hide types that the selected interpolator does + not support. +4. Is there a minimum QGIS version for the new widgets? +5. Must the Processing algorithms also use the derived-data record, or only + the dock? Recommendation: only the dock. Processing runs are single runs + with explicit inputs. diff --git a/docs/index.md b/docs/index.md index 6c8ea8a5..ca83f886 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,4 +34,5 @@ development/translation development/packaging development/testing development/history +development/usability-plan ``` From 9202211f38f182150addf9a37c4bf832da8a8f0a Mon Sep 17 00:00:00 2001 From: lachlangrose Date: Wed, 7 Oct 2026 09:18:43 -0300 Subject: [PATCH 2/3] fix: quick fixes to the toolbar, adjacency legend and success messages Add each toolbar action one time and call initProcessing one time. Show the fault adjacency instructions. Show one message for the basal contacts result. Show success results in the message bar. Ask for confirmation before the stratigraphic column is cleared. Delete the unused .ui files, and keep export_tab.ui for the export step. --- docs/development/usability-plan.md | 14 +- .../map2loop_tools/basal_contacts_widget.py | 16 +- .../thickness_calculator_widget.py | 13 +- loopstructural/gui/messages.py | 33 ++ .../gui/modelling/fault_adjacency_tab.py | 2 + .../gui/modelling/geological_history_tab.ui | 371 ------------------ .../model_definition/model_definition_tab.py | 5 +- .../stratigraphic_column.py | 20 +- loopstructural/gui/modelling/topology_tab.ui | 69 ---- loopstructural/gui/modelling/viewer_tab.ui | 69 ---- loopstructural/plugin_main.py | 3 - 11 files changed, 72 insertions(+), 543 deletions(-) create mode 100644 loopstructural/gui/messages.py delete mode 100644 loopstructural/gui/modelling/geological_history_tab.ui delete mode 100644 loopstructural/gui/modelling/topology_tab.ui delete mode 100644 loopstructural/gui/modelling/viewer_tab.ui diff --git a/docs/development/usability-plan.md b/docs/development/usability-plan.md index e6de4dde..c9854f32 100644 --- a/docs/development/usability-plan.md +++ b/docs/development/usability-plan.md @@ -204,16 +204,16 @@ of date with its contacts, and update it at build time. No change to the layout. -- [ ] Remove the second `toolbar.addAction(self.action_fault_topology)`. -- [ ] Remove the second `initProcessing()` call. -- [ ] Add the Fault Adjacency instruction labels to their group layouts. -- [ ] Basal Contacts: show one message only. Do not show "Success" after "No +- [x] Remove the second `toolbar.addAction(self.action_fault_topology)`. +- [x] Remove the second `initProcessing()` call. +- [x] Add the Fault Adjacency instruction labels to their group layouts. +- [x] Basal Contacts: show one message only. Do not show "Success" after "No Contacts Found". -- [ ] Replace success `QMessageBox.information` calls with message bar +- [x] Replace success `QMessageBox.information` calls with message bar messages. Start with the stratigraphic column, basal contacts, thickness and save state. -- [ ] Ask for confirmation before "Clear Stratigraphic Column". -- [ ] Delete the unused `.ui` files, or keep `export_tab.ui` for phase 5. +- [x] Ask for confirmation before "Clear Stratigraphic Column". +- [x] Delete the unused `.ui` files, or keep `export_tab.ui` for phase 5. Files: `plugin_main.py`, `fault_adjacency_tab.py`, `basal_contacts_widget.py`, `stratigraphic_column.py`, `model_definition_tab.py`. diff --git a/loopstructural/gui/map2loop_tools/basal_contacts_widget.py b/loopstructural/gui/map2loop_tools/basal_contacts_widget.py index b7d3400c..91a4fbb1 100644 --- a/loopstructural/gui/map2loop_tools/basal_contacts_widget.py +++ b/loopstructural/gui/map2loop_tools/basal_contacts_widget.py @@ -12,6 +12,7 @@ from ...main.vectorLayerWrapper import addGeoDataFrameToproject from ..background_task import finish_background_task, start_background_task from ..compatibility import configure_layer_combo +from ..messages import push_success, push_warning class BasalContactsWidget(QWidget): @@ -268,16 +269,13 @@ def _on_extractor_finished(self, payload): basal_layer, 'basal_unit' ) else: - QMessageBox.information( - self, - "No Contacts Found", - "No contacts were found with the given parameters.", + contact_type = None + if contact_type is None: + push_warning( + "No Contacts Found", "No contacts were found with the given parameters." ) - QMessageBox.information( - self, - "Success", - f"Successfully extracted {contact_type}!", - ) + else: + push_success("Basal Contacts", f"Successfully extracted {contact_type}.") if self._debug and self._debug.is_debug(): try: self._debug.save_debug_file( diff --git a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py index 16f2c42a..b77013f5 100644 --- a/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py +++ b/loopstructural/gui/map2loop_tools/thickness_calculator_widget.py @@ -10,6 +10,7 @@ from loopstructural.gui.background_task import finish_background_task, start_background_task from loopstructural.gui.compatibility import configure_layer_combo +from loopstructural.gui.messages import push_success from loopstructural.toolbelt.preferences import PlgOptionsManager from ...main.helpers import ColumnMatcher, get_layer_names @@ -468,9 +469,8 @@ def _on_calculator_finished(self, result): addGeoDataFrameToproject(lines, "Thickness Lines") if location_tracking is not None: addGeoDataFrameToproject(location_tracking, "Thickness Locations") - QMessageBox.information( - self, - "Success", + push_success( + "Thickness Calculator", "Thickness calculation completed successfully and added to project.", ) self.task_succeeded.emit() @@ -478,15 +478,14 @@ def _on_calculator_finished(self, result): if hasattr(result, 'geometry'): addGeoDataFrameToproject(result, "Thickness Results") - QMessageBox.information( - self, - "Success", + push_success( + "Thickness Calculator", "Thickness calculation completed successfully and added to project.", ) self.task_succeeded.emit() return - QMessageBox.information(self, "Success", f"Thickness calculation completed: {result}") + push_success("Thickness Calculator", f"Thickness calculation completed: {result}") self.task_succeeded.emit() def _on_calculator_error(self, traceback_text): diff --git a/loopstructural/gui/messages.py b/loopstructural/gui/messages.py new file mode 100644 index 00000000..d0f245f8 --- /dev/null +++ b/loopstructural/gui/messages.py @@ -0,0 +1,33 @@ +"""Show the result of an action without a modal dialog.""" + +from qgis.core import Qgis, QgsMessageLog +from qgis.utils import iface + + +def push_success(title: str, text: str, duration: int = 5): + """Show a success message in the message bar of QGIS. + + Use a `QMessageBox` only for an error that stops an action, and for the + confirmation before a destructive action. + """ + if iface is not None: + iface.messageBar().pushSuccess(title, text, duration=duration) + else: + # No main window, for example in the tests + QgsMessageLog.logMessage(f"{title}: {text}", "LoopStructural", Qgis.MessageLevel.Success) + + +def push_info(title: str, text: str, duration: int = 5): + """Show an information message in the message bar of QGIS.""" + if iface is not None: + iface.messageBar().pushInfo(title, text, duration=duration) + else: + QgsMessageLog.logMessage(f"{title}: {text}", "LoopStructural", Qgis.MessageLevel.Info) + + +def push_warning(title: str, text: str, duration: int = 10): + """Show a warning in the message bar of QGIS, for a result that is not an error.""" + if iface is not None: + iface.messageBar().pushWarning(title, text, duration=duration) + else: + QgsMessageLog.logMessage(f"{title}: {text}", "LoopStructural", Qgis.MessageLevel.Warning) diff --git a/loopstructural/gui/modelling/fault_adjacency_tab.py b/loopstructural/gui/modelling/fault_adjacency_tab.py index 4908154f..cb2c94b8 100644 --- a/loopstructural/gui/modelling/fault_adjacency_tab.py +++ b/loopstructural/gui/modelling/fault_adjacency_tab.py @@ -38,6 +38,7 @@ def init_ui(self): ) self.fault_fault_instructions_label = QLabel(self.fault_fault_instructions) self.fault_fault_instructions_label.setWordWrap(True) + self.fault_table_layout.addWidget(self.fault_fault_instructions_label) self.update_fault_adjacency_table() self.layout().addWidget(self.fault_table_group) @@ -51,6 +52,7 @@ def init_ui(self): ) self.strat_fault_instructions_label = QLabel(self.strat_fault_instructions) self.strat_fault_instructions_label.setWordWrap(True) + self.stratigraphic_table_layout.addWidget(self.strat_fault_instructions_label) self.update_stratigraphic_units_table() self.layout().addWidget(self.stratigraphic_table_group) diff --git a/loopstructural/gui/modelling/geological_history_tab.ui b/loopstructural/gui/modelling/geological_history_tab.ui deleted file mode 100644 index 6a576424..00000000 --- a/loopstructural/gui/modelling/geological_history_tab.ui +++ /dev/null @@ -1,371 +0,0 @@ - - - Form - - - - 0 - 0 - 622 - 785 - - - - Form - - - - - 20 - 10 - 591 - 701 - - - - - - - - - Stratigraphic Column - - - - - 10 - 30 - 561 - 281 - - - - - QLayout::SetMinimumSize - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - <html><head/><body><p>Save the stratigraphic order and thickness as new columns in the contacts basal contacts layer. Note this will overwrite any existing data in the &quot;LS_order' and 'LS_thickness' columns</p></body></html> - - - Add Unit - - - - - - - - - - - - - - - - - Fault Properties - - - - - 10 - 20 - 679 - 540 - - - - - - - - - - - - Active - - - - - - - false - - - - - - - - - Orientation - - - - - - Displacement - - - - - - - false - - - -10000000000.000000000000000 - - - 10000000000.000000000000000 - - - - - - - Dip - - - - - - - false - - - 0.000000000000000 - - - 90.000000000000000 - - - - - - - Pitch - - - - - - - false - - - 0.000000000000000 - - - 180.000000000000000 - - - - - - - - - - Position - - - - - - - - x - - - - - - - false - - - -100000000000.000000000000000 - - - 100000000000.000000000000000 - - - - - - - false - - - -100000000000.000000000000000 - - - 100000000000.000000000000000 - - - - - - - y - - - - - - - z - - - - - - - false - - - -100000000000.000000000000000 - - - 0.000000000000000 - - - - - - - - - false - - - Select on Map - - - - - - - - - - Ellipsoid Size - - - - - - Major Axis - - - - - - - false - - - 10000000.000000000000000 - - - - - - - Minor Axis - - - - - - - false - - - 10000000.000000000000000 - - - - - - - Intermediate Axis - - - - - - - false - - - 1000000.000000000000000 - - - - - - - - - - Add Elipse to Map - - - - - - - - - - - - - - - QgsDoubleSpinBox - QDoubleSpinBox -
qgsdoublespinbox.h
-
-
- - -
diff --git a/loopstructural/gui/modelling/model_definition/model_definition_tab.py b/loopstructural/gui/modelling/model_definition/model_definition_tab.py index c7a115da..a6e969fe 100644 --- a/loopstructural/gui/modelling/model_definition/model_definition_tab.py +++ b/loopstructural/gui/modelling/model_definition/model_definition_tab.py @@ -8,6 +8,7 @@ QWidget, ) +from loopstructural.gui.messages import push_success from loopstructural.gui.modelling.base_tab import BaseTab from .bounding_box import BoundingBoxWidget @@ -87,9 +88,7 @@ def on_save_state_clicked(self): self, "Save Application State", f"Failed to save application state:\n{err}" ) else: - QMessageBox.information( - self, "Save Application State", f"Application state saved to:\n{filepath}" - ) + push_success("Save Application State", f"Application state saved to: {filepath}") def on_load_state_clicked(self): """Prompt for a state file and, after confirmation, load it.""" diff --git a/loopstructural/gui/modelling/stratigraphic_column/stratigraphic_column.py b/loopstructural/gui/modelling/stratigraphic_column/stratigraphic_column.py index 86cbd7d1..48faf685 100644 --- a/loopstructural/gui/modelling/stratigraphic_column/stratigraphic_column.py +++ b/loopstructural/gui/modelling/stratigraphic_column/stratigraphic_column.py @@ -19,6 +19,7 @@ from loopstructural.__about__ import DIR_PLUGIN_ROOT from loopstructural.gui.compatibility import configure_layer_combo +from loopstructural.gui.messages import push_success from loopstructural.gui.modelling.stratigraphic_column.unconformity import UnconformityWidget from loopstructural.main.helpers import ColumnMatcher, get_layer_names @@ -217,7 +218,18 @@ def _build_tool_button(self, icon: QIcon, tooltip: str) -> QToolButton: return button def clearColumn(self): - """Clear the stratigraphic column.""" + """Clear the stratigraphic column, after the user confirms.""" + if self.unitList.count() > 0: + reply = QMessageBox.question( + self, + "Clear Stratigraphic Column", + "This removes all units and unconformities from the stratigraphic column. " + "This cannot be undone.\n\nContinue?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No, + ) + if reply != QMessageBox.StandardButton.Yes: + return # Use the data manager's clear method to ensure callback is triggered # This will notify all listening widgets (including this one and others) if self.data_manager: @@ -470,8 +482,7 @@ def apply_colours_to_layer(self): return applied = self.data_manager.apply_stratigraphic_colours_to_layer(layer, field_name) if applied: - QMessageBox.information( - self, + push_success( "Apply Colours to Map Layer", f"Applied stratigraphic column colours to layer '{layer.name()}'.", ) @@ -501,8 +512,7 @@ def apply_age_to_layer(self): layer, field_name, ramp_name=ramp_name ) if applied: - QMessageBox.information( - self, + push_success( "Apply Stratigraphic Age to Map Layer", f"Applied stratigraphic age and graduated styling to layer '{layer.name()}'.", ) diff --git a/loopstructural/gui/modelling/topology_tab.ui b/loopstructural/gui/modelling/topology_tab.ui deleted file mode 100644 index e3162d4d..00000000 --- a/loopstructural/gui/modelling/topology_tab.ui +++ /dev/null @@ -1,69 +0,0 @@ - - - Form - - - - 0 - 0 - 607 - 713 - - - - Form - - - - - 0 - 0 - 591 - 701 - - - - - - - - - Fault-Fault - - - - - 0 - 20 - 571 - 271 - - - - - - - - - - - Fault-Stratigraphy - - - - - 0 - 20 - 571 - 311 - - - - - - - - - - - diff --git a/loopstructural/gui/modelling/viewer_tab.ui b/loopstructural/gui/modelling/viewer_tab.ui deleted file mode 100644 index ec3088d4..00000000 --- a/loopstructural/gui/modelling/viewer_tab.ui +++ /dev/null @@ -1,69 +0,0 @@ - - - Form - - - - 0 - 0 - 123 - 207 - - - - Form - - - - - 0 - 10 - 591 - 511 - - - - - - - - - - QLayout::SetMinimumSize - - - - - Add Data - - - - - - - Add Block Model - - - - - - - Add Surfaces - - - - - - - Clear - - - - - - - - - - - diff --git a/loopstructural/plugin_main.py b/loopstructural/plugin_main.py index 8204b6dd..9f3c48b3 100644 --- a/loopstructural/plugin_main.py +++ b/loopstructural/plugin_main.py @@ -236,7 +236,6 @@ def initGui(self): self.toolbar.addAction(self.action_basal_contacts) self.toolbar.addAction(self.action_thickness) self.toolbar.addAction(self.action_paint_strat_order) - self.toolbar.addAction(self.action_fault_topology) self.iface.addPluginToMenu(__title__, self.action_sampler) self.iface.addPluginToMenu(__title__, self.action_sorter) @@ -246,8 +245,6 @@ def initGui(self): self.iface.addPluginToMenu(__title__, self.action_paint_strat_order) self.iface.addPluginToMenu(__title__, self.action_fault_topology) - self.initProcessing() - # -- Help menu # documentation From 8e9e045c0070f6428b06fb5fe584ad1cce1b8b90 Mon Sep 17 00:00:00 2001 From: lachlangrose Date: Wed, 7 Oct 2026 11:31:18 -0300 Subject: [PATCH 3/3] fix: use pushMessage so the message bar accepts a duration QgsMessageBar.pushSuccess, pushInfo and pushWarning do not accept a duration argument. This caused a TypeError when a tool showed a success message. --- loopstructural/gui/messages.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/loopstructural/gui/messages.py b/loopstructural/gui/messages.py index d0f245f8..96ada241 100644 --- a/loopstructural/gui/messages.py +++ b/loopstructural/gui/messages.py @@ -11,7 +11,7 @@ def push_success(title: str, text: str, duration: int = 5): confirmation before a destructive action. """ if iface is not None: - iface.messageBar().pushSuccess(title, text, duration=duration) + iface.messageBar().pushMessage(title, text, level=Qgis.MessageLevel.Success, duration=duration) else: # No main window, for example in the tests QgsMessageLog.logMessage(f"{title}: {text}", "LoopStructural", Qgis.MessageLevel.Success) @@ -20,7 +20,7 @@ def push_success(title: str, text: str, duration: int = 5): def push_info(title: str, text: str, duration: int = 5): """Show an information message in the message bar of QGIS.""" if iface is not None: - iface.messageBar().pushInfo(title, text, duration=duration) + iface.messageBar().pushMessage(title, text, level=Qgis.MessageLevel.Info, duration=duration) else: QgsMessageLog.logMessage(f"{title}: {text}", "LoopStructural", Qgis.MessageLevel.Info) @@ -28,6 +28,6 @@ def push_info(title: str, text: str, duration: int = 5): def push_warning(title: str, text: str, duration: int = 10): """Show a warning in the message bar of QGIS, for a result that is not an error.""" if iface is not None: - iface.messageBar().pushWarning(title, text, duration=duration) + iface.messageBar().pushMessage(title, text, level=Qgis.MessageLevel.Warning, duration=duration) else: QgsMessageLog.logMessage(f"{title}: {text}", "LoopStructural", Qgis.MessageLevel.Warning)