From cdeca8a2e8a76a3b1c279e16df69738abf8471fb Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 23 Oct 2026 13:23:34 +0200 Subject: [PATCH 1/6] BoundaryCondition: restore back/forward and view buttons in the edit plot toolbar --- src/View/BoundaryCondition/Edit/Window.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 29281d6f..e4555dac 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -185,7 +185,10 @@ class EditBoundaryConditionWindow(PamhyrWindow): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") self.toolbar = PamhyrPlotToolbar( - self.canvas, self + self.canvas, self, items=[ + "home", "move", "zoom", "save", + "iso", "back/forward" + ] ) self.verticalLayout.addWidget(self.toolbar) self.verticalLayout.addWidget(self.canvas) From d8d1b4cd1f37015bb77cefd34d897de09ecb62f4 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Fri, 23 Oct 2026 13:37:03 +0200 Subject: [PATCH 2/6] HydraulicStructures: fix crash when selecting a rk before the reach --- src/View/HydraulicStructures/Table.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/View/HydraulicStructures/Table.py b/src/View/HydraulicStructures/Table.py index 5947b9b0..c553139e 100644 --- a/src/View/HydraulicStructures/Table.py +++ b/src/View/HydraulicStructures/Table.py @@ -96,7 +96,8 @@ class ComboBoxDelegate(QItemDelegate): reach = self._data.hydraulic_structures\ .get(index.row())\ .input_reach - profiles = list( + # No reach selected yet: no profile to choose from + profiles = [] if reach is None else list( filter( lambda p: p.display_name() == text, reach.reach.profiles From a4eee962f7a25eb441bd133a76d82c789542e261 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 26 Oct 2026 10:57:29 +0100 Subject: [PATCH 3/6] Geometry: fix error on the first right click in the profile plot (water limits not yet cached) --- src/Model/Geometry/ProfileXYZ.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Model/Geometry/ProfileXYZ.py b/src/Model/Geometry/ProfileXYZ.py index 905b2701..f5750071 100644 --- a/src/Model/Geometry/ProfileXYZ.py +++ b/src/Model/Geometry/ProfileXYZ.py @@ -17,6 +17,7 @@ # -*- coding: utf-8 -*- import queue +import types import random import logging import numpy as np @@ -806,7 +807,11 @@ class ProfileXYZ(Profile, SQLSubModel): return poly def wet_points(self, z): - left, right = self.get_water_limits(z) + limits = self.get_water_limits(z) + + if isinstance(limits, types.GeneratorType): + limits = next(limits) + left, right = limits points = list(filter(lambda p: p.z < z, self._points)) points = [left] + points + [right] From c2a8e17bf382f17552691b08271d828a55e9588c Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 26 Oct 2026 15:52:46 +0100 Subject: [PATCH 4/6] HydraulicStructures: always show the reach plot, highlight the selected structure in blue, full zoom toolbars and fitted views --- src/View/HydraulicStructures/PlotAC.py | 2 +- src/View/HydraulicStructures/PlotRKC.py | 45 ++++++++--- src/View/HydraulicStructures/Window.py | 101 +++++++++++++++--------- 3 files changed, 99 insertions(+), 49 deletions(-) diff --git a/src/View/HydraulicStructures/PlotAC.py b/src/View/HydraulicStructures/PlotAC.py index 7c39ec19..e15efe34 100644 --- a/src/View/HydraulicStructures/PlotAC.py +++ b/src/View/HydraulicStructures/PlotAC.py @@ -42,7 +42,7 @@ class PlotAC(PamhyrPlot): self._isometric_axis = False self._auto_relim_update = True - self._autoscale_update = False + self._autoscale_update = True @property def river(self): diff --git a/src/View/HydraulicStructures/PlotRKC.py b/src/View/HydraulicStructures/PlotRKC.py index 4c4a982b..9ec4d052 100644 --- a/src/View/HydraulicStructures/PlotRKC.py +++ b/src/View/HydraulicStructures/PlotRKC.py @@ -30,6 +30,14 @@ _translate = QCoreApplication.translate class PlotRKC(PamhyrPlot): + # Class attributes: PlotRKZ (frictions) does not call this __init__ + _selected = None # Selected hydraulic structure, in blue + profile = None + line_rk_zmin = None + line_rk_zmin_zmax = None + hs_vlines = None + _hs_drawn = [] # Structures of hs_vlines, in order + def __init__(self, canvas=None, trad=None, toolbar=None, river=None, reach=None, profile=None, parent=None): @@ -133,19 +141,14 @@ class PlotRKC(PamhyrPlot): vymin = [] vymax = [] self.anotate_lst = [] + self._hs_drawn = [] hs_color = [] - index = self.parent.tableView.selectedIndexes() - for i, hs in enumerate(lhs): + for hs in lhs: section = hs.input_section if section is None: continue - if i == index[0].row(): - color = "blue" - elif hs.enabled: - color = "red" - else: - color = "darkgrey" + color = self._hs_color(hs) x = section.rk if x is not None: @@ -163,6 +166,7 @@ class PlotRKC(PamhyrPlot): vymin.append(min(z_min)) vymax.append(max(z_max)) hs_color.append(color) + self._hs_drawn.append(hs) self.hs_vlines = self.canvas.axes.vlines( x=vx, ymin=vymin, ymax=vymax, @@ -171,6 +175,28 @@ class PlotRKC(PamhyrPlot): color=hs_color, ) + def _hs_color(self, hs): + if hs is self._selected: + return "blue" + if hs.enabled: + return "red" + return "darkgrey" + + def set_selected(self, hs): + """Draw the selected structure in blue, without redrawing the + plot (the view is kept)""" + self._selected = hs + if self.hs_vlines is None: + return + + colors = [self._hs_color(h) for h in self._hs_drawn] + if colors: + self.hs_vlines.set_color(colors) + for annotation, color in zip(self.anotate_lst, colors): + annotation.set_color(color) + + self.canvas.figure.canvas.draw_idle() + def color_highlight(self, highlight): reach = self._current_reach @@ -254,7 +280,8 @@ class PlotRKC(PamhyrPlot): closest = self._closest_profile(event) index = self.parent.tableView.selectedIndexes() - if self.parent._table is not None: + # Moves the selected structure: nothing to do without selection + if self.parent._table is not None and len(index) > 2: self.parent._table.setData(index[2], closest) return diff --git a/src/View/HydraulicStructures/Window.py b/src/View/HydraulicStructures/Window.py index 3cb4a126..2e321346 100644 --- a/src/View/HydraulicStructures/Window.py +++ b/src/View/HydraulicStructures/Window.py @@ -130,11 +130,13 @@ class HydraulicStructuresWindow(PamhyrWindow): self._checkbox = self.find(QCheckBox, f"checkBox") self._set_checkbox_state() + _toolbar_items = ["home", "move", "zoom", "save", "iso", "back/forward"] + def setup_plots(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") self.toolbar = PamhyrPlotToolbar( - self.canvas, self + self.canvas, self, items=self._toolbar_items ) self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.toolbar) @@ -143,7 +145,7 @@ class HydraulicStructuresWindow(PamhyrWindow): self.plot_rkc = PlotRKC( canvas=self.canvas, river=self._study.river, - reach=None, + reach=self._displayed_reach(None), profile=None, trad=self._trad, toolbar=self.toolbar, @@ -154,7 +156,7 @@ class HydraulicStructuresWindow(PamhyrWindow): self.canvas_2 = MplCanvas(width=5, height=4, dpi=100) self.canvas_2.setObjectName("canvas_2") self.toolbar_2 = PamhyrPlotToolbar( - self.canvas_2, self + self.canvas_2, self, items=self._toolbar_items ) self.plot_layout_2 = self.find(QVBoxLayout, "verticalLayout_2") self.plot_layout_2.addWidget(self.toolbar_2) @@ -183,7 +185,7 @@ class HydraulicStructuresWindow(PamhyrWindow): table = self.find(QTableView, "tableView") table.selectionModel()\ .selectionChanged\ - .connect(self.update) + .connect(self.update_selection) self._table.dataChanged.connect(self.update) self._table.layoutChanged.connect(self.update) @@ -282,44 +284,65 @@ class HydraulicStructuresWindow(PamhyrWindow): ) def update(self): + """Data changed: plots redrawn, fitted to their data""" self._set_checkbox_state() - self._update_clear_plot() + self._update_plots(redraw=True) - def _update_clear_plot(self): + def update_selection(self): + """Selection changed: the selected structure is drawn in blue, + the view is kept (unless its reach is another one)""" + self._set_checkbox_state() + self._update_plots(redraw=False) + + def _selected_structure(self): rows = self.index_selected_rows() - if len(rows) == 0 or len(self._hs_lst) == 0: - self._update_clear_all() + return None + return self._hs_lst.get(rows[0]) + + def _displayed_reach(self, selected): + """Reach of the selected structure; otherwise the reach already + shown, the reach of the first structure, or the first reach""" + edges = self._study.river.enable_edges() + if selected is not None and selected.input_reach in edges: + return selected.input_reach + + current = getattr(self, "plot_rkc", None) + if current is not None and current._current_reach in edges: + return current._current_reach + + for hs in self._hs_lst.lst: + if hs.input_reach in edges: + return hs.input_reach + + return edges[0] if len(edges) > 0 else None + + def _update_plots(self, redraw=True): + selected = self._selected_structure() + reach = self._displayed_reach(selected) + + self.plot_rkc._selected = selected + if redraw or reach is not self.plot_rkc._current_reach: + self.plot_rkc.set_reach(reach) + else: + self.plot_rkc.set_selected(selected) + + self._update_profile_plot(selected, reach) + + def _update_profile_plot(self, selected, reach): + profile = None + if (selected is not None and reach is not None + and selected.input_section is not None): + profiles = reach.reach.get_profiles_from_rk( + float(selected.input_section.rk) + ) + if profiles: + profile = profiles[0] + + if profile is None: + self.plot_ac.clear() + self.plot_ac.update_idle() return - reach = self._hs_lst.get(rows[0]).input_reach - if reach is not None: - self.plot_rkc.set_reach(reach) - self.plot_ac.set_reach(reach) - - profile = self._hs_lst.get(rows[0]).input_section - if profile is not None: - profiles = reach.reach\ - .get_profiles_from_rk( - float(profile.rk) - ) - - if len(profiles) != 0 and profiles is not None: - profile = profiles[0] - - self.plot_rkc.set_profile(profile) - self.plot_ac.set_profile(profile) - else: - self._update_clear_profile() - else: - self._update_clear_profile() - else: - self._update_clear_all() - - def _update_clear_all(self): - self.plot_rkc.clear() - self.plot_ac.clear() - - def _update_clear_profile(self): - self.plot_ac.clear() - self.plot_rkc.clear_profile() + self.plot_ac.set_reach(reach) + self.plot_ac.set_profile(profile) From ef10588880f5e6a31e35cc611daa88cb5f99514d Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 26 Oct 2026 16:06:55 +0100 Subject: [PATCH 5/6] BoundaryCondition: refit the edit plot to the data after each change --- src/View/BoundaryCondition/Edit/Plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py index badcb96a..88e97ee9 100644 --- a/src/View/BoundaryCondition/Edit/Plot.py +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -54,7 +54,7 @@ class Plot(PamhyrPlot): self._isometric_axis = False self._auto_relim_update = True - self._autoscale_update = False + self._autoscale_update = True def custom_ticks(self): if self.data.header[0] != "time": From 797de096838449dbc126b57727882a9e1bbeb516 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 26 Oct 2026 16:55:01 +0100 Subject: [PATCH 6/6] InitialConditions: sort rows after each generation (single undo step), and fix undo of generation and sort (avoid STOP7 error on Hogneau example) --- src/View/InitialConditions/Table.py | 10 ++++++++++ src/View/InitialConditions/UndoCommand.py | 22 +++++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/View/InitialConditions/Table.py b/src/View/InitialConditions/Table.py index 0290f817..d3799496 100644 --- a/src/View/InitialConditions/Table.py +++ b/src/View/InitialConditions/Table.py @@ -376,9 +376,19 @@ class InitialConditionTableModel(PamhyrTableModel): self.layoutChanged.emit() def generate(self, generator, param, option): + self.layoutAboutToBeChanged.emit() + + self._undo.beginMacro(f"generate {generator}") self._undo.push( GenerateCommand( self._lst, generator, param, option ) ) + self._undo.push( + SortCommand( + self._lst, False + ) + ) + self._undo.endMacro() + self.layoutChanged.emit() diff --git a/src/View/InitialConditions/UndoCommand.py b/src/View/InitialConditions/UndoCommand.py index d5af00f5..95f1ee47 100644 --- a/src/View/InitialConditions/UndoCommand.py +++ b/src/View/InitialConditions/UndoCommand.py @@ -100,15 +100,19 @@ class SortCommand(QUndoCommand): self._indexes = None def undo(self): - ll = self._ics.data + order = { + id(x): self._indexes[i] + for i, x in enumerate(self._ics.data) + } self._ics.sort( - key=lambda x: self._indexes[ll.index(x)] + key=lambda x: order.get(id(x), len(order)) ) def redo(self): self._ics.sort( reverse=self._reverse, - key=lambda x: x["rk"].rk + key=lambda x: ((x["rk"] is None), + 0.0 if x["rk"] is None else x["rk"].rk) ) if self._indexes is None: self._indexes = list( @@ -182,11 +186,19 @@ class GenerateCommand(QUndoCommand): self._ics = ics self._param = param self._option = option - self._copy = self._ics.data + # All the rows, with their deleted flag: the generation marks the + # previous rows as deleted (clear_data) + self._copy = list(self._ics._data) + self._deleted = [d.is_deleted() for d in self._copy] self._generator = generator def undo(self): - self._ics.data = self._copy + for data, deleted in zip(self._copy, self._deleted): + if deleted: + data.set_as_deleted() + else: + data.set_as_not_deleted() + self._ics.data = list(self._copy) def redo(self): if self._generator == "growing":