From 499e16975f353cf0b9c434fe020854b008208c51 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 7 Sep 2026 16:31:35 +0200 Subject: [PATCH 1/7] Design main window: New tab with network of the study --- src/View/MainWindow.py | 19 +++++++ src/View/MainWindowTabGraph.py | 92 ++++++++++++++++++++++++++++++++++ src/View/Translate.py | 3 ++ 3 files changed, 114 insertions(+) create mode 100644 src/View/MainWindowTabGraph.py diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 8e31bb35..06619bde 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -61,6 +61,7 @@ from View.WaitingDialog import WaitingDialog from View.MainWindowTabInfo import WidgetInfo from View.MainWindowTabChecker import WidgetChecker +from View.MainWindowTabGraph import WidgetGraph from View.Configure.Window import ConfigureWindow from View.Study.Window import NewStudyWindow @@ -235,6 +236,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def setup_tab(self): self.setup_tab_info() + self.setup_tab_graph() self.setup_tab_checker() def setup_tab_info(self): @@ -261,6 +263,18 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self._trad["tab_checker_name"] ) + def setup_tab_graph(self): + tab_widget = self.findChild(QTabWidget, "tabWidget") + + self._tab_widget_graph = WidgetGraph( + study=self._study, trad=self._trad, parent=self + ) + + tab_widget.addTab( + self._tab_widget_graph, + self._trad["tab_graph_name"] + ) + def enable_actions(self, action: str, enable: bool): """Enable of disable an action componant @@ -545,6 +559,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self._do_propagate_update_rec(window, keys) self._tab_widget_checker.update(modules=keys) + self._tab_widget_graph.update() def _do_propagate_update_info_tab(self, keys): modules = Modules.modelling_list() @@ -570,6 +585,10 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self._tab_widget_info.study = self._study self._tab_widget_info.update() + if self._tab_widget_graph.study != self._study: + self._tab_widget_graph.study = self._study + self._tab_widget_graph.update() + if self._tab_widget_checker.study != self._study: self._tab_widget_checker.study = self._study self._tab_widget_checker.update(modules=Modules.STUDY) diff --git a/src/View/MainWindowTabGraph.py b/src/View/MainWindowTabGraph.py new file mode 100644 index 00000000..88244d8c --- /dev/null +++ b/src/View/MainWindowTabGraph.py @@ -0,0 +1,92 @@ +# MainWindowTabGraph.py -- Pamhyr +# Copyright (C) 2026 INRAE +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# -*- coding: utf-8 -*- + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.PlotXY import PlotXY +from View.Network.GraphWidget import GraphWidget + +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout + + +class WidgetGraph(QWidget): + """Read-only network graph displayed in the main window.""" + + def __init__(self, study=None, trad=None, parent=None): + super(WidgetGraph, self).__init__(parent) + + self._study = study + self._trad = trad + + layout = QHBoxLayout(self) + self._graph_widget = None + + self._graph_layout = QVBoxLayout() + layout.addLayout(self._graph_layout, 1) + + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas_graph") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self, + items=["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout = QVBoxLayout() + self._plot_layout.addWidget(self.toolbar) + self._plot_layout.addWidget(self.canvas) + layout.addLayout(self._plot_layout, 1) + + self.plot = PlotXY( + canvas=self.canvas, + data=None, + trad=self._trad, + toolbar=self.toolbar, + parent=self + ) + + @property + def study(self): + return self._study + + @study.setter + def study(self, study): + self._study = study + + def update(self): + if (self._graph_widget is not None + and (self._study is None + or self._graph_widget.graph is not self._study.river)): + self._graph_layout.removeWidget(self._graph_widget) + self._graph_widget.deleteLater() + self._graph_widget = None + + if self._study is not None and self._graph_widget is None: + self._graph_widget = GraphWidget( + self._study.river, + parent=self, + only_display=True, + trad=self._trad, + ) + self._graph_layout.addWidget(self._graph_widget) + + data = None + geotiff = None + if self._study is not None: + data = self._study.river.enable_edges() + geotiff = self._study.river.geotiff + + self.plot = PlotXY( + canvas=self.canvas, + data=data, + geotiff=geotiff, + trad=self._trad, + toolbar=self.toolbar, + parent=self + ) + self.plot.update() \ No newline at end of file diff --git a/src/View/Translate.py b/src/View/Translate.py index 5a7f20b0..fbf0687c 100644 --- a/src/View/Translate.py +++ b/src/View/Translate.py @@ -175,6 +175,9 @@ class MainTranslate(UnitTranslate): self._dict["tab_checker_name"] = _translate( "MainWindow", "Checks" ) + self._dict["tab_graph_name"] = _translate( + "MainWindow", "Graph" + ) self._dict["open_debug"] = _translate( "MainWindow", "Open debug window" From 97f487c0af52d813ebd766cfba7dee4af8dd8fac Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 7 Sep 2026 17:06:24 +0200 Subject: [PATCH 2/7] Design main window: Network graph is now connected with the edition window to refresh after every change --- src/View/MainWindow.py | 3 +++ src/View/MainWindowTabGraph.py | 4 +++- src/View/Network/GraphWidget.py | 7 +++++++ src/View/Network/Window.py | 3 +++ 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 06619bde..aff3a6a8 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -1306,6 +1306,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): return self.network = NetworkWindow(study=self._study, parent=self) + self.network.networkChanged.connect( + self._tab_widget_graph.update, Qt.QueuedConnection + ) self.network.show() def open_scenarios(self): diff --git a/src/View/MainWindowTabGraph.py b/src/View/MainWindowTabGraph.py index 88244d8c..41f3401f 100644 --- a/src/View/MainWindowTabGraph.py +++ b/src/View/MainWindowTabGraph.py @@ -74,6 +74,8 @@ class WidgetGraph(QWidget): trad=self._trad, ) self._graph_layout.addWidget(self._graph_widget) + elif self._graph_widget is not None: + self._graph_widget.display_update() data = None geotiff = None @@ -89,4 +91,4 @@ class WidgetGraph(QWidget): toolbar=self.toolbar, parent=self ) - self.plot.update() \ No newline at end of file + self.plot.update() diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index 263ab957..3c008c41 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -667,6 +667,13 @@ class GraphWidget(QGraphicsView): Returns: Nothing """ + # Scene items are destroyed by clear(); do not retain hover/selection + # references to them when refreshing another view of the same graph. + self._selected_item = None + self._selected_new_edge_src_node = None + self._current_edge = None + self._current_moved_node = None + self.tmp_line = None self.scene().clear() self.create_items() diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index 4934c53d..63797bee 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -58,6 +58,8 @@ _translate = QCoreApplication.translate class NetworkWindow(PamhyrWindow): + networkChanged = pyqtSignal() + _pamhyr_ui = "Network" _pamhyr_name = "River network" @@ -140,6 +142,7 @@ class NetworkWindow(PamhyrWindow): self._graph_layout.addWidget(self._graph_widget) def setup_connections(self): + self._undo_stack.indexChanged.connect(self.networkChanged) self._nodes_model.dataChanged.connect(self.update) self._reachs_model.dataChanged.connect( self._graph_widget.display_update) From f41050fe5c4c346964d6317d584e7a41e5b8ca62 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Mon, 7 Sep 2026 17:37:52 +0200 Subject: [PATCH 3/7] Design main window: UI improvement for the graph tab, QBox, titles... --- src/View/MainWindowTabGraph.py | 35 +++++++++++++++++++++++++++------- src/View/Translate.py | 6 ++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/View/MainWindowTabGraph.py b/src/View/MainWindowTabGraph.py index 41f3401f..048d8328 100644 --- a/src/View/MainWindowTabGraph.py +++ b/src/View/MainWindowTabGraph.py @@ -13,7 +13,8 @@ from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.PlotXY import PlotXY from View.Network.GraphWidget import GraphWidget -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QWidget, QGroupBox, QLabel, QHBoxLayout, QVBoxLayout class WidgetGraph(QWidget): @@ -28,8 +29,18 @@ class WidgetGraph(QWidget): layout = QHBoxLayout(self) self._graph_widget = None - self._graph_layout = QVBoxLayout() - layout.addLayout(self._graph_layout, 1) + self._graph_group = QGroupBox( + self._trad["graph_network_title"], self + ) + self._graph_layout = QVBoxLayout(self._graph_group) + + self._graph_title = QLabel(self._graph_group) + self._graph_title.setTextFormat(Qt.PlainText) + self._graph_title.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self._graph_title.setWordWrap(True) + self._graph_layout.addWidget(self._graph_title) + self._update_graph_title() + layout.addWidget(self._graph_group, 1) self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas_graph") @@ -37,10 +48,13 @@ class WidgetGraph(QWidget): self.canvas, self, items=["home", "zoom", "save", "iso", "back/forward", "move"] ) - self._plot_layout = QVBoxLayout() + self._plot_group = QGroupBox( + self._trad["graph_geographic_title"], self + ) + self._plot_layout = QVBoxLayout(self._plot_group) self._plot_layout.addWidget(self.toolbar) - self._plot_layout.addWidget(self.canvas) - layout.addLayout(self._plot_layout, 1) + self._plot_layout.addWidget(self.canvas, 1) + layout.addWidget(self._plot_group, 1) self.plot = PlotXY( canvas=self.canvas, @@ -57,8 +71,15 @@ class WidgetGraph(QWidget): @study.setter def study(self, study): self._study = study + self._update_graph_title() + + def _update_graph_title(self): + self._graph_title.setText( + self._study.name if self._study is not None else "" + ) def update(self): + self._update_graph_title() if (self._graph_widget is not None and (self._study is None or self._graph_widget.graph is not self._study.river)): @@ -73,7 +94,7 @@ class WidgetGraph(QWidget): only_display=True, trad=self._trad, ) - self._graph_layout.addWidget(self._graph_widget) + self._graph_layout.addWidget(self._graph_widget, 1) elif self._graph_widget is not None: self._graph_widget.display_update() diff --git a/src/View/Translate.py b/src/View/Translate.py index fbf0687c..40f8efd8 100644 --- a/src/View/Translate.py +++ b/src/View/Translate.py @@ -178,6 +178,12 @@ class MainTranslate(UnitTranslate): self._dict["tab_graph_name"] = _translate( "MainWindow", "Graph" ) + self._dict["graph_network_title"] = _translate( + "MainWindow", "Hydrographic network" + ) + self._dict["graph_geographic_title"] = _translate( + "MainWindow", "Geographic view" + ) self._dict["open_debug"] = _translate( "MainWindow", "Open debug window" From fc40552d68b9d717d469c17b64ac54bcf491b4e7 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Tue, 8 Sep 2026 13:28:51 +0200 Subject: [PATCH 4/7] View main window: add a splitter to resize both network graph and geometry graph --- src/View/MainWindowTabGraph.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/View/MainWindowTabGraph.py b/src/View/MainWindowTabGraph.py index 048d8328..8da4968a 100644 --- a/src/View/MainWindowTabGraph.py +++ b/src/View/MainWindowTabGraph.py @@ -14,7 +14,9 @@ from View.PlotXY import PlotXY from View.Network.GraphWidget import GraphWidget from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QWidget, QGroupBox, QLabel, QHBoxLayout, QVBoxLayout +from PyQt5.QtWidgets import ( + QWidget, QGroupBox, QLabel, QHBoxLayout, QVBoxLayout, QSplitter, +) class WidgetGraph(QWidget): @@ -27,6 +29,9 @@ class WidgetGraph(QWidget): self._trad = trad layout = QHBoxLayout(self) + self._splitter = QSplitter(Qt.Horizontal, self) + self._splitter.setChildrenCollapsible(False) + layout.addWidget(self._splitter) self._graph_widget = None self._graph_group = QGroupBox( @@ -40,7 +45,7 @@ class WidgetGraph(QWidget): self._graph_title.setWordWrap(True) self._graph_layout.addWidget(self._graph_title) self._update_graph_title() - layout.addWidget(self._graph_group, 1) + self._splitter.addWidget(self._graph_group) self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas_graph") @@ -54,7 +59,10 @@ class WidgetGraph(QWidget): self._plot_layout = QVBoxLayout(self._plot_group) self._plot_layout.addWidget(self.toolbar) self._plot_layout.addWidget(self.canvas, 1) - layout.addWidget(self._plot_group, 1) + self._splitter.addWidget(self._plot_group) + self._splitter.setStretchFactor(0, 1) + self._splitter.setStretchFactor(1, 1) + self._splitter.setSizes([1000, 1000]) self.plot = PlotXY( canvas=self.canvas, From bcc9dd1a0df795bc8c829a0a700e9601468c0058 Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Tue, 8 Sep 2026 14:02:53 +0200 Subject: [PATCH 5/7] View main window: keep zoom on the geographical graph after a change in the geometry --- src/View/MainWindowTabGraph.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/View/MainWindowTabGraph.py b/src/View/MainWindowTabGraph.py index 8da4968a..5d4b9e15 100644 --- a/src/View/MainWindowTabGraph.py +++ b/src/View/MainWindowTabGraph.py @@ -27,6 +27,7 @@ class WidgetGraph(QWidget): self._study = study self._trad = trad + self._plot_river = None layout = QHBoxLayout(self) self._splitter = QSplitter(Qt.Horizontal, self) @@ -87,6 +88,14 @@ class WidgetGraph(QWidget): ) def update(self): + river = self._study.river if self._study is not None else None + view = None + if river is not None and river is self._plot_river: + axes = self.canvas.axes + if axes.has_data(): + view = (axes.get_xlim(), axes.get_ylim(), + axes.get_aspect(), axes.get_adjustable()) + self._update_graph_title() if (self._graph_widget is not None and (self._study is None @@ -121,3 +130,10 @@ class WidgetGraph(QWidget): parent=self ) self.plot.update() + self._plot_river = river + if view is not None: + x_limits, y_limits, aspect, adjustable = view + self.canvas.axes.set_aspect(aspect, adjustable=adjustable) + self.canvas.axes.set_xlim(x_limits) + self.canvas.axes.set_ylim(y_limits) + self.canvas.draw_idle() From 9f46bcd853d9e5d1659433f8d6420af32a6f4a2c Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Tue, 8 Sep 2026 15:42:04 +0200 Subject: [PATCH 6/7] View main window: fix refresh network preview when selecting a reach --- src/View/Network/GraphWidget.py | 4 ++++ src/View/Network/Window.py | 1 + 2 files changed, 5 insertions(+) diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index 3c008c41..67b95985 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -448,6 +448,7 @@ class NewEdgeLine(QGraphicsItem): class GraphWidget(QGraphicsView): changeEdge = pyqtSignal(object) changeNode = pyqtSignal(object) + currentEdgeChanged = pyqtSignal() def __init__(self, graph, parent=None, min_size=(400, 400), max_size=None, @@ -882,6 +883,9 @@ class GraphWidget(QGraphicsView): if previous_edge: previous_edge.update() + edge.update() + if previous_edge is not edge: + self.currentEdgeChanged.emit() except Exception as e: logger.warning(str(e)) diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index 63797bee..4bf93052 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -143,6 +143,7 @@ class NetworkWindow(PamhyrWindow): def setup_connections(self): self._undo_stack.indexChanged.connect(self.networkChanged) + self._graph_widget.currentEdgeChanged.connect(self.networkChanged) self._nodes_model.dataChanged.connect(self.update) self._reachs_model.dataChanged.connect( self._graph_widget.display_update) From 74d23386a7b023f17636e413397a792f67aef28a Mon Sep 17 00:00:00 2001 From: Dylan Jeannin Date: Tue, 8 Sep 2026 17:31:01 +0200 Subject: [PATCH 7/7] Network: cleanest version of drag clic to move on the graph + network graph view only on the main tab (no more interaction on mouse hover) + graph on main tab --- src/View/MainWindow.py | 5 ++-- src/View/Network/GraphWidget.py | 53 ++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index aff3a6a8..7b407ce1 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -270,10 +270,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): study=self._study, trad=self._trad, parent=self ) - tab_widget.addTab( - self._tab_widget_graph, + tab_widget.insertTab( + 0, self._tab_widget_graph, self._trad["tab_graph_name"] ) + tab_widget.setCurrentWidget(self._tab_widget_graph) def enable_actions(self, action: str, enable: bool): """Enable of disable an action componant diff --git a/src/View/Network/GraphWidget.py b/src/View/Network/GraphWidget.py index 67b95985..038e4a4e 100644 --- a/src/View/Network/GraphWidget.py +++ b/src/View/Network/GraphWidget.py @@ -477,6 +477,7 @@ class GraphWidget(QGraphicsView): self.m_origin_x = 0.0 self.m_origin_y = 0.0 self.clicked = False + self._panning = False self.setup_scene(min_size, max_size, size) @@ -492,8 +493,8 @@ class GraphWidget(QGraphicsView): self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorViewCenter) - self.scale(1, 1) - self.previousScale = 1 + self.scale(0.7, 0.7) + self.previousScale = 0.7 self.centerOn(1000.0, 1000.0) if min_size: @@ -948,6 +949,8 @@ class GraphWidget(QGraphicsView): def mousePressEvent(self, event): if self._only_display: + if event.button() == Qt.LeftButton: + self._start_pan(event) return if event.button() == Qt.RightButton \ @@ -970,6 +973,12 @@ class GraphWidget(QGraphicsView): self._selected_new_edge_src_node = None items = self.items(event.pos()) + if event.button() == Qt.LeftButton and not any( + isinstance(item, (NodeItem, EdgeItem, NodeText)) + for item in items + ): + self._start_pan(event) + return if items and type(items[0]) is EdgeItem: edge = items[0] if edge: @@ -1018,7 +1027,10 @@ class GraphWidget(QGraphicsView): def mouseReleaseEvent(self, event): self.clicked = False - if self._only_display: + if self._panning or self._only_display: + self._panning = False + self.viewport().unsetCursor() + event.accept() return locked = self.graph._status.is_read_only() @@ -1040,7 +1052,28 @@ class GraphWidget(QGraphicsView): self.update() super(GraphWidget, self).mouseReleaseEvent(event) + def _start_pan(self, event): + self.clicked = True + self._panning = True + self.m_origin_x = event.x() + self.m_origin_y = event.y() + self.viewport().setCursor(Qt.ClosedHandCursor) + event.accept() + def mouseMoveEvent(self, event): + if self._panning or self._only_display: + if self._panning and event.buttons() & Qt.LeftButton: + dx = event.x() - self.m_origin_x + dy = event.y() - self.m_origin_y + horizontal = self.horizontalScrollBar() + vertical = self.verticalScrollBar() + horizontal.setValue(horizontal.value() - dx) + vertical.setValue(vertical.value() - dy) + self.m_origin_x = event.x() + self.m_origin_y = event.y() + event.accept() + return + pos = self.mapToScene(event.pos()) locked = self.graph._status.is_read_only() @@ -1072,20 +1105,6 @@ class GraphWidget(QGraphicsView): # If state is "move" if self._state == "move": - # Move on scene - if not self.selected_item(): - if event.buttons() & Qt.LeftButton: - old_p = self.mapToScene( - int(self.m_origin_x), int(self.m_origin_y) - ) - new_p = self.mapToScene(event.pos()) - translation = new_p - old_p - - self.translate(translation.x(), translation.y()) - - self.m_origin_x = event.x() - self.m_origin_y = event.y() - # Propagate event self.update() super(GraphWidget, self).mouseMoveEvent(event)