From 6c9481296b2aa8c48ccdc0719cc7f26e9cad624a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 20 Sep 2023 15:21:46 +0200 Subject: [PATCH 01/44] doc: dev: Add SQL section with examples. --- doc/dev/documentation.org | 321 +++++++++++++++++++++++++++++--------- 1 file changed, 245 insertions(+), 76 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index e245b959..56b45032 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -139,101 +139,270 @@ https://doc.qt.io/qt-5/model-view-programming.html ** TODO Model -- Model de donnée Python -- Correspond à une sauvegarde SQL +The model is a set of Python classes. In Pamhyr2, this classes must +respect some constraint. Each model class must inherits +=Model.Tools.SQLSubModel= abstract class, except the =Model.Study= +class who inherits =Model.Tools.SQLModel= (see [[SQL]]). + +The model entry point is the Study class. It contains infomation about +the study: their name, description, time system, and so on. Their +contains a River object too. This river object inherits the network +graph and contains a list of =RiverNode= and a list of =RiverReach= +(an edge who contains a source node, and destination node). +=RiverReach= contrains geometry, so, the river network (node and edge) +associated with the geometry forms the basis of the model, and the +other components are linked to one of these basic components. #+name: graph-model #+header: :results drawer #+header: :exports results -#+header: :post attr_wrap(width="16cm", data=*this*, name="graph-model", caption="Pamhyr2 model class dependencies", float="t") +#+header: :post attr_wrap(width="16cm", data=*this*, name="graph-model", caption="Pamhyr2 model class dependencies (A -> B means A can contain references to B)", float="t") #+begin_src dot :file "images/graph-model.png" :cache no digraph { - bgcolor="transparent"; - node[colorscheme=set19,shape=box,style="filled",fillcolor="2"]; + bgcolor="transparent"; + node[colorscheme=set19,shape=box,style="filled",fillcolor="2"]; - study[label="Study"]; - river[label="River"]; + subgraph cluster0 { + style=dashed; + study[label="Study"]; + river[label="River"]; - subgraph cluster00 { - label="Network" - rnode[label="RiverNode"]; - redge[label="RiverReach"]; - } + subgraph cluster00 { + style=solid; + label="Network" + rnode[label="RiverNode"]; + redge[label="RiverReach"]; + } - frictionlist[label="FrictionList"]; + subgraph cluster06 { + style=solid; + label="Greometry" + georeach[label="Reach"]; + geocrosssection[label="Cross-section"]; + geopoint[label="Point"]; + } + } - subgraph cluster01 { - label="Stricklers"; - stricklers[label="Stricklers"]; - stricklerslist[label="StricklersList"]; - } - subgraph cluster02 { - label="BoundaryCondition"; - boundaryconditionlist[label="BoundaryConditionList"]; - boundarycondition[label="BoundaryCondition"]; - } - subgraph cluster03 { - label="LateralContribution"; - lateralcontributionlist[label="LateralContributionList"]; - lateralcontribution[label="LateralContribution"]; - } - subgraph cluster04 { - label="InitialConditions"; - initialconditionsdict[label="InitialConditionsDict"]; - initialconditions[label="InitialConditions"]; - } + //subgraph cluster1 { + // style=dashed; + frictionlist[label="FrictionList"]; - solverparameterslist[label="SolverParametersList"]; + subgraph cluster01 { + style=solid; + label="Stricklers"; + stricklers[label="Stricklers"]; + stricklerslist[label="StricklersList"]; + } + subgraph cluster02 { + style=solid; + label="BoundaryCondition"; + boundaryconditionlist[label="BoundaryConditionList"]; + boundarycondition[label="BoundaryCondition"]; + } + subgraph cluster03 { + style=solid; + label="LateralContribution"; + lateralcontributionlist[label="LateralContributionList"]; + lateralcontribution[label="LateralContribution"]; + } + subgraph cluster04 { + style=solid; + label="InitialConditions"; + initialconditionsdict[label="InitialConditionsDict"]; + initialconditions[label="InitialConditions"]; + } - subgraph cluster05 { - label="Sediment"; - sedimentlayerlist[label="SedimentLayerList"]; - sedimentlayer[label="SedimentLayer"]; - layer[label="Layer"]; - } + solverparameterslist[label="SolverParametersList"]; - subgraph cluster06 { - label="Greometry" - georeach[label="Reach"]; - geocrosssection[label="Cross-section"]; - geopoint[label="Point"]; - } + subgraph cluster05 { + style=solid; + label="Sediment"; + sedimentlayerlist[label="SedimentLayerList"]; + sedimentlayer[label="SedimentLayer"]; + layer[label="Layer"]; + } + //} - subgraph cluster07 { - label="Results" - results[label="Results"] - rriver[label="River"]; - rreach[label="Reach"]; - rcrosssection[label="Cross-section"]; - } + subgraph cluster2 { + style=dashed; + label="Results" + results[label="Results"] + rriver[label="River"]; + rreach[label="Reach"]; + rcrosssection[label="Cross-section"]; + } - study -> river; - river -> rnode; - river -> redge; - redge -> rnode; - river -> boundaryconditionlist -> boundarycondition -> rnode; - river -> lateralcontributionlist -> lateralcontribution -> redge; - river -> initialconditionsdict -> initialconditions; - initialconditions -> redge; - river -> stricklerslist -> stricklers; - river -> solverparameterslist; - river -> sedimentlayerlist -> sedimentlayer -> layer; - redge -> frictionlist -> stricklers; - redge -> georeach -> geocrosssection -> geopoint; - geocrosssection -> sedimentlayer; - geopoint -> sedimentlayer; + study -> river; + river -> rnode; + river -> redge; + redge -> rnode; + river -> boundaryconditionlist -> boundarycondition -> rnode; + river -> lateralcontributionlist -> lateralcontribution -> redge; + river -> initialconditionsdict -> initialconditions; + initialconditions -> redge; + river -> stricklerslist -> stricklers; + river -> solverparameterslist; + river -> sedimentlayerlist -> sedimentlayer -> layer; + redge -> frictionlist -> stricklers; + redge -> georeach -> geocrosssection -> geopoint; + geocrosssection -> sedimentlayer; + geopoint -> sedimentlayer; - results -> study[style="dashed"]; - results -> rriver; - rriver -> river[style="dashed"]; - rriver -> rreach; - rreach -> georeach[style="dashed"]; - rreach -> rcrosssection; - rcrosssection -> geocrosssection[style="dashed"]; + results -> study; + results -> rriver; + rriver -> river; + rriver -> rreach; + rreach -> georeach; + rreach -> rcrosssection; + rcrosssection -> geocrosssection; } #+end_src -*** TODO SQL study file +*** TODO SQL + +The model must be export to a database file to create a study save +file. This file use SQLite3[fn:sqlite] format and the extention +=.pamhyr=. So, each model componante must be register into this study +file. To create, update, set and get information into SQLite database +we use SQL command. The database use version number and some +modification could be perform to update database. For each model +componante, correspond one or more SQL table to store information. To +normalize the interaction with database we made two classes, SQLModel +and SQLSubModel. The Study class use SQLModel because is the top of +the model hierachy. The rest of model class inherits to SQLSubModel. + +A class who inherits SQLSubModel, must implement some methods: +- =_sql_create=: Class method to create the database scheme +- =_sql_update=: Class method to update the database scheme if necessary +- =_sql_load=: Class method to load data from DB +- =_sql_save=: Method to save current object into DB + +Class method take in arguments: The class (=cls=), a function to +execute SQL command into the database (=execute=). In addition, the +update method take the previous version of database, load method take +an optional arguments =data= if additional infomation ar needed, and +who can contains whatever you want. The method save take in arguments +the current object (=self=), a function to execute SQL command into +the database (=execute=), and optional data (=data=). + +The class who inherits SQLSubModel can also define an class attribute +=_sub_classes= to set a formal class dependencies into database. This +attribute is use at database creation to create all table, and at +update to update all the database table. Let see examples of +SQLSubModel usage for two classes Foo and Bar with Foo contains list +of Bar (Listing [[sql-bar]] and [[sql-foo]]). + +#+NAME: sql-bar +#+CAPTION: Exemple of class Bar inherits SQLSubModel. +#+begin_src python :python python3 :results output :noweb yes + from Model.Tools.PamhyrDB import SQLSubModel + + class Bar(SQLSubModel): + _id_cnt = 0 + def __init__(self, id = -1, x = 0, y = 0): + self._x = x + self._y = y + if id == -1: + self.id = Bar._id_cnt + 1 + else: + self.id = id + Bar._id_cnt = max(id, Bar._id_cnt+1) + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE bar ( + id INTEGER NOT NULL PRIMARY KEY, + x INTEGER NOT NULL, + foo_id INTEGER NOT NULL, + FOREIGN KEY(foo_id) REFERENCES foo(id), + )""") + return True + + @classmethod + def _sql_update(cls, execute, version): + # If version is lesser than 0.0.2, add column to bar table + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 2: + execute("ALTER TABLE bar ADD COLUMN y INTEGER") + return True + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + table = execute( + f"SELECT id, x, y FROM bar WHERE foo_id = {data['id']}" + ) + for row in table: + bar = cls( + id = row[0], x = row[1], y = row[2], + ) + new.append(bar) + return new + + def _sql_save(self, execute, data = None): + execute("INSERT INTO bar (id,x,y,foo_id) VALUES " + + f"({self.id}, {self._x}, {self._y}, {data['id']})") +#+end_src + +#+NAME: sql-foo +#+CAPTION: Exemple of class Foo inherits SQLSubModel. +#+begin_src python :python python3 :results output :noweb yes + class Foo(SQLSubModel): + _id_cnt = 0 + _sub_classes = [Bar] + + def __init__(self, id = -1, name = ""): + self._name = name + self._bar = [] + # ... + + @classmethod + def _sql_create(cls, execute): + execute(""" + CREATE TABLE foo ( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + ) + """) + return cls._create_submodel(execute) + + @classmethod + def _sql_update(cls, excute, version): + return cls._update_submodel(execute, version) + + @classmethod + def _sql_load(cls, execute, data = None): + new = [] + table = execute( + "SELECT id, name FROM foo" + ) + for row in table: + foo = cls( + id = row[0], + name = row[1], + ) + data = { + "id": row[0], # Current Foo ID + } + foo._bar = Bar._sql_load(execute, data=data) + new.append(foo) + return new + + def _sql_save(self, execute, data = None): + execute(f"DELETE FROM foo WHERE id = {self.id}") + execute(f"DELETE FROM bar WHERE foo_id = {self.id}") + # Save new data + execute(f"INSERT INTO bar (id,name) VALUES ({self.id}, {self._name})") + data = {"id": self.id} + for bar in self._bar: + bar._sql_save(execute, data=data) +#+end_src + +[fn:sqlite] The SQLite web site: https://www.sqlite.org/index.html +(last access 2023-09-20) + *** TODO List class *** TODO Dict class ** TODO View From 42dfdcc3b1fcc67a4a893f935779e3caaaa1650d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 20 Sep 2023 15:32:04 +0200 Subject: [PATCH 02/44] doc: dev: Complete Model section. --- doc/dev/documentation.org | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 56b45032..0279790c 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -137,7 +137,7 @@ in {{{file(src/Solver)}}} (see section [[Solver]]). [fn:qt-mv] The Qt Model/View documentation web page: https://doc.qt.io/qt-5/model-view-programming.html -** TODO Model +** Model The model is a set of Python classes. In Pamhyr2, this classes must respect some constraint. Each model class must inherits @@ -258,7 +258,7 @@ other components are linked to one of these basic components. } #+end_src -*** TODO SQL +*** SQL The model must be export to a database file to create a study save file. This file use SQLite3[fn:sqlite] format and the extention @@ -403,8 +403,22 @@ of Bar (Listing [[sql-bar]] and [[sql-foo]]). [fn:sqlite] The SQLite web site: https://www.sqlite.org/index.html (last access 2023-09-20) -*** TODO List class -*** TODO Dict class +*** List class + +A abstract class PamhyrModelList is available and provide some of +basic methods for object list in Model. This abstract class implement +some classic action in View like: insert new object, delete object, +sort, move object up, move object down, and so on. An variant exists +for multiple list with same type of object, each sublist is called +tab, because in View, this kind of list si prensented in different +table PamhyrModelListWithTab. + +*** Dict class + +A abstract class PamhyrModelDict is available and provide some of +basic methods for object dictionary in Model. This class is like +PamhyrModelList but use a dictionary instead of list. + ** TODO View *** TODO UI file *** TODO Translate From afa21c90576d882166668bd70b4a1b9d3d2add52 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 20 Sep 2023 16:46:37 +0200 Subject: [PATCH 03/44] doc: dev: View section (UI, window). --- doc/dev/documentation.org | 118 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 5 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 0279790c..16d6a69c 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -347,7 +347,7 @@ of Bar (Listing [[sql-bar]] and [[sql-foo]]). #+end_src #+NAME: sql-foo -#+CAPTION: Exemple of class Foo inherits SQLSubModel. +#+CAPTION: Exemple of class Foo inherits SQLSubModel and contains a list of Bar object (Listing [[sql-bar]]). #+begin_src python :python python3 :results output :noweb yes class Foo(SQLSubModel): _id_cnt = 0 @@ -420,11 +420,119 @@ basic methods for object dictionary in Model. This class is like PamhyrModelList but use a dictionary instead of list. ** TODO View -*** TODO UI file -*** TODO Translate -*** TODO Window + +Pamhyr2 use Qt as graphical user interface library with the +application "Qt designer" for windows or widget creation (see [[UI file]]) +and "Qt linguist" for interface translate (see [[Translate]]). In +addition, we use matplotlib as ploting library (see [[Plot]]). + +Typically, each model componant have an associated window in +application to add, delete or edit this componant. At top level of +View directory we found the =MainWindow.py= file and some +sub-directories. A view sub-directory contains: A =Window.py= file, a +=Table.py= file with table model definition if nessessary, one or more +=Plot*.py= file with plot class definition, a =translate.py= file with +componant translate, and possible other files or sub-directories. + +*** UI file + +We define as possible all Pamhyr2 windows and custom widgets with "Qt +designer". This application generate UI file who describes interface +organisation with table, layout, button, etc. This method is faster +than hand made windows and widget creation, and saves us some purely +descriptive code. The UI files are saved into =src/View/ui= for +window, and =/src/View/ui/Widgets= for custom widget. + +*** Window + +An abstract class ASubMainWindow is used for most of Pamhyr2 +windows. This class implemente some useful method allows to easy load +an window UI file. In addition, the abstract ListedSubWindow must be +also inherits by a window class, it is allow to keep a list of open +windows and avoid window duplicate. + +#+NAME: window +#+CAPTION: Example of Pamhyr2 window +#+begin_src python :python python3 :results output :noweb yes + class MyWindow(ASubMainWindow, ListedSubWindow): + def __init__(self, title="My window", + study=None, config=None, parent=None): + self._study = study + self._config = config + self._parent = parent + + self._title = title + " - " + study.name + + super(MyWindow, self).__init__( + name=title, # The windows name for windows + # registration (avoid duplicate) + ui="My", # The ui file name (here "My.ui") + parent=parent # The parent window + ) + # Set Windows title + self.ui.setWindowTitle(self._title) + + self.setup_table() + self.setup_connections() + + def setup_table(self): + # Init table(s)... + + def setup_connections(self): + # Init action connection(s)... + + # ... +#+end_src + +Typically we called method =setup_*=, the method to initialize some +window componants or connections. + +*** Table + +An abstract class PamhyrTableModel is available to define a simple +QAbstractTableModel shortly. In simple cases, there are only =data= +and =setData= methode to implement, but the constructor needs more +information than a classic QAbstractTableModel class. + +#+begin_src python :python python3 :results output :noweb yes + # Table headers with display name translatable (translate.py) + table_headers = { + "foo": _translate("My", "Foo"), + "bar": _translate("My", "Bar"), + "baz": _translate("My", "Baz"), + } + + def retranslate(): + # Hack because dict definition is not able to be + # translate... (need to be called in window '__init__') + table_headers["foo"] = _translate("My", "Foo") + table_headers["bar"] = _translate("My", "Bar") + table_headers["baz"] = _translate("My", "Baz") + + # Table model definition (Table.py) + class MyTableModel(PamhyrTableModel): + def data(self, index, role): + # Retrun data at INDEX... + + @pyqtSlot() + def setData(self, index, value, role=Qt.EditRole): + # Set VALUE at INDEX... + + # Table model creation (Window.py) + self._model = MyTableModel( + table_view = table, # The table view object + table_headers = table_headers, # The table column headers dict + # (with traduction) + editable_headers = ["foo", "bar"], # List of editable column name + delegates = { + "bar": self.my_delegate, # Custom delegate for column 'bar' + }, + data = self._my_lst, # The data + undo = self._undo_stack, # The undo command stack + ) +#+end_src + *** TODO UndoCommand -*** TODO Table *** TODO Plot *** TODO Mainwindow ** TODO Solver From edc5b65234ad64a6cc123b94c68bab8a138248bf Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:03:07 +0200 Subject: [PATCH 04/44] Scripts: Convert plot_3DST.py to Pamhyr2 script. --- src/Model/Geometry/Reach.py | 2 +- src/Scripts/Hello.py | 2 +- src/Scripts/P3DST.py | 115 ++++++++++++++++++++++++++++++++++++ src/Scripts/plot_3DST.py | 68 --------------------- src/pamhyr.py | 2 + 5 files changed, 119 insertions(+), 70 deletions(-) create mode 100644 src/Scripts/P3DST.py delete mode 100644 src/Scripts/plot_3DST.py diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 0c641478..26cdefc9 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -42,7 +42,7 @@ class Reach(SQLSubModel): ] def __init__(self, status=None, parent=None): - self.id = parent.id + self.id = parent.id if parent is not None else 0 self._status = status self._parent = parent self._profiles: List[Profile] = [] diff --git a/src/Scripts/Hello.py b/src/Scripts/Hello.py index b1c27fe5..c7333b84 100644 --- a/src/Scripts/Hello.py +++ b/src/Scripts/Hello.py @@ -1,4 +1,4 @@ -# help.py -- Pamhyr +# Hello.py -- Pamhyr # Copyright (C) 2023 INRAE # # This program is free software: you can redistribute it and/or modify diff --git a/src/Scripts/P3DST.py b/src/Scripts/P3DST.py new file mode 100644 index 00000000..55a99928 --- /dev/null +++ b/src/Scripts/P3DST.py @@ -0,0 +1,115 @@ +# P3DST.py -- Pamhyr +# Copyright (C) 2023 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import sys +import logging + +from matplotlib import pyplot as plt +from numpy import mean + +from Scripts.AScript import AScript + +from Model.Saved import SavedStatus +from Model.Geometry.Reach import Reach + +logger = logging.getLogger() + +class Script3DST(AScript): + name = "3DST" + description = "Display a 3D plot of a river reach from ST file" + + def usage(self): + logger.info(f"Usage : {self._args[0]} 3DST ") + + def set_axes_equal(self, ax): + """Make axes of 3D plot have equal scale + + Make axes of 3D plot have equal scale so that spheres appear + as spheres, cubes as cubes, etc.. This is one possible + solution to Matplotlib's ax.set_aspect('equal') and + ax.axis('equal') not working for 3D. + + Args: + ax: a matplotlib axis, e.g., as output from plt.gca() + + Returns: + The input axis + """ + x_limits = ax.get_xlim3d() + y_limits = ax.get_ylim3d() + z_limits = ax.get_zlim3d() + + x_range = abs(x_limits[1] - x_limits[0]) + x_middle = mean(x_limits) + y_range = abs(y_limits[1] - y_limits[0]) + y_middle = mean(y_limits) + z_range = abs(z_limits[1] - z_limits[0]) + z_middle = mean(z_limits) + + # The plot bounding box is a sphere in the sense of the infinity + # norm, hence I call half the max range the plot radius. + plot_radius = 0.5*max([x_range, y_range, z_range]) + + ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius]) + ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) + ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius]) + + return ax + + def run(self): + try: + st_file = self._args[2] + logger.info(f"Use ST file: {st_file}") + except Exception as e: + logger.error(f"Arguments parcing: {str(e)}") + return 1 + + try: + status = SavedStatus() + + my_reach = Reach(status = status) + my_reach.import_geometry(st_file) + my_reach.compute_guidelines() + + ax = plt.figure().add_subplot(projection="3d") + logger.info(my_reach.get_x()) + for x, y, z in zip( + my_reach.get_x(), + my_reach.get_y(), + my_reach.get_z() + ): + ax.plot(x, y, z, color='r', lw=1.) + + for x, y, z in zip( + my_reach.get_guidelines_x(), + my_reach.get_guidelines_y(), + my_reach.get_guidelines_z() + ): + ax.plot(x, y, z, color='b', lw=1.) + + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_zlabel('Z') + plt.tight_layout() + self.set_axes_equal(ax) + plt.show() + + return 0 + except Exception as e: + logger.error(str(e)) + return 1 diff --git a/src/Scripts/plot_3DST.py b/src/Scripts/plot_3DST.py deleted file mode 100644 index bcadf1f1..00000000 --- a/src/Scripts/plot_3DST.py +++ /dev/null @@ -1,68 +0,0 @@ -# plot_3DST.py -- Pamhyr -# Copyright (C) 2023 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. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# -*- coding: utf-8 -*- - -# a lancer depuis src -import sys -from matplotlib import pyplot as plt -from Model.Geometry.Reach import Reach -from numpy import mean - -def set_axes_equal(ax): - '''Make axes of 3D plot have equal scale so that spheres appear as spheres, - cubes as cubes, etc.. This is one possible solution to Matplotlib's - ax.set_aspect('equal') and ax.axis('equal') not working for 3D. - - Input - ax: a matplotlib axis, e.g., as output from plt.gca(). - ''' - - x_limits = ax.get_xlim3d() - y_limits = ax.get_ylim3d() - z_limits = ax.get_zlim3d() - - x_range = abs(x_limits[1] - x_limits[0]) - x_middle = mean(x_limits) - y_range = abs(y_limits[1] - y_limits[0]) - y_middle = mean(y_limits) - z_range = abs(z_limits[1] - z_limits[0]) - z_middle = mean(z_limits) - - # The plot bounding box is a sphere in the sense of the infinity - # norm, hence I call half the max range the plot radius. - plot_radius = 0.5*max([x_range, y_range, z_range]) - - ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius]) - ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius]) - ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius]) - -st_file = sys.argv[1] -my_reach = Reach(None) -my_reach.import_geometry(st_file) -my_reach.compute_guidelines() - -ax = plt.figure().add_subplot(projection="3d") -for x, y, z in zip(my_reach.get_x(), my_reach.get_y(), my_reach.get_z()): - ax.plot(x, y, z, color='r', lw=1.) -for x, y, z in zip(my_reach.get_guidelines_x(), my_reach.get_guidelines_y(), my_reach.get_guidelines_z()): - ax.plot(x, y, z, color='b', lw=1.) -ax.set_xlabel('X') -ax.set_ylabel('Y') -ax.set_zlabel('Z') -plt.tight_layout() -set_axes_equal(ax) -plt.show() diff --git a/src/pamhyr.py b/src/pamhyr.py index b4bd401f..0d595df4 100755 --- a/src/pamhyr.py +++ b/src/pamhyr.py @@ -34,6 +34,7 @@ from tools import ( from View.MainWindow import ApplicationWindow from Model.Study import Study +from Scripts.P3DST import Script3DST from Scripts.Hello import ScriptHello from init import license, setup_lang @@ -42,6 +43,7 @@ logger = logging.getLogger() scripts = { "hello": ScriptHello, + "3DST": Script3DST, } def usage(argv): From 2bb783b5d49cdd095cb0a9e5b533a61cebe8b291 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:09:55 +0200 Subject: [PATCH 05/44] ci: Add HTML documentation. --- .gitlab-ci.yml | 11 +++++++++++ doc/dev/build.sh | 11 +++++++++-- doc/users/build.sh | 11 +++++++++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5f12c3e7..7f2aecdf 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -109,9 +109,18 @@ build-developers-doc: script: - cd ./doc/dev/ - ./build.sh + # Create html documentation + - cd ../../src + - pdoc --html . + # Create archive + - tar cf html.tar ./html + - cd ../doc/dev + - mv ../../src/html.tar ./ artifacts: paths: - doc/dev/documentation.pdf + - doc/dev/documentation.html + - doc/dev/html.tar build-linux: stage: build @@ -167,8 +176,10 @@ build-linux: - cp ../tests_cases/Saar/Saar.pamhyr pamhyr/tests_cases/Saar/ # Copy documentations - mkdir -p pamhyr/doc/ + - mkdir -p pamhyr/doc/html/ - cp ../doc/users/documentation.pdf pamhyr/doc/Pamhyr2-users.pdf - cp ../doc/dev/documentation.pdf pamhyr/doc/Pamhyr2-dev.pdf + - cp ../doc/dev/documentation.html pamhyr/doc/html/dev.html # Create running script - echo "#!/bin/sh" > pamhyr/Pamhyr2 - echo "./pamhyr/pamhyr $@" >> pamhyr/Pamhyr2 diff --git a/doc/dev/build.sh b/doc/dev/build.sh index 476dd5ad..bf1a6790 100755 --- a/doc/dev/build.sh +++ b/doc/dev/build.sh @@ -1,10 +1,17 @@ #! /bin/sh -# Build the PDF - +echo " PDF documentation.org" emacs -Q -q -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-latex-export-to-pdf + +echo " HTML documentation.org" +emacs -Q -q -nw --batch \ + --eval "(require 'org)" \ + --eval "(require 'ox-latex)" \ + --eval "(load-file \"../tools/setup.el\")" \ + --file ./documentation.org \ + -f org-html-export-to-html diff --git a/doc/users/build.sh b/doc/users/build.sh index 476dd5ad..bf1a6790 100755 --- a/doc/users/build.sh +++ b/doc/users/build.sh @@ -1,10 +1,17 @@ #! /bin/sh -# Build the PDF - +echo " PDF documentation.org" emacs -Q -q -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-latex-export-to-pdf + +echo " HTML documentation.org" +emacs -Q -q -nw --batch \ + --eval "(require 'org)" \ + --eval "(require 'ox-latex)" \ + --eval "(load-file \"../tools/setup.el\")" \ + --file ./documentation.org \ + -f org-html-export-to-html From d461cb80019afd476e2cf7ea7e88258066be0158 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:26:44 +0200 Subject: [PATCH 06/44] doc: Fix htmlize into html build. --- doc/dev/build.sh | 3 +++ doc/users/build.sh | 3 +++ 2 files changed, 6 insertions(+) diff --git a/doc/dev/build.sh b/doc/dev/build.sh index bf1a6790..1afb7ca2 100755 --- a/doc/dev/build.sh +++ b/doc/dev/build.sh @@ -12,6 +12,9 @@ echo " HTML documentation.org" emacs -Q -q -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ + --eval "(require 'ox-html)" \ + --eval "(package-initialize)" \ + --eval "(require 'htmlize)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-html-export-to-html diff --git a/doc/users/build.sh b/doc/users/build.sh index bf1a6790..1afb7ca2 100755 --- a/doc/users/build.sh +++ b/doc/users/build.sh @@ -12,6 +12,9 @@ echo " HTML documentation.org" emacs -Q -q -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ + --eval "(require 'ox-html)" \ + --eval "(package-initialize)" \ + --eval "(require 'htmlize)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-html-export-to-html From 3842e689ac387dcbef3cb8f8eb68811d6b5ccea4 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:41:56 +0200 Subject: [PATCH 07/44] doc: Fix build package missing. --- doc/dev/build.sh | 6 ++---- doc/tools/setup.el | 12 ++++++++++++ doc/users/build.sh | 6 ++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/doc/dev/build.sh b/doc/dev/build.sh index 1afb7ca2..2e8deb56 100755 --- a/doc/dev/build.sh +++ b/doc/dev/build.sh @@ -1,7 +1,7 @@ #! /bin/sh echo " PDF documentation.org" -emacs -Q -q -nw --batch \ +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ @@ -9,12 +9,10 @@ emacs -Q -q -nw --batch \ -f org-latex-export-to-pdf echo " HTML documentation.org" -emacs -Q -q -nw --batch \ +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(require 'ox-html)" \ - --eval "(package-initialize)" \ - --eval "(require 'htmlize)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-html-export-to-html diff --git a/doc/tools/setup.el b/doc/tools/setup.el index 027e5700..e7d7b765 100644 --- a/doc/tools/setup.el +++ b/doc/tools/setup.el @@ -1,6 +1,18 @@ (require 'org) (require 'subr-x) +;; Needed packages + +(require 'package) +(add-to-list 'package-archives + '("melpa" . "https://melpa.org/packages/")) + +(package-initialize) + +;;; Install HTMLIZE +(package-install 'htmlize) +(require 'htmlize) + ;; LaTeX config (add-to-list diff --git a/doc/users/build.sh b/doc/users/build.sh index 1afb7ca2..2e8deb56 100755 --- a/doc/users/build.sh +++ b/doc/users/build.sh @@ -1,7 +1,7 @@ #! /bin/sh echo " PDF documentation.org" -emacs -Q -q -nw --batch \ +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(load-file \"../tools/setup.el\")" \ @@ -9,12 +9,10 @@ emacs -Q -q -nw --batch \ -f org-latex-export-to-pdf echo " HTML documentation.org" -emacs -Q -q -nw --batch \ +emacs -nw --batch \ --eval "(require 'org)" \ --eval "(require 'ox-latex)" \ --eval "(require 'ox-html)" \ - --eval "(package-initialize)" \ - --eval "(require 'htmlize)" \ --eval "(load-file \"../tools/setup.el\")" \ --file ./documentation.org \ -f org-html-export-to-html From 9c7f1f7ba939649cd27eac4ed943d7fc0aae5925 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 22 Sep 2023 11:51:00 +0200 Subject: [PATCH 08/44] doc: Try with system package. --- doc/tools/setup.el | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/doc/tools/setup.el b/doc/tools/setup.el index e7d7b765..027e5700 100644 --- a/doc/tools/setup.el +++ b/doc/tools/setup.el @@ -1,18 +1,6 @@ (require 'org) (require 'subr-x) -;; Needed packages - -(require 'package) -(add-to-list 'package-archives - '("melpa" . "https://melpa.org/packages/")) - -(package-initialize) - -;;; Install HTMLIZE -(package-install 'htmlize) -(require 'htmlize) - ;; LaTeX config (add-to-list From fa32e42933337635c2d8c9717c5fe346d4cb95b1 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 15:10:30 +0200 Subject: [PATCH 09/44] refactoring: Add PamhyrWindow and PamhyrDialog (WIP). --- src/View/About/Window.py | 19 ++- src/View/Configure/Solver/Window.py | 26 +++- src/View/Configure/Window.py | 117 ++++++++------- src/View/DummyWindow.py | 15 +- src/View/MainWindow.py | 130 ++++++++-------- src/View/Network/Table.py | 1 - src/View/Network/Window.py | 45 +++--- src/View/Study/Window.py | 48 +++--- src/View/{ => Tools}/ASubWindow.py | 10 +- src/View/{ => Tools}/ListedSubWindow.py | 2 +- src/View/Tools/PamhyrWindow.py | 189 ++++++++++++++++++++++++ 11 files changed, 408 insertions(+), 194 deletions(-) rename src/View/{ => Tools}/ASubWindow.py (97%) rename src/View/{ => Tools}/ListedSubWindow.py (98%) create mode 100644 src/View/Tools/PamhyrWindow.py diff --git a/src/View/About/Window.py b/src/View/About/Window.py index 7c269aaf..218869b5 100644 --- a/src/View/About/Window.py +++ b/src/View/About/Window.py @@ -19,14 +19,17 @@ import os import logging -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtCore import QCoreApplication _translate = QCoreApplication.translate logger = logging.getLogger() -class AboutWindow(ASubWindow): +class AboutWindow(PamhyrDialog): + _pamhyr_ui = "about" + _pamhyr_name = "About" + def _path_file(self, filename): return os.path.abspath( os.path.join( @@ -35,10 +38,14 @@ class AboutWindow(ASubWindow): ) ) - - def __init__(self, title="About", parent=None): - super(AboutWindow, self).__init__(name=title, ui="about", parent=parent) - self.ui.setWindowTitle(title) + def __init__(self, study = None, config = None, parent=None): + super(AboutWindow, self).__init__( + title = _translate("About", "About"), + study = study, + config = config, + options = [], + parent = parent + ) # Version with open(self._path_file("VERSION"), "r") as f: diff --git a/src/View/Configure/Solver/Window.py b/src/View/Configure/Solver/Window.py index c295481f..369ff40a 100644 --- a/src/View/Configure/Solver/Window.py +++ b/src/View/Configure/Solver/Window.py @@ -16,7 +16,7 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from Solver.Solvers import solver_type_list from Solver.GenericSolver import GenericSolver @@ -24,12 +24,22 @@ from PyQt5.QtWidgets import ( QPushButton, ) -class ConfigureSolverWindow(ASubWindow): - def __init__(self, data=None, title="Configuration : Add Solver", parent=None): +class ConfigureSolverWindow(PamhyrDialog): + _pamhyr_ui = "ConfigureAddSolverDialog" + _pamhyr_name = "Add/Edit Solver" + + def __init__(self, data=None, config=None, parent=None): + if data is not None: + name = f"Edit Solver - {data.name}" + else: + name = "Add a new Solver" + super(ConfigureSolverWindow, self).__init__( - name=title, ui="ConfigureAddSolverDialog", parent=parent + title = name, + config = config, + options = [], + parent=parent ) - self.ui.setWindowTitle(title) # Combo box item for solver in solver_type_list: @@ -37,10 +47,10 @@ class ConfigureSolverWindow(ASubWindow): # Data to return self.data = data - if not self.data is None: + if self.data is not None: self.copy_data() - self.connect() + self.setup_connection() def copy_data(self): self.set_combobox_text("comboBox_solver", self.data.type) @@ -53,7 +63,7 @@ class ConfigureSolverWindow(ASubWindow): self.set_line_edit_text("lineEdit_output", self.data._path_output) self.set_line_edit_text("lineEdit_output_cmd", self.data._cmd_output) - def connect(self): + def setup_connection(self): # File button buttons = { "pushButton_input": (lambda: self.file_dialog( diff --git a/src/View/Configure/Window.py b/src/View/Configure/Window.py index 23a371ac..83db81f6 100644 --- a/src/View/Configure/Window.py +++ b/src/View/Configure/Window.py @@ -21,8 +21,7 @@ import logging from copy import deepcopy from config import Config -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from View.Stricklers.Table import TableModel from View.Stricklers.translate import * @@ -95,50 +94,42 @@ class SolverTableModel(QAbstractTableModel): self.layoutChanged.emit() -class ConfigureWindow(ASubWindow, ListedSubWindow): - def __init__(self, conf=None, title="Configure", parent=None): +class ConfigureWindow(PamhyrDialog): + _pamhyr_ui = "ConfigureDialog" + _pamhyr_name = "Configure" + + def __init__(self, config=None, parent=None): + if config is None: + config = Config() + super(ConfigureWindow, self).__init__( - name=title, ui="ConfigureDialog", parent=parent + title = self._pamhyr_name, + config = config, + options = [], + parent=parent ) - self.ui.setWindowTitle(title) - if conf is None: - self.conf = Config() - else: - self.conf = conf + self.setup_custom_sc() + self.setup_solver() + self.setup_stricklers() + self.setup_data() + self.setup_connection() - self.setup_sc() - - # Solver + def setup_solver(self): table = self.find(QTableView, "tableView_solver") self.solver_table_model = SolverTableModel( headers = ["name", "type", "description"], - rows = conf.solvers + rows = self._config.solvers ) table.setModel(self.solver_table_model) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) + table.resizeColumnsToContents() - # Meshing_Tool - self.set_line_edit_text("lineEdit_meshing_tool", self.conf.meshing_tool) - - # Const - self.set_line_edit_text("lineEdit_segment", str(self.conf.segment)) - self.set_line_edit_text("lineEdit_max_listing", str(self.conf.max_listing)) - - # Backup - self.set_check_box("checkBox_backup", self.conf.backup_enable) - self.set_line_edit_text("lineEdit_backup_path", self.conf.backup_path) - self.set_time_edit("timeEdit_backup_frequence", self.conf.backup_frequence) - self.set_spin_box("spinBox_backup_max", self.conf.backup_max) - - self.find(QTableView, "tableView_solver").resizeColumnsToContents() - self.connect() - - # Stricklers + def setup_stricklers(self): table = self.find(QTableView, f"tableView_stricklers") - self._stricklers = deepcopy(self.conf.stricklers) + self._stricklers = deepcopy(self._config.stricklers) self._stricklers_table = TableModel( data = self._stricklers, undo = self._undo_stack, @@ -148,26 +139,35 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) + def setup_data(self): + # Meshing_Tool + self.set_line_edit_text("lineEdit_meshing_tool", self._config.meshing_tool) + + # Const + self.set_line_edit_text("lineEdit_segment", str(self._config.segment)) + self.set_line_edit_text("lineEdit_max_listing", str(self._config.max_listing)) + + # Backup + self.set_check_box("checkBox_backup", self._config.backup_enable) + self.set_line_edit_text("lineEdit_backup_path", self._config.backup_path) + self.set_time_edit("timeEdit_backup_frequence", self._config.backup_frequence) + self.set_spin_box("spinBox_backup_max", self._config.backup_max) + # Editor - self.set_line_edit_text("lineEdit_editor_cmd", str(self.conf.editor)) + self.set_line_edit_text("lineEdit_editor_cmd", str(self._config.editor)) # Language languages = Config.languages() for lang in languages: self.combobox_add_item("comboBox_language", lang) - if self.conf.lang == languages[lang]: + if self._config.lang == languages[lang]: self.set_combobox_text("comboBox_language", lang) - def setup_sc(self): - self._undo_stack = QUndoStack() + def setup_custom_sc(self): + self._debug_sc = QShortcut(QKeySequence("Ctrl+G"), self) + self._debug_sc.activated.connect(self.set_debug) - self.debug_sc = QShortcut(QKeySequence("Ctrl+G"), self) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def connect(self): + def setup_connection(self): buttons = { # Solvers "pushButton_solver_add": self.add_solver, @@ -195,33 +195,31 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): for button in buttons: self.find(QPushButton, button).clicked.connect(buttons[button]) - self.debug_sc.activated.connect(self.set_debug) - def accept(self): # Solvers - self.conf.solvers = self.solver_table_model.rows.copy() + self._config.solvers = self.solver_table_model.rows.copy() # Meshing_Tool - self.conf.meshing_tool = self.get_line_edit_text("lineEdit_meshing_tool") + self._config.meshing_tool = self.get_line_edit_text("lineEdit_meshing_tool") # Const - self.conf.segment = self.get_line_edit_text("lineEdit_segment") - self.conf.max_listing = self.get_line_edit_text("lineEdit_max_listing") + self._config.segment = self.get_line_edit_text("lineEdit_segment") + self._config.max_listing = self.get_line_edit_text("lineEdit_max_listing") # Backup - self.conf.backup_enable = self.get_check_box("checkBox_backup") - self.conf.backup_path = self.get_line_edit_text("lineEdit_backup_path") - self.conf.backup_frequence = self.get_time_edit("timeEdit_backup_frequence") - self.conf.backup_max = self.get_spin_box("spinBox_backup_max") + self._config.backup_enable = self.get_check_box("checkBox_backup") + self._config.backup_path = self.get_line_edit_text("lineEdit_backup_path") + self._config.backup_frequence = self.get_time_edit("timeEdit_backup_frequence") + self._config.backup_max = self.get_spin_box("spinBox_backup_max") # Stricklers - self.conf.stricklers = deepcopy(self._stricklers) + self._config.stricklers = deepcopy(self._stricklers) # Editor - self.conf.editor = self.get_line_edit_text("lineEdit_editor_cmd") + self._config.editor = self.get_line_edit_text("lineEdit_editor_cmd") # Language - self.conf.lang = Config.languages()[self.get_combobox_text("comboBox_language")] + self._config.lang = Config.languages()[self.get_combobox_text("comboBox_language")] self.end() @@ -230,13 +228,13 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): self.end() def end(self): - self.conf.save() + self._config.save() self.close() # Debug def set_debug(self): - self.conf.debug = not self.conf.debug - logger.info(f"Debug mode set : {self.conf.debug}") + self._config.debug = not self._config.debug + logger.info(f"Debug mode set : {self._config.debug}") self.parent.setup_debug_mode() # Solvers @@ -246,6 +244,7 @@ class ConfigureWindow(ASubWindow, ListedSubWindow): for index in indexes: self.edit_solver = ConfigureSolverWindow( data=self.solver_table_model.rows[index.row()], + config=self._config, parent=self ) if self.edit_solver.exec_(): diff --git a/src/View/DummyWindow.py b/src/View/DummyWindow.py index 05117bca..768d6fbc 100644 --- a/src/View/DummyWindow.py +++ b/src/View/DummyWindow.py @@ -16,9 +16,14 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrWindow -class DummyWindow(ASubWindow): - def __init__(self, title="Dummy", parent=None): - super(DummyWindow, self).__init__(name=title, ui="dummy", parent=parent) - self.ui.setWindowTitle(title) +class DummyWindow(PamhyrWindow): + __ui = "Dummy" + __name = "Pamhyr Dummy Window" + + def __init__(self, parent=None): + super(DummyWindow, self).__init__( + title = self.__name, + parent = parent, + ) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 47e14850..cf253dd5 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -36,27 +36,27 @@ from PyQt5.QtWidgets import ( ) from PyQt5.uic import loadUi -from View.ASubWindow import WindowToolKit -from View.ListedSubWindow import ListedSubWindow +from View.Tools.ASubWindow import WindowToolKit +from View.Tools.ListedSubWindow import ListedSubWindow from View.DummyWindow import DummyWindow from View.Configure.Window import ConfigureWindow from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow -from View.Geometry.Window import GeometryWindow -from View.BoundaryCondition.Window import BoundaryConditionWindow -from View.LateralContribution.Window import LateralContributionWindow -from View.InitialConditions.Window import InitialConditionsWindow -from View.Stricklers.Window import StricklersWindow -from View.Frictions.Window import FrictionsWindow -from View.SedimentLayers.Window import SedimentLayersWindow -from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow -from View.SolverParameters.Window import SolverParametersWindow -from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow -from View.CheckList.Window import CheckListWindow -from View.Results.Window import ResultsWindow -from View.Debug.Window import ReplWindow +# from View.Geometry.Window import GeometryWindow +# from View.BoundaryCondition.Window import BoundaryConditionWindow +# from View.LateralContribution.Window import LateralContributionWindow +# from View.InitialConditions.Window import InitialConditionsWindow +# from View.Stricklers.Window import StricklersWindow +# from View.Frictions.Window import FrictionsWindow +# from View.SedimentLayers.Window import SedimentLayersWindow +# from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow +# from View.SolverParameters.Window import SolverParametersWindow +# from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow +# from View.CheckList.Window import CheckListWindow +# from View.Results.Window import ResultsWindow +# from View.Debug.Window import ReplWindow from Model.Study import Study @@ -108,7 +108,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.conf = conf # Model - self.model = None + self._study = None # Results self._last_results = None @@ -133,8 +133,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def set_title(self): title = "(dbg) " if self.conf.debug else "" - if self.model is not None: - title += f"Pamhyr2 - {self.model.name}" + if self._study is not None: + title += f"Pamhyr2 - {self._study.name}" self.setWindowTitle(title) else: title += "Pamhyr2" @@ -216,7 +216,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): super(ApplicationWindow, self).changeEvent(event) def close(self): - if self.model is not None and not self.model.is_saved: + if self._study is not None and not self._study.is_saved: self._close_question = True if self.dialog_close(): # PAMHYR is close correctly (no crash) @@ -233,7 +233,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def closeEvent(self, event): if not self._close_question: - if self.model is not None and not self.model.is_saved: + if self._study is not None and not self._study.is_saved: if self.dialog_close(cancel = False): # PAMHYR is close correctly (no crash) self.conf.set_close_correctly() @@ -291,16 +291,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ######### def get_model(self): - return self.model + return self._study def set_model(self, model): - self.model = model + self._study = model self.update_enable_action() - self.conf.set_last_study(self.model.filename) + self.conf.set_last_study(self._study.filename) self.set_title() def close_model(self): - self.model = None + self._study = None self.update_enable_action() self.conf.set_close_correctly() self.set_title() @@ -314,7 +314,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - no_model = self.model is None + no_model = self._study is None for action in no_model_action: self.enable_actions(action, no_model) @@ -342,7 +342,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Nothing """ self.set_model(Study.open(filename)) - logger.info(f"Open Study - {self.model.name}") + logger.info(f"Open Study - {self._study.name}") self.set_title() def save_study(self): @@ -354,22 +354,22 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model.filename is None or self.model.filename == "": + if self._study.filename is None or self._study.filename == "": file_name, _ = QFileDialog.getSaveFileName( self, "Save File", "", "Pamhyr(*.pamhyr)" ) if file_name.rsplit(".", 1)[-1] == "pamhyr": - self.model.filename = file_name + self._study.filename = file_name else: - self.model.filename = file_name + ".pamhyr" + self._study.filename = file_name + ".pamhyr" - if self.model.is_saved: + if self._study.is_saved: return logger.info("Save...") - self.model.save() + self._study.save() def save_as_study(self): """Save current study as new file @@ -386,11 +386,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) if file_name[-4:] == ".pamhyr": - self.model.filename = file_name + self._study.filename = file_name else: - self.model.filename = file_name + ".pamhyr" + self._study.filename = file_name + ".pamhyr" - self.model.save() + self._study.save() ################## # MSG AND DIALOG # @@ -456,7 +456,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - self.config = ConfigureWindow(conf=self.conf, parent=self) + self.config = ConfigureWindow(config=self.conf, parent=self) self.config.show() def open_about(self): @@ -476,7 +476,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model is None: + if self._study is None: dialog = QFileDialog(self) dialog.setFileMode(QFileDialog.FileMode.ExistingFile) dialog.setDefaultSuffix(".pamhyr") @@ -494,7 +494,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model is None: + if self._study is None: self.new_study = NewStudyWindow(parent=self) self.new_study.show() @@ -504,8 +504,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if not self.model is None: - self.new_study = NewStudyWindow(study=self.model, parent=self) + if not self._study is None: + self.new_study = NewStudyWindow(study=self._study, parent=self) self.new_study.show() def open_network(self): @@ -514,9 +514,9 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if self.model is not None: + if self._study is not None: if not self.sub_win_exists("River network"): - self.network = NetworkWindow(model=self.model, parent=self) + self.network = NetworkWindow(study=self._study, parent=self) self.network.show() else: self.network.activateWindow() @@ -527,14 +527,14 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ - if (self.model is not None and self.model.river.has_current_reach()): + if (self._study is not None and self._study.river.has_current_reach()): geometry = self.sub_win_filter_first( "Geometry", - contain = [self.model.river.current_reach().name] + contain = [self._study.river.current_reach().name] ) if geometry is None: - geometry = GeometryWindow(model=self.model, parent=self) + geometry = GeometryWindow(model=self._study, parent=self) geometry.show() else: geometry.activateWindow() @@ -548,7 +548,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) if bound is None: - bound = BoundaryConditionWindow(study = self.model, parent = self) + bound = BoundaryConditionWindow(study = self._study, parent = self) bound.show() else: bound.activateWindow() @@ -560,7 +560,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) if lateral is None: - lateral = LateralContributionWindow(study = self.model, parent = self) + lateral = LateralContributionWindow(study = self._study, parent = self) lateral.show() else: lateral.activateWindow() @@ -573,7 +573,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if strick is None: strick = StricklersWindow( - study = self.model, + study = self._study, config = self.conf, parent = self ) @@ -582,17 +582,17 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): strick.activateWindow() def open_frictions(self): - if (self.model is not None and - self.model.river.has_current_reach()): + if (self._study is not None and + self._study.river.has_current_reach()): frictions = self.sub_win_filter_first( "Frictions", - contain = [self.model.river.current_reach().name] + contain = [self._study.river.current_reach().name] ) if frictions is None: frictions = FrictionsWindow( - study = self.model, + study = self._study, parent = self ) frictions.show() @@ -602,15 +602,15 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.msg_select_reach() def open_initial_conditions(self): - if self.model.river.has_current_reach(): + if self._study.river.has_current_reach(): initial = self.sub_win_filter_first( "Initial condition", - contain = [self.model.river.current_reach().name] + contain = [self._study.river.current_reach().name] ) if initial is None: initial = InitialConditionsWindow( - study = self.model, + study = self._study, parent = self ) initial.show() @@ -627,7 +627,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if params is None: params = SolverParametersWindow( - study = self.model, + study = self._study, parent = self ) params.show() @@ -636,31 +636,31 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_sediment_layers(self): sl = SedimentLayersWindow( - study = self.model, + study = self._study, parent = self ) sl.show() def open_reach_sediment_layers(self): sl = ReachSedimentLayersWindow( - study = self.model, + study = self._study, parent = self ) sl.show() def run_solver(self): - if self.model is None: + if self._study is None: return run = SelectSolverWindow( - study = self.model, + study = self._study, config = self.conf, parent = self ) if run.exec(): solver = run.solver check = CheckListWindow( - study = self.model, + study = self._study, config = self.conf, solver = solver, parent = self @@ -669,7 +669,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def solver_log(self, solver): sol = SolverLogWindow( - study = self.model, + study = self._study, config = self.conf, solver = solver, parent = self @@ -693,7 +693,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if res is None: res = ResultsWindow( - study = self.model, + study = self._study, solver = solver, results = results, parent = self @@ -708,18 +708,18 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): def open_debug(self): repl = ReplWindow( - study = self.model, + study = self._study, config = self.conf, parent = self ) repl.show() def open_sqlite(self): - if self.model is None: + if self._study is None: logger.debug("No study open for sql debuging...") return - file = self.model.filename + file = self._study.filename _ = subprocess.Popen( f"sqlitebrowser {file}", shell=True diff --git a/src/View/Network/Table.py b/src/View/Network/Table.py index 733a910d..da74d813 100644 --- a/src/View/Network/Table.py +++ b/src/View/Network/Table.py @@ -22,7 +22,6 @@ import traceback from Model.Network.Node import Node from Model.Network.Edge import Edge from Model.Network.Graph import Graph -from View.ASubWindow import ASubWindow from View.Network.GraphWidget import GraphWidget from View.Network.UndoCommand import * from View.Tools.PamhyrTable import PamhyrTableModel diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index 95e575ea..a54c0527 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -16,6 +16,8 @@ # -*- coding: utf-8 -*- +import logging + from PyQt5.QtCore import QCoreApplication from PyQt5.QtGui import ( @@ -36,7 +38,7 @@ from PyQt5.QtWidgets import ( from Model.River import RiverNode, RiverReach, River -from View.ASubWindow import ASubMainWindow +from View.Tools.PamhyrWindow import PamhyrWindow from View.Network.GraphWidget import GraphWidget from View.Network.UndoCommand import * from View.Network.translate import ( @@ -47,36 +49,30 @@ from View.Network.Table import ( ComboBoxDelegate, NodeTableModel, EdgeTableModel, ) +logger = logging.getLogger() + _translate = QCoreApplication.translate -class NetworkWindow(ASubMainWindow): - def __init__(self, model=None, title="River network", parent=None): - self._title = title - self._model = model - self._graph = self._model.river - - self.setup_title() +class NetworkWindow(PamhyrWindow): + _pamhyr_ui = "Network" + _pamhyr_name = "River network" + def __init__(self, study=None, config=None, parent=None): super(NetworkWindow, self).__init__( - name=title, ui="Network", parent=parent + title = self._pamhyr_name + " - " + study.name, + study = study, + config = config, + options = ['undo'], + parent=parent, ) - self.ui.setWindowTitle(self._title) - self.setup_sc() + self._graph = study.river + self.setup_graph() self.setup_table() self.setup_connections() - def setup_title(self): - self._title = self._title + " - " + self._model.name - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - def setup_table(self): retranslate() @@ -143,9 +139,6 @@ class NetworkWindow(ASubMainWindow): self.clicked_del ) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - def clicked_add(self): if self.get_action_checkable("action_toolBar_add"): self.set_action_checkable("action_toolBar_del", False) @@ -166,13 +159,15 @@ class NetworkWindow(ASubMainWindow): if key == Qt.Key_Escape: self._graph_widget.reset_selection - def undo(self): + # Redefine undo/redo method + + def _undo(self): self._undo_stack.undo() self._reachs_model.update() self._nodes_model.update() self._graph_widget.display_update() - def redo(self): + def _redo(self): self._undo_stack.redo() self._reachs_model.update() self._nodes_model.update() diff --git a/src/View/Study/Window.py b/src/View/Study/Window.py index 7218f9d3..871d0545 100644 --- a/src/View/Study/Window.py +++ b/src/View/Study/Window.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from Model.Study import Study -from View.ASubWindow import ASubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtCore import QCoreApplication @@ -27,27 +27,37 @@ from PyQt5.QtWidgets import ( _translate = QCoreApplication.translate -class NewStudyWindow(ASubWindow): - def __init__(self, study=None, title="New Study", parent=None): - super(NewStudyWindow, self).__init__(name=title, ui="NewStudy", parent=parent) - self.ui.setWindowTitle(title) +class NewStudyWindow(PamhyrDialog): + _pamhyr_ui = "NewStudy" + _pamhyr_name = "Edit/New Study" - self.parent = parent - self.study = study + def __init__(self, study=None, config = None, parent=None): + if study is not None: + name = f"Edit study - {study.name}" + else: + name = "New study" - if not self.study is None: - self.set_line_edit_text("lineEdit_name", self.study.name) - self.set_text_edit_text("textEdit_description", self.study.description) - self.set_datetime_edit("dateTimeEdit_date", self.study.date) + super(NewStudyWindow, self).__init__( + title = name, + study = study, + config = config, + options = [], + parent = parent + ) + + if not self._study is None: + self.set_line_edit_text("lineEdit_name", self._study.name) + self.set_text_edit_text("textEdit_description", self._study.description) + self.set_datetime_edit("dateTimeEdit_date", self._study.date) self.find(QLabel, "label_creation_date_data").setText( - self.study.creation_date.isoformat(sep=" ") + self._study.creation_date.isoformat(sep=" ") ) self.find(QLabel, "label_last_modification_data").setText( - self.study.last_save_date.isoformat(sep=" ") + self._study.last_save_date.isoformat(sep=" ") ) - if self.study.time_system == "date": + if self._study.time_system == "date": self.set_radio_button("radioButton_date", True) self.find(QLabel, "label_date").setEnabled(True) self.find(QDateTimeEdit, "dateTimeEdit_date").setEnabled(True) @@ -76,18 +86,18 @@ class NewStudyWindow(ASubWindow): name = self.get_line_edit_text("lineEdit_name") description = self.get_text_edit_text("textEdit_description") - if self.study is None: + if self._study is None: study = Study.new(name, description) if self.get_radio_button("radioButton_date"): date = self.get_datetime_edit("dateTimeEdit_date") study.use_date(date) self.parent.set_model(study) else: - self.study.name = name - self.study.description = description + self._study.name = name + self._study.description = description if self.get_radio_button("radioButton_date"): date = self.get_datetime_edit("dateTimeEdit_date") - self.study.use_date(date) + self._study.use_date(date) else: - self.study.use_time() + self._study.use_time() self.done(True) diff --git a/src/View/ASubWindow.py b/src/View/Tools/ASubWindow.py similarity index 97% rename from src/View/ASubWindow.py rename to src/View/Tools/ASubWindow.py index 4e1c1327..9ac63c2d 100644 --- a/src/View/ASubWindow.py +++ b/src/View/Tools/ASubWindow.py @@ -463,11 +463,11 @@ class ASubWindowFeatures(object): # Top level interface class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): - def __init__(self, name="", ui="dummy", parent=None): + def __init__(self, name="", ui="dummy", parent=None, **kwargs): super(ASubMainWindow, self).__init__(parent=parent) if ui is not None: self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", f"{ui}.ui"), + os.path.join(os.path.dirname(__file__), "..", "ui", f"{ui}.ui"), self ) @@ -496,10 +496,10 @@ class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): return self.ui.findChild(qtype, name) class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): - def __init__(self, name="", ui="dummy", parent=None): + def __init__(self, name="", ui="dummy", parent=None, **kwargs): super(ASubWindow, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", f"{ui}.ui"), + os.path.join(os.path.dirname(__file__), "..", "ui", f"{ui}.ui"), self ) self.name = name @@ -530,7 +530,7 @@ class AWidget(QWidget, ASubWindowFeatures): def __init__(self, ui="", parent=None): super(AWidget, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "ui", "Widgets", f"{ui}.ui"), + os.path.join(os.path.dirname(__file__), "..", "ui", "Widgets", f"{ui}.ui"), self ) self.parent = parent diff --git a/src/View/ListedSubWindow.py b/src/View/Tools/ListedSubWindow.py similarity index 98% rename from src/View/ListedSubWindow.py rename to src/View/Tools/ListedSubWindow.py index b4dfe416..f671cc60 100644 --- a/src/View/ListedSubWindow.py +++ b/src/View/Tools/ListedSubWindow.py @@ -22,7 +22,7 @@ from functools import reduce logger = logging.getLogger() class ListedSubWindow(object): - def __init__(self): + def __init__(self, **kwargs): super(ListedSubWindow, self).__init__() self.sub_win_cnt = 0 self.sub_win_list = [] diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py new file mode 100644 index 00000000..d6ae6752 --- /dev/null +++ b/src/View/Tools/PamhyrWindow.py @@ -0,0 +1,189 @@ +# PamhyrWindow.py -- Pamhyr +# Copyright (C) 2023 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import logging + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtWidgets import ( + QUndoStack, QShortcut, +) + +from View.Tools.ASubWindow import ASubMainWindow, ASubWindow +from View.Tools.ListedSubWindow import ListedSubWindow + +logger = logging.getLogger() + +class PamhyrWindowTools(object): + def __init__(self, options = ["undo", "copy"], parent = None, **kwargs): + super(PamhyrWindowTools, self).__init__() + + self._undo_stack = None + + if "undo" in options: + self._init_undo() + + if "copy" in options: + self._init_copy() + + # Undo/Redo + + def _init_undo(self): + self._undo_stack = QUndoStack() + + self._undo_sc = QShortcut(QKeySequence.Undo, self) + self._redo_sc = QShortcut(QKeySequence.Redo, self) + + self._undo_sc.activated.connect(self._undo) + self._redo_sc.activated.connect(self._redo) + + def _undo(self): + if self._undo_stack is not None: + self._undo_stack.undo() + self._update() + + def _redo(self): + if self._undo_stack is not None: + self._undo_stack.redo() + self._update() + + # Copy/Paste + + def _init_copy(self): + self._copy_sc = QShortcut(QKeySequence.Copy, self) + self._paste_sc = QShortcut(QKeySequence.Paste, self) + + self._copy_sc.activated.connect(self._copy) + self._paste_sc.activated.connect(self._paste) + + def _copy(self): + if self._copy_stack is not None: + self._copy_stack.copy() + self._update() + + def _paste(self): + if self._copy_stack is not None: + self._copy_stack.redo() + self._update() + + # Display + + def _set_title(self): + """Apply `self._title` at current window title displayed + + Returns: + Nothing + """ + self.ui.setWindowTitle(self._title) + + def _update(self): + """Update window display component + + Returns: + Nothing + """ + self._set_title() + + # Hash methods + + @classmethod + def _hash(cls, data): + """Compute window hash + + Args: + data: window data parameters + + Returns: + The hash + """ + hash_str = cls._pamhyr_name + hash_str += cls._pamhyr_ui + + for el in data: + hash_str += repr(el) + + h = hash(hash_str) + logger.debug(f"Compute hash = {h} for window {cls._pamhyr_name}") + + return h + + def hash(self): + """Compute window hash + + Returns: + The hash + """ + data = [ + self._study, + self._config, + ] + + return self._hash() + + +class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): + _pamhyr_ui = "dummy" + _pamhyr_name = "PamhyrWindow" + + def __init__(self, + title = "Pamhyr2", + study = None, config = None, + options = ["undo", "copy"], + parent = None): + self._title = title + self._study = study + self._config = config + self._parent = parent + + logger.info(self._pamhyr_name) + + super(PamhyrWindow, self).__init__( + name = self._pamhyr_name, + ui = self._pamhyr_ui, + parent = parent, + ) + + self._set_title() + + +class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools): + _pamhyr_ui = "dummy" + _pamhyr_name = "PamhyrWindow" + + def __init__(self, + title = "Pamhyr2", + study = None, config = None, + options = ["undo", "copy"], + parent = None): + self._title = title + self._study = study + self._config = config + self._parent = parent + + logger.info(self._pamhyr_name) + logger.info(self._pamhyr_ui) + + super(PamhyrDialog, self).__init__( + name = self._pamhyr_name, + ui = self._pamhyr_ui, + parent = parent, + ) + + self._set_title() From cc9f853a34498049af3092d1688b297922a28fe1 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 15:31:41 +0200 Subject: [PATCH 10/44] refactoring: Add PamhyrWidget and continue refacto using PamhyrWindow. --- src/View/BoundaryCondition/Edit/Table.py | 2 - src/View/BoundaryCondition/Edit/Window.py | 75 +++++++++-------------- src/View/BoundaryCondition/Window.py | 40 +++++------- src/View/Geometry/Profile/Window.py | 3 +- src/View/Geometry/Window.py | 5 +- src/View/MainWindow.py | 4 +- src/View/Plot/PamhyrToolbar.py | 2 - src/View/Tools/PamhyrWidget.py | 13 +++- 8 files changed, 64 insertions(+), 80 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index 35360400..e89e6d0d 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -23,8 +23,6 @@ from datetime import date, time, datetime, timedelta from tools import trace, timer -from View.ASubWindow import ASubMainWindow, AWidget -from View.ListedSubWindow import ListedSubWindow from View.Tools.PamhyrTable import PamhyrTableModel from PyQt5.QtCore import ( diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 6e0d6131..249a34cb 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -20,8 +20,8 @@ import logging from tools import timer, trace -from View.ASubWindow import ASubMainWindow, AWidget -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.PamhyrWidget import PamhyrWidget from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate from PyQt5.QtGui import ( @@ -54,16 +54,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class WD50Sigma(AWidget): +class WD50Sigma(PamhyrWidget): + _pamhyr_ui = "d50sigma" + d50Changed = pyqtSignal(float) sigmaChanged = pyqtSignal(float) def __init__(self, parent=None): super(WD50Sigma, self).__init__( - ui="d50sigma", parent=parent ) - self.parent = parent self.spinBox_d50 = self.find(QDoubleSpinBox, "doubleSpinBox_d50") self.spinBox_sigma = self.find(QDoubleSpinBox, "doubleSpinBox_sigma") @@ -95,45 +95,37 @@ class WD50Sigma(AWidget): def valueChangedSigma(self, value): self.sigmaChanged.emit(value) -class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit boundary condition", - data=None, study=None, parent=None): +class EditBoundaryConditionWindow(PamhyrWindow): + _pamhyr_ui = "EditBoundaryConditions" + _pamhyr_name = "Edit Boundary Conditions" + + def __init__(self, data=None, study=None, config=None, parent=None): self._data = data - self._study = study - self._title = title - self.compute_title() - - super(EditBoundaryConditionWindow, self).__init__( - name=self._title, ui="EditBoundaryConditions", parent=parent - ) - - self.ui.setWindowTitle(self._title) - - self.setup_sc() - self.setup_table() - self.setup_plot() - self.setup_data() - self.setup_connections() - - def compute_title(self): + name = self._pamhyr_name if self._data is not None: node_name = (self._data.node.name if self._data.node is not None else _translate("BoundaryCondition", "Not associate")) - self._title = ( - _translate("Edit boundary condition", self._title) + - f" - {self._study.name} " + + name = ( + _translate("Edit boundary condition", self._pamhyr_name) + + f" - {study.name} " + f" - {self._data.name} ({self._data.id}) " + f"({long_types[self._data.bctype]} - {node_name})" ) - def setup_sc(self): - self._undo_stack = QUndoStack() + super(EditBoundaryConditionWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent + ) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self.ui.setWindowTitle(self._title) + + self.setup_table() + self.setup_plot() + self.setup_data() + self.setup_connections() def setup_data(self): self._is_solid = self._data.bctype == "SL" @@ -193,17 +185,11 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self.update) if self._is_solid: @@ -252,7 +238,6 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): ) ) - def add(self): rows = self.index_selected_rows() if len(self._data) == 0 or len(rows) == 0: @@ -285,7 +270,7 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.plot.update() - def copy(self): + def _copy(self): rows = self.index_selected_rows() table = [] @@ -297,7 +282,7 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.copyTableIntoClipboard(table) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: @@ -311,12 +296,12 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self._table.paste(row, header, data) self.plot.update() - def undo(self): + def _undo(self): self._table.undo() self.plot.update() self.widget_update() - def redo(self): + def _redo(self): self._table.redo() self.plot.update() self.widget_update() diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py index 7f8c13e2..5f08ecf8 100644 --- a/src/View/BoundaryCondition/Window.py +++ b/src/View/BoundaryCondition/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -63,32 +62,28 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Boundary conditions", study=None, parent=None): - self._title = title + " - " + study.name +class BoundaryConditionWindow(PamhyrWindow): + _pamhyr_ui = "BoundaryConditions" + _pamhyr_name = "Boundary conditions" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(BoundaryConditionWindow, self).__init__( - name=title, ui="BoundaryConditions", parent=parent + title = name, + study = study, + config = config, + parent=parent ) - self._study = study self._bcs = self._study.river.boundary_condition - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() self.ui.setWindowTitle(self._title) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() self._table = {} @@ -141,11 +136,6 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_edit").triggered.connect(self.edit) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - def current_tab(self): return self.find(QTabWidget, "tabWidget")\ .currentWidget()\ @@ -202,17 +192,17 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): row = self.index_selected_row() self._table[tab].move_down(row) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): tab = self.current_tab() self._table[tab].undo() - def redo(self): + def _redo(self): tab = self.current_tab() self._table[tab].redo() diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 54d1e011..eca6e7af 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -37,7 +37,8 @@ from PyQt5.QtWidgets import ( from Model.Geometry.Reach import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ -from View.ASubWindow import ASubMainWindow +from View.Tools.ASubWindow import ASubMainWindow + from View.Geometry.Profile.mainwindow_ui_profile import Ui_MainWindow from View.Geometry.Profile.Plot import Plot from View.Geometry.Profile.Table import * diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 1c8cb5da..a66ed781 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -41,8 +41,9 @@ from View.Geometry.PlotXY import PlotXY from View.Geometry.PlotKPZ import PlotKPZ from View.Geometry.PlotAC import PlotAC -from View.ASubWindow import ASubMainWindow, WindowToolKit -from View.ListedSubWindow import ListedSubWindow +from View.Tools.ASubWindow import ASubMainWindow, WindowToolKit +from View.Tools.ListedSubWindow import ListedSubWindow + from View.Geometry.mainwindow_ui_reach import Ui_MainWindow from View.Geometry.Table import * from View.Geometry.Profile.Window import ProfileWindow diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index cf253dd5..f701a474 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -44,8 +44,8 @@ from View.Configure.Window import ConfigureWindow from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow -# from View.Geometry.Window import GeometryWindow -# from View.BoundaryCondition.Window import BoundaryConditionWindow +from View.Geometry.Window import GeometryWindow +from View.BoundaryCondition.Window import BoundaryConditionWindow # from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow diff --git a/src/View/Plot/PamhyrToolbar.py b/src/View/Plot/PamhyrToolbar.py index 6a729f04..c88dfacf 100644 --- a/src/View/Plot/PamhyrToolbar.py +++ b/src/View/Plot/PamhyrToolbar.py @@ -81,8 +81,6 @@ class PamhyrPlotToolbar(NavigationToolbar2QT): os.path.abspath(f"{file_path}/../ui/ressources/zoom.png") )) - logger.info(os.path.abspath(f"{file_path}/../ui/ressources/zoom.png")) - icons.append(("zoom",icon_zoom)) if "iso" in items: diff --git a/src/View/Tools/PamhyrWidget.py b/src/View/Tools/PamhyrWidget.py index 60e06174..22bf9584 100644 --- a/src/View/Tools/PamhyrWidget.py +++ b/src/View/Tools/PamhyrWidget.py @@ -26,10 +26,21 @@ from PyQt5.QtWidgets import ( QSpinBox, QTimeEdit, QDateTimeEdit, QItemDelegate, ) -from View.ASubWindow import AWidget +from View.Tools.ASubWindow import AWidget logger = logging.getLogger() +class PamhyrWidget(AWidget): + _pamhyr_ui = "" + _pamhyr_name = "PamhyrWidget" + + def __init__(self, parent=None): + super(PamhyrWidget, self).__init__( + ui = self._pamhyr_ui, + parent = parent + ) + + class PamhyrExtendedTimeEdit(AWidget): def __init__(self, parent=None): super(PamhyrExtendedTimeEdit, self).__init__( From 7c4ba10871ca4b3e49f0e84dec01abda458cfeaf Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 15:57:28 +0200 Subject: [PATCH 11/44] refactoring: Continue refacto using PamhyrWindow. --- src/View/LateralContribution/Edit/Table.py | 2 - src/View/LateralContribution/Edit/Window.py | 66 ++++++++------------- src/View/LateralContribution/Window.py | 42 +++++-------- src/View/MainWindow.py | 2 +- 4 files changed, 41 insertions(+), 71 deletions(-) diff --git a/src/View/LateralContribution/Edit/Table.py b/src/View/LateralContribution/Edit/Table.py index 2fff0895..8b43602d 100644 --- a/src/View/LateralContribution/Edit/Table.py +++ b/src/View/LateralContribution/Edit/Table.py @@ -22,8 +22,6 @@ from datetime import date, time, datetime, timedelta from tools import trace, timer -from View.ASubWindow import ASubMainWindow, AWidget -from View.ListedSubWindow import ListedSubWindow from View.Tools.PamhyrTable import PamhyrTableModel from PyQt5.QtCore import ( diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index 53576a6b..f37c1759 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -18,8 +18,7 @@ from tools import timer, trace -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from View.Tools.PamhyrDelegate import PamhyrExTimeDelegate from PyQt5.QtGui import ( @@ -47,44 +46,36 @@ from View.LateralContribution.Edit.Plot import Plot _translate = QCoreApplication.translate -class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit lateral contribution", - data=None, study=None, parent=None): +class EditLateralContributionWindow(PamhyrWindow): + _pamhyr_ui = "EditLateralContribution" + _pamhyr_name = "Edit lateral contribution" + + def __init__(self, data=None, + study=None, config=None, + parent=None): self._data = data - self._study = study - self._title = title - self.compute_title() - - super(EditLateralContributionWindow, self).__init__( - name=title, ui="EditLateralContribution", parent=parent - ) - - self.ui.setWindowTitle(self._title) - - self.setup_sc() - self.setup_table() - self.setup_plot() - self.setup_connections() - - def compute_title(self): + name = self._pamhyr_name if self._data is not None: edge_name = (self._data.edge.name if self._data.edge is not None else _translate("LateralContribution", "Not associate")) - self._title = ( - _translate("Edit lateral contribution", self._title) + - f" - {self._study.name} " + + name = ( + _translate("Edit lateral contribution", self._pamhyr_name) + + f" - {study.name} " + f" - {self._data.name} ({self._data.id}) " + f"({long_types[self._data.lctype]} - {edge_name})" ) - def setup_sc(self): - self._undo_stack = QUndoStack() + super(EditLateralContributionWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent + ) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self.setup_table() + self.setup_plot() + self.setup_connections() def setup_table(self): retranslate() @@ -133,17 +124,11 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): ) self.plot.draw() - def setup_connections(self): self.find(QAction, "action_add").triggered.connect(self.add) self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self.update) def update(self): @@ -199,8 +184,7 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(row) self.plot.update() - - def copy(self): + def _copy(self): rows = self.index_selected_rows() table = [] @@ -212,7 +196,7 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self.copyTableIntoClipboard(table) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: @@ -226,10 +210,10 @@ class EditLateralContributionWindow(ASubMainWindow, ListedSubWindow): self._table.paste(row, header, data) self.plot.update() - def undo(self): + def _undo(self): self._table.undo() self.plot.update() - def redo(self): + def _redo(self): self._table.redo() self.plot.update() diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index 31f82906..2793ee36 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -66,32 +65,26 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class LateralContributionWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Lateral contribution", study=None, parent=None): - self._title = title + " - " + study.name +class LateralContributionWindow(PamhyrWindow): + _pamhyr_ui = "LateralContributions" + _pamhyr_name = "Lateral contribution" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(LateralContributionWindow, self).__init__( - name=title, ui="LateralContributions", parent=parent + title = name, + study = study, + config = config, + parent=parent ) - self._study = study self._lcs = self._study.river.lateral_contribution - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() self._table = {} @@ -146,11 +139,6 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_edit").triggered.connect(self.edit) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - for t in ["liquid", "solid", "suspenssion"]: table = self.find(QTableView, f"tableView_{t}") table.selectionModel()\ @@ -247,20 +235,20 @@ class LateralContributionWindow(ASubMainWindow, ListedSubWindow): self._table[tab].move_down(row) self._set_current_reach() - def copy(self): + def _copy(self): logger.info("TODO: copy") self._set_current_reach() - def paste(self): + def _paste(self): logger.info("TODO: paste") self._set_current_reach() - def undo(self): + def _undo(self): tab = self.current_tab() self._table[tab].undo() self._set_current_reach() - def redo(self): + def _redo(self): tab = self.current_tab() self._table[tab].redo() self._set_current_reach() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index f701a474..02a4e1fe 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -46,7 +46,7 @@ from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow -# from View.LateralContribution.Window import LateralContributionWindow +from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow # from View.Frictions.Window import FrictionsWindow From b2b5c971c3b56751fd6817da64f66ae681893d04 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 16:38:14 +0200 Subject: [PATCH 12/44] refactoring: Apply new Pamhyr tools on IC. --- src/View/InitialConditions/DialogDischarge.py | 12 ++-- src/View/InitialConditions/DialogHeight.py | 14 +++-- src/View/InitialConditions/Window.py | 62 ++++++++----------- src/View/MainWindow.py | 3 +- 4 files changed, 45 insertions(+), 46 deletions(-) diff --git a/src/View/InitialConditions/DialogDischarge.py b/src/View/InitialConditions/DialogDischarge.py index f8e7e32a..7bbe8abd 100644 --- a/src/View/InitialConditions/DialogDischarge.py +++ b/src/View/InitialConditions/DialogDischarge.py @@ -16,8 +16,7 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtGui import ( QKeySequence, @@ -32,10 +31,15 @@ from PyQt5.QtWidgets import ( QDoubleSpinBox, ) -class DischargeDialog(ASubWindow, ListedSubWindow): +class DischargeDialog(PamhyrDialog): + _pamhyr_ui = "InitialConditions_Dialog_Generator_Discharge" + _pamhyr_name = "Discharge" + def __init__(self, title="Discharge", parent=None): super(DischargeDialog, self).__init__( - name=title, ui="InitialConditions_Dialog_Generator_Discharge", parent=parent + title = _pamhyr_name, + options = [], + parent = parent ) self.value = None diff --git a/src/View/InitialConditions/DialogHeight.py b/src/View/InitialConditions/DialogHeight.py index 18ad67c8..9ccadbd6 100644 --- a/src/View/InitialConditions/DialogHeight.py +++ b/src/View/InitialConditions/DialogHeight.py @@ -16,8 +16,7 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtGui import ( QKeySequence, @@ -32,10 +31,15 @@ from PyQt5.QtWidgets import ( QDoubleSpinBox, ) -class HeightDialog(ASubWindow, ListedSubWindow): - def __init__(self, title="Height", parent=None): +class HeightDialog(PamhyrDialog): + _pamhyr_ui = "InitialConditions_Dialog_Generator_Height" + _pamhyr_name = "Height" + + def __init__(self, parent=None): super(HeightDialog, self).__init__( - name=title, ui="InitialConditions_Dialog_Generator_Height", parent=parent + title = name, + options = [], + parent = parent ) self.value = None diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py index 00e7b121..3f5ee1d8 100644 --- a/src/View/InitialConditions/Window.py +++ b/src/View/InitialConditions/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -61,41 +60,37 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Initial condition", - study=None, parent=None): - self._study = study - self._reach = study.river.current_reach() - self._ics = self._study.river.initial_conditions.get(self._reach) +class InitialConditionsWindow(PamhyrWindow): + _pamhyr_ui = "InitialConditions" + _pamhyr_name = "Initial condition" - self.setup_title(title) + def __init__(self, study=None, config=None, reach=None, parent=None): + if reach is not None: + self._reach = reach + else: + self._reach = study.river.current_reach() - super(InitialConditionsWindow, self).__init__( - name=self._title, ui="InitialConditions", parent=parent + name = ( + self._pamhyr_name + + " - " + study.name + + " - " + self._reach.name ) - self.setup_sc() + super(InitialConditionsWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent + ) + + self._ics = study.river.initial_conditions.get(self._reach) + self.setup_table() self.setup_graph() self.setup_connections() self.ui.setWindowTitle(self._title) - def setup_title(self, title): - self._title = ( - title + " - " - + self._study.name + " - " - + self._reach.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() @@ -167,11 +162,6 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): self.generate_discharge ) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self._update_plot) def index_selected_row(self): @@ -227,19 +217,19 @@ class InitialConditionsWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(row) self._update_plot() - def copy(self): + def _copy(self): logger.info("TODO: copy") self._update_plot() - def paste(self): + def _paste(self): logger.info("TODO: paste") self._update_plot() - def undo(self): + def _undo(self): self._table.undo() self._update_plot() - def redo(self): + def _redo(self): self._table.redo() self._update_plot() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 02a4e1fe..f5b3ffef 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -47,7 +47,7 @@ from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.LateralContribution.Window import LateralContributionWindow -# from View.InitialConditions.Window import InitialConditionsWindow +from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow # from View.Frictions.Window import FrictionsWindow # from View.SedimentLayers.Window import SedimentLayersWindow @@ -611,6 +611,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): if initial is None: initial = InitialConditionsWindow( study = self._study, + config = self.conf, parent = self ) initial.show() From 809a0dd9cbf051298c2e996ddf2568395ab8a077 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 25 Sep 2023 17:01:59 +0200 Subject: [PATCH 13/44] refarctoring: Apply PamhyrWindow to frictions. --- src/View/Frictions/Window.py | 62 ++++++++++++++-------------------- src/View/MainWindow.py | 4 +-- src/View/Stricklers/Window.py | 45 ++++++++---------------- src/View/Tools/PamhyrWidget.py | 2 +- 4 files changed, 44 insertions(+), 69 deletions(-) diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index 060be330..2a547528 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -59,39 +58,35 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class FrictionsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Frictions", study=None, parent=None): - self._study = study - self._reach = self._study.river.current_reach() +class FrictionsWindow(PamhyrWindow): + _pamhyr_ui = "Frictions" + _pamhyr_name = "Edit frictions" + + def __init__(self, reach=None, study=None, config=None, parent=None): + if reach is not None: + self._reach = reach + else: + self._reach = study.river.current_reach() + self._frictions = self._reach.frictions - self.setup_title(title) - - super(FrictionsWindow, self).__init__( - name=title, ui="Frictions", parent=parent + name = ( + self._pamhyr_name + " - " + + study.name + " - " + + self._reach.name + ) + + super(FrictionsWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " - + self._study.name + " - " - + self._reach.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) def setup_table(self): retranslate() @@ -156,11 +151,6 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_sort").triggered.connect(self.sort) self.find(QAction, "action_edit_stricklers").triggered.connect(self.edit_stricklers) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - table = self.find(QTableView, f"tableView") table.selectionModel()\ .selectionChanged\ @@ -234,16 +224,16 @@ class FrictionsWindow(ASubMainWindow, ListedSubWindow): row = self.index_selected_row() self._table.move_down(row) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() def edit_stricklers(self): diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index f5b3ffef..562b05d7 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -48,8 +48,8 @@ from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow -# from View.Stricklers.Window import StricklersWindow -# from View.Frictions.Window import FrictionsWindow +from View.Stricklers.Window import StricklersWindow +from View.Frictions.Window import FrictionsWindow # from View.SedimentLayers.Window import SedimentLayersWindow # from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow diff --git a/src/View/Stricklers/Window.py b/src/View/Stricklers/Window.py index 12620c4f..d9128d00 100644 --- a/src/View/Stricklers/Window.py +++ b/src/View/Stricklers/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -50,32 +49,23 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class StricklersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Stricklers", study=None, config=None, parent=None): - self._title = title + " - " + study.name +class StricklersWindow(PamhyrWindow): + _pamhyr_ui = "Stricklers" + _pamhyr_name = "Stricklers" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name super(StricklersWindow, self).__init__( - name=title, ui="Stricklers", parent=parent + title = name, + study = study, + config = config, + parent = parent ) - self._study = study - self._config = config - - self.setup_sc() self.setup_table() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def setup_table(self): retranslate() self._table = {} @@ -105,11 +95,6 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_sort").triggered.connect(self.sort) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - def index_selected_rows(self): table = self.find(QTableView, f"tableView_study") return list( @@ -139,14 +124,14 @@ class StricklersWindow(ASubMainWindow, ListedSubWindow): def sort(self): self._table['study'].sort(False) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table['study'].undo() - def redo(self): + def _redo(self): self._table['study'].redo() diff --git a/src/View/Tools/PamhyrWidget.py b/src/View/Tools/PamhyrWidget.py index 22bf9584..8c85b1c1 100644 --- a/src/View/Tools/PamhyrWidget.py +++ b/src/View/Tools/PamhyrWidget.py @@ -31,7 +31,7 @@ from View.Tools.ASubWindow import AWidget logger = logging.getLogger() class PamhyrWidget(AWidget): - _pamhyr_ui = "" + _pamhyr_ui = None _pamhyr_name = "PamhyrWidget" def __init__(self, parent=None): From 1a36d78ee29233e8c572f5c6284a913d3da0f2b2 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 09:34:26 +0200 Subject: [PATCH 14/44] refactoring: Apply PamhyrWindow to SL. --- src/View/MainWindow.py | 4 +- src/View/SedimentLayers/Edit/Window.py | 59 +++++++---------- .../SedimentLayers/Reach/Profile/Window.py | 62 +++++++----------- src/View/SedimentLayers/Reach/SLDialog.py | 15 +++-- src/View/SedimentLayers/Reach/Window.py | 57 +++++++---------- src/View/SedimentLayers/Window.py | 64 ++++++++++--------- src/View/ui/ProfileSedimentLayers.ui | 14 ---- 7 files changed, 114 insertions(+), 161 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 562b05d7..002ee11b 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -50,8 +50,8 @@ from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow -# from View.SedimentLayers.Window import SedimentLayersWindow -# from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow +from View.SedimentLayers.Window import SedimentLayersWindow +from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow # from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow # from View.CheckList.Window import CheckListWindow diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index 29b9ddff..ebe3aece 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -52,41 +51,32 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Edit Sediment Layers", - study=None, sl=None, parent=None): - self._study = study +class EditSedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "EditSedimentLayers" + _pamhyr_name = "Edit Sediment Layers" + + def __init__(self, study=None, config = None, + sl=None, parent=None): self._sl = sl - self.setup_title(title) - - super(EditSedimentLayersWindow, self).__init__( - name=self._title, ui="EditSedimentLayers", parent=parent - ) - - self.setup_sc() - self.setup_table() - self.setup_graph() - self.setup_connections() - - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): name = self._sl.name if name == "": name = _translate("SedimentLayers", "(no name)") - self._title = ( - title + " - " + self._study.name + " - " + name + name = ( + self._pamhyr_name + " - " + study.name + " - " + name ) - def setup_sc(self): - self._undo_stack = QUndoStack() + super(EditSedimentLayersWindow, self).__init__( + title = name, + study = study, + config = config, + parent=parent + ) - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self.setup_table() + self.setup_graph() + self.setup_connections() def setup_table(self): retranslate() @@ -131,11 +121,6 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_move_up").triggered.connect(self.move_up) self.find(QAction, "action_move_down").triggered.connect(self.move_down) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.dataChanged.connect(self._set_plot) self._table.layoutChanged.connect(self._set_plot) @@ -179,14 +164,14 @@ class EditSedimentLayersWindow(ASubMainWindow, ListedSubWindow): self._table.move_down(rows[0]) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index eaae59d3..f424e06a 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -4,8 +4,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -38,27 +37,31 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Profile sediment layers", study=None, profile=None, parent=None): - self._study = study - self._sediment_layers = self._study.river.sediment_layers - self._profile = profile - self._reach = self._study.river.current_reach().reach +class ProfileSedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "ProfileSedimentLayers" + _pamhyr_name = "Profile sediment layers" - self.setup_title(title) + def __init__(self, study=None, config=None, profile=None, parent=None): + self._sediment_layers = study.river.sediment_layers + self._profile = profile + self._reach = profile.reach + + name = self.compute_name(study) super(ProfileSedimentLayersWindow, self).__init__( - name=self._title, ui="ProfileSedimentLayers", parent=parent + title = name, + study = study, + config = config, + parent=parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() self.ui.setWindowTitle(self._title) - def setup_title(self, title): + def compute_name(self, study): rname = self._reach.name if rname == "": rname = _translate("SedimentLayers", "(no name)") @@ -70,20 +73,12 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): "(no name - @kp)").replace("@kp", str(self._profile.kp) ) - self._title = ( - title + " - " - + self._study.name + " - " + return ( + self._pamhyr_name + " - " + + study.name + " - " + rname + " - " + pname ) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() @@ -130,18 +125,9 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): ) self.plot.draw() - def setup_connections(self): - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - - self._table.layoutChanged\ - .connect(self._update_plot) - self._table.dataChanged\ - .connect(self._update_plot) - + self._table.layoutChanged.connect(self._update_plot) + self._table.dataChanged.connect(self._update_plot) def index_selected_rows(self): table = self.find(QTableView, f"tableView") @@ -155,14 +141,14 @@ class ProfileSedimentLayersWindow(ASubMainWindow, ListedSubWindow): ) ) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() diff --git a/src/View/SedimentLayers/Reach/SLDialog.py b/src/View/SedimentLayers/Reach/SLDialog.py index ce1e9794..c8a1efa0 100644 --- a/src/View/SedimentLayers/Reach/SLDialog.py +++ b/src/View/SedimentLayers/Reach/SLDialog.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from View.ASubWindow import ASubWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog from PyQt5.QtGui import ( QKeySequence, @@ -20,12 +19,16 @@ from View.SedimentLayers.Reach.translate import * _translate = QCoreApplication.translate -class SLDialog(ASubWindow, ListedSubWindow): - def __init__(self, title="SL", study=None, parent=None): - self._study = study +class SLDialog(PamhyrDialog): + _pamhyr_ui = "SLDialog" + _pamhyr_name = "SL" + def __init__(self, study=None, config=None, parent=None): super(SLDialog, self).__init__( - name=title, ui="SLDialog", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + parent=parent ) self.setup_combobox() diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index d79312b2..2a655b7c 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -4,8 +4,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -40,38 +39,31 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Reach sediment layers", study=None, parent=None): - self._study = study - self._sediment_layers = self._study.river.sediment_layers - self._reach = self._study.river.current_reach().reach +class ReachSedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "ReachSedimentLayers" + _pamhyr_name = "Reach sediment layers" - self.setup_title(title) + def __init__(self, study=None, config=None, parent=None): + self._sediment_layers = study.river.sediment_layers + self._reach = study.river.current_reach().reach - super(ReachSedimentLayersWindow, self).__init__( - name=self._title, ui="ReachSedimentLayers", parent=parent + name = ( + self._pamhyr_name + " - " + + study.name + " - " + + self._reach.name + ) + + super(ReachSedimentLayersWindow, self).__init__( + title = name, + study = study, + config = config, + parent = parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " + self._study.name + " - " + self._reach.name - ) - - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() @@ -129,11 +121,6 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): .clicked\ .connect(self.apply_sl_each_profile) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - self._table.layoutChanged\ .connect(self._update_plot) self._table.dataChanged\ @@ -152,17 +139,17 @@ class ReachSedimentLayersWindow(ASubMainWindow, ListedSubWindow): ) ) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() self._update_plot() - def redo(self): + def _redo(self): self._table.redo() self._update_plot() diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index 6c14ae6c..c4010745 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -1,11 +1,26 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + # -*- coding: utf-8 -*- import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -38,37 +53,33 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="SedimentLayersList", study=None, parent=None): - self._study = study - self._sediment_layers = self._study.river.sediment_layers +class SedimentLayersWindow(PamhyrWindow): + _pamhyr_ui = "SedimentLayersList" + _pamhyr_name = "Sediment Layers List" - self.setup_title(title) + def __init__(self, study=None, config=None, parent=None): + self._sediment_layers = study.river.sediment_layers - super(SedimentLayersWindow, self).__init__( - name=self._title, ui="SedimentLayersList", parent=parent + name = ( + self._pamhyr_name + " - " + study.name + ) + + super(SedimentLayersWindow, self).__init__( + title = name, + study = study, + config = config, + parent=parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_connections() - self.ui.setWindowTitle(self._title) - def setup_title(self, title): self._title = ( title + " - " + self._study.name ) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() @@ -94,11 +105,6 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self.find(QAction, "action_del").triggered.connect(self.delete) self.find(QAction, "action_edit").triggered.connect(self.edit_sediment_layers) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - table = self.find(QTableView, f"tableView") table.selectionModel()\ .selectionChanged\ @@ -148,16 +154,16 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow): self._table.delete(rows) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() def edit_sediment_layers(self): diff --git a/src/View/ui/ProfileSedimentLayers.ui b/src/View/ui/ProfileSedimentLayers.ui index b9f5106d..05f6618e 100644 --- a/src/View/ui/ProfileSedimentLayers.ui +++ b/src/View/ui/ProfileSedimentLayers.ui @@ -39,20 +39,6 @@ - - - toolBar - - - TopToolBarArea - - - false - - - - - From ca10f19fe53990493f9adac8d14a88ecab75c647 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 12:03:41 +0200 Subject: [PATCH 15/44] refactoring: Continue apply PamhyrWindow. --- src/View/CheckList/Window.py | 28 ++++++++-------- src/View/MainWindow.py | 8 ++--- src/View/Results/Window.py | 51 +++++++++++++---------------- src/View/RunSolver/Log/Window.py | 24 +++++++------- src/View/RunSolver/Window.py | 42 +++++++++++------------- src/View/SolverParameters/Window.py | 43 +++++++++++------------- 6 files changed, 92 insertions(+), 104 deletions(-) diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index 4c40ad1f..b02522bc 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -18,8 +18,7 @@ from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -44,30 +43,31 @@ from View.CheckList.Worker import Worker _translate = QCoreApplication.translate -class CheckListWindow(ASubMainWindow, ListedSubWindow): +class CheckListWindow(PamhyrWindow): + _pamhyr_ui = "CheckList" + _pamhyr_name = "Check list" signalStatus = pyqtSignal(str) - def __init__(self, title="Check list", - autorun:bool=True, + def __init__(self, autorun:bool=True, study=None, config=None, solver=None, parent=None): - self._title = title + " - " + study.name - self._autorun = autorun - - self._study = study - self._config = config self._solver = solver - self._parent = parent + + name = self._pamhyr_name + " - " + study.name super(CheckListWindow, self).__init__( - name=self._title, ui="CheckList", parent=parent + title = name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self._checker_list = ( - self._study.checkers() + self._solver.checkers() + self._study.checkers() + + self._solver.checkers() ) self.setup_table() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 002ee11b..01e7a3b0 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -52,10 +52,10 @@ from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow from View.SedimentLayers.Window import SedimentLayersWindow from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow -# from View.SolverParameters.Window import SolverParametersWindow -# from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow -# from View.CheckList.Window import CheckListWindow -# from View.Results.Window import ResultsWindow +from View.SolverParameters.Window import SolverParametersWindow +from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow +from View.CheckList.Window import CheckListWindow +from View.Results.Window import ResultsWindow # from View.Debug.Window import ReplWindow from Model.Study import Study diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 251f5a13..4009c836 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -21,8 +21,7 @@ from datetime import datetime from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -63,23 +62,32 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class ResultsWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Results", - study=None, solver=None, results=None, +class ResultsWindow(PamhyrWindow): + _pamhyr_ui = "Results" + _pamhyr_name = "Results" + + def __init__(self, study=None, config=None, + solver=None, results=None, parent=None): - self._study = study self._solver = solver self._results = results self._timestamps = sorted(list(self._results.get("timestamps"))) - self.setup_title(title) - - super(ResultsWindow, self).__init__( - name=title, ui="Results", parent=parent + name = ( + self._pamhyr_name + " - " + + study.name + " - " + + self._solver.name + " - " + + self._results.date + ) + + super(ResultsWindow, self).__init__( + title = name, + study = study, + config = config, + parent=parent ) - self.setup_sc() self.setup_table() self.setup_graph() self.setup_slider() @@ -96,14 +104,6 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): + self._results.date ) - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - def setup_table(self): retranslate() self._table = {} @@ -289,11 +289,6 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): self._status_label.setText(txt) def setup_connections(self): - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - fun = { "reach": self._set_current_reach, "profile": self._set_current_profile, @@ -411,14 +406,14 @@ class ResultsWindow(ASubMainWindow, ListedSubWindow): timestamp = self._timestamps[self._slider_time.value()] self.update(timestamp = timestamp) - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") - def undo(self): + def _undo(self): self._table.undo() - def redo(self): + def _redo(self): self._table.redo() diff --git a/src/View/RunSolver/Log/Window.py b/src/View/RunSolver/Log/Window.py index 93d42165..2a33311f 100644 --- a/src/View/RunSolver/Log/Window.py +++ b/src/View/RunSolver/Log/Window.py @@ -22,8 +22,7 @@ import os from queue import Queue from tools import trace, timer -from View.ASubWindow import ASubWindow, ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog, PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -45,24 +44,23 @@ from PyQt5.QtWidgets import ( _translate = QCoreApplication.translate -class SolverLogFileWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Solver logs", - file_name = None, +class SolverLogFileWindow(PamhyrWindow): + _pamhyr_ui = "SolverLogFile" + _pamhyr_name = "Solver logs" + + def __init__(self, file_name = None, study=None, config=None, solver=None, parent=None): - self._title = title - self._parent = parent - - self._study = study - self._config = config self._solver = solver - self._file_name = file_name super(SolverLogFileWindow, self).__init__( - name=self._title, ui="SolverLogFile", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self.setup_action() self.setup_connections() diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 226d4da5..72a65d45 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -23,8 +23,7 @@ import tempfile from queue import Queue from tools import trace, timer -from View.ASubWindow import ASubWindow, ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrDialog, PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -56,20 +55,21 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class SelectSolverWindow(ASubWindow, ListedSubWindow): - def __init__(self, title="Select solver", - study=None, config=None, - parent=None): - self._title = title +class SelectSolverWindow(PamhyrDialog): + _pamhyr_ui = "SelectSolver" + _pamhyr_name = "Select solver" - self._study = study - self._config = config + def __init__(self, study=None, config=None, + parent=None): self._solver = None super(SelectSolverWindow, self).__init__( - name=self._title, ui="SelectSolver", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self.setup_combobox() self.setup_connections() @@ -101,24 +101,22 @@ class SelectSolverWindow(ASubWindow, ListedSubWindow): super(SelectSolverWindow, self).accept() +class SolverLogWindow(PamhyrWindow): + _pamhyr_ui = "SolverLog" + _pamhyr_name = "Solver Log" -class SolverLogWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Solver logs", - study=None, config=None, + def __init__(self, study=None, config=None, solver=None, parent=None): - self._title = title - self._parent = parent - - self._study = study - self._config = config self._solver = solver - self._results = None super(SolverLogWindow, self).__init__( - name=self._title, ui="SolverLog", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self.setup_action() self.setup_alarm() diff --git a/src/View/SolverParameters/Window.py b/src/View/SolverParameters/Window.py index c04eebc7..a3b225a6 100644 --- a/src/View/SolverParameters/Window.py +++ b/src/View/SolverParameters/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -51,23 +50,28 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class SolverParametersWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Solver parameters", study=None, parent=None): +class SolverParametersWindow(PamhyrWindow): + _pamhyr_ui = "SolverParameters" + _pamhyr_name = "Solver parameters" + + def __init__(self, study=None, config=None, parent=None): # Init tanslate dictionary tr.init() - self._title = title + " - " + study.name + name = self._pamhyr_name + " - " + study.name super(SolverParametersWindow, self).__init__( - name=title, ui="SolverParameters", parent=parent + title = name, + study = study, + config = config, + options = ['copy'], + parent = parent ) - self._study = study self._params = self._study.river.parameters self.setup_sc() self.setup_table() - self.setup_connections() self.ui.setWindowTitle(self._title) @@ -77,10 +81,10 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): for st in solver_type_list: self._undo_stack[st] = QUndoStack() - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) + self._undo_sc = QShortcut(QKeySequence.Undo, self) + self._redo_sc = QShortcut(QKeySequence.Redo, self) + self._undo_sc.activated.connect(self._undo) + self._redo_sc.activated.connect(self._redo) def setup_table(self): self._table = {} @@ -115,31 +119,24 @@ class SolverParametersWindow(ASubMainWindow, ListedSubWindow): # Create tab self._tab_widget.addTab(widget, f"{solver_long_name[st]}") - - def setup_connections(self): - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - def current_tab(self): return self.find(QTabWidget, "tabWidget")\ .currentWidget()\ .objectName()\ .replace("tab_", "") - def undo(self): + def _undo(self): tab = self.current_tab() self._table[tab].undo() self._set_current_reach() - def redo(self): + def _redo(self): tab = self.current_tab() self._table[tab].redo() self._set_current_reach() - def copy(self): + def _copy(self): logger.info("TODO: copy") - def paste(self): + def _paste(self): logger.info("TODO: paste") From 624e9b6f71b72ab208b960efc25a6c5c02f4575a Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 12:07:10 +0200 Subject: [PATCH 16/44] refactoring: Apply PamhyrWindow on Debug window. --- src/View/Debug/Window.py | 24 +++++++++++------------- src/View/MainWindow.py | 2 +- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/View/Debug/Window.py b/src/View/Debug/Window.py index 9b14aa20..2e99dadb 100644 --- a/src/View/Debug/Window.py +++ b/src/View/Debug/Window.py @@ -20,8 +20,7 @@ import logging from tools import trace, timer -from View.ASubWindow import ASubMainWindow -from View.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow from PyQt5.QtGui import ( QKeySequence, @@ -45,20 +44,19 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class ReplWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="Debug REPL", - study=None, config=None, +class ReplWindow(PamhyrWindow): + _pamhyr_ui = "DebugRepl" + _pamhyr_name = "Debug REPL" + + def __init__(self, study=None, config=None, solver=None, parent=None): - self._title = title - - self._study = study - self._config = config - self._parent = parent - super(ReplWindow, self).__init__( - name=self._title, ui="DebugRepl", parent=parent + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent ) - self.ui.setWindowTitle(self._title) self.__debug_exec_result__ = None self._history = [] diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 01e7a3b0..cb9d8b7e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -56,7 +56,7 @@ from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow from View.Results.Window import ResultsWindow -# from View.Debug.Window import ReplWindow +from View.Debug.Window import ReplWindow from Model.Study import Study From e7fd2077bd3f7c390488fc352df98205026a5d75 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 14:19:22 +0200 Subject: [PATCH 17/44] doc: dev: Update with PamhyrWindow, and minor change in hash computation. --- doc/dev/documentation.org | 40 +++++++++++++++++-------------- src/View/Tools/ListedSubWindow.py | 7 ++++-- src/View/Tools/PamhyrWindow.py | 13 ++++------ 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 16d6a69c..c5266896 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -445,33 +445,37 @@ window, and =/src/View/ui/Widgets= for custom widget. *** Window -An abstract class ASubMainWindow is used for most of Pamhyr2 -windows. This class implemente some useful method allows to easy load -an window UI file. In addition, the abstract ListedSubWindow must be -also inherits by a window class, it is allow to keep a list of open -windows and avoid window duplicate. +The abstract class PamhyrWindow and PamhyrDialog are used for most of +Pamhyr2 window. These class allow to create an window for Pamhyr2 GUI +and implemente some useful methods. #+NAME: window #+CAPTION: Example of Pamhyr2 window #+begin_src python :python python3 :results output :noweb yes - class MyWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, title="My window", - study=None, config=None, parent=None): - self._study = study - self._config = config - self._parent = parent + from View.Tools.PamhyrWindow import PamhyrWindow - self._title = title + " - " + study.name + class MyWindow(PamhyrWindow): + _pamhyr_ui = "MyUI" + _pamhyr_name = "My window" + + def __init__(self, my_data=None, + study=None, config=None, + parent=None): + self._my_data = my_data super(MyWindow, self).__init__( - name=title, # The windows name for windows - # registration (avoid duplicate) - ui="My", # The ui file name (here "My.ui") - parent=parent # The parent window + # Window title + title = self._pamhyr_name + " - " + study.name, + # Window standard data + study = study, config = config, parent = parent, + # Activate undo/redo and copy/paste shortcut + options = ["undo", "copy"] ) - # Set Windows title - self.ui.setWindowTitle(self._title) + # Add custom data to hash window computation + self._hash_data.append(self._my_data) + + # Setup custom window components self.setup_table() self.setup_connections() diff --git a/src/View/Tools/ListedSubWindow.py b/src/View/Tools/ListedSubWindow.py index f671cc60..6c93b84e 100644 --- a/src/View/Tools/ListedSubWindow.py +++ b/src/View/Tools/ListedSubWindow.py @@ -36,7 +36,10 @@ class ListedSubWindow(object): def sub_win_add(self, name, win): self.sub_win_list.append((name, win)) self.sub_win_cnt += 1 - logger.info(f"Open window: {name} ({self.sub_win_cnt})") + try: + logger.info(f"Open window: {name}: {self.sub_win_cnt}: {win.hash()}") + except: + logger.info(f"Open window: {name}: {self.sub_win_cnt}: X") def sub_win_del(self, name): self.sub_win_list = list( @@ -46,7 +49,7 @@ class ListedSubWindow(object): ) ) self.sub_win_cnt = len(self.sub_win_list) - logger.info(f"Close window: {name} ({self.sub_win_cnt})") + logger.info(f"Close window: {name}: {self.sub_win_cnt}") def _sub_win_exists(self, name): return reduce( diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py index d6ae6752..34ff547f 100644 --- a/src/View/Tools/PamhyrWindow.py +++ b/src/View/Tools/PamhyrWindow.py @@ -35,6 +35,7 @@ class PamhyrWindowTools(object): def __init__(self, options = ["undo", "copy"], parent = None, **kwargs): super(PamhyrWindowTools, self).__init__() + self._hash_data = [] self._undo_stack = None if "undo" in options: @@ -130,12 +131,7 @@ class PamhyrWindowTools(object): Returns: The hash """ - data = [ - self._study, - self._config, - ] - - return self._hash() + return self._hash(self._hash_data) class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): @@ -152,14 +148,15 @@ class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): self._config = config self._parent = parent - logger.info(self._pamhyr_name) - super(PamhyrWindow, self).__init__( name = self._pamhyr_name, ui = self._pamhyr_ui, parent = parent, ) + self._hash_data.append(self._study) + self._hash_data.append(self._config) + self._set_title() From 2e94348bb8026fff36d625a1ddbbfcbeba2a85d5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 26 Sep 2023 17:16:53 +0200 Subject: [PATCH 18/44] refactoring: Add PamhyrTranslate class for GUI translation and document it. --- doc/dev/documentation.org | 103 ++++++++++++++++++------------ src/View/Network/Window.py | 14 ++-- src/View/Network/translate.py | 30 ++++----- src/View/Tools/PamhyrTranslate.py | 56 ++++++++++++++++ src/View/Tools/PamhyrWindow.py | 12 ++-- 5 files changed, 145 insertions(+), 70 deletions(-) create mode 100644 src/View/Tools/PamhyrTranslate.py diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index c5266896..ed4bc222 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -443,49 +443,83 @@ than hand made windows and widget creation, and saves us some purely descriptive code. The UI files are saved into =src/View/ui= for window, and =/src/View/ui/Widgets= for custom widget. +*** Translate + +#+NAME: pamhyr-trad +#+CAPTION: Example of =PamhyrTranslate= class implementation with a global traduction for /FooBar/ and a additional dictionary =table_headers= +#+begin_src python :python python3 :results output :noweb yes + from PyQt5.QtCore import QCoreApplication + from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate + + class MyTranslate(PamhyrTranslate): + def __init__(self): + super(MyTranslate, self).__init__() + + # Add traduction to global dictionary + self._dict["My"] = _translate("My", "FooBar") + # Add an additional translate dictionary + self._sub_dict["table_headers"] = { + "foo": _translate("My", "Foo"), + "bar": _translate("My", "Bar"), + "baz": _translate("My", "Baz"), + } +#+end_src + *** Window The abstract class PamhyrWindow and PamhyrDialog are used for most of Pamhyr2 window. These class allow to create an window for Pamhyr2 GUI -and implemente some useful methods. +and implemente some useful methods. The super class method difine some +generic value from optional parameters, for examples: +- =self._study=: The study giving in constructor parameters =study= + (typically a =Model.Study= class object) +- =self._config=: The configuration giving in constructor parameters + =config= (typically a =Config= class object) +- =self._trad=: The traductor dictionary giving in constructor + parameters =trad= (typically a =Model.Tools.PamhyrTranslate= class + object) #+NAME: window #+CAPTION: Example of Pamhyr2 window #+begin_src python :python python3 :results output :noweb yes - from View.Tools.PamhyrWindow import PamhyrWindow + from View.Tools.PamhyrWindow import PamhyrWindow + from View.My.Translate import MyTranslate - class MyWindow(PamhyrWindow): - _pamhyr_ui = "MyUI" - _pamhyr_name = "My window" + class MyWindow(PamhyrWindow): + _pamhyr_ui = "MyUI" + _pamhyr_name = "My window" - def __init__(self, my_data=None, - study=None, config=None, - parent=None): - self._my_data = my_data + def __init__(self, my_data=None, + study=None, config=None, + parent=None): + self._my_data = my_data - super(MyWindow, self).__init__( - # Window title - title = self._pamhyr_name + " - " + study.name, - # Window standard data - study = study, config = config, parent = parent, - # Activate undo/redo and copy/paste shortcut - options = ["undo", "copy"] - ) + super(MyWindow, self).__init__( + # Window title + title = self._pamhyr_name + " - " + study.name, + # Window standard data + study = study, config = config, + trad = MyTranslate(), + parent = parent, + # Activate undo/redo and copy/paste shortcut + options = ["undo", "copy"] + ) - # Add custom data to hash window computation - self._hash_data.append(self._my_data) + # Add custom data to hash window computation + self._hash_data.append(self._my_data) - # Setup custom window components - self.setup_table() - self.setup_connections() + # Setup custom window components + self.setup_table() + self.setup_connections() - def setup_table(self): - # Init table(s)... + def setup_table(self): + # Init table(s)... - def setup_connections(self): - # Init action connection(s)... + def setup_connections(self): + # Init action connection(s)... - # ... + # ... #+end_src Typically we called method =setup_*=, the method to initialize some @@ -499,20 +533,6 @@ and =setData= methode to implement, but the constructor needs more information than a classic QAbstractTableModel class. #+begin_src python :python python3 :results output :noweb yes - # Table headers with display name translatable (translate.py) - table_headers = { - "foo": _translate("My", "Foo"), - "bar": _translate("My", "Bar"), - "baz": _translate("My", "Baz"), - } - - def retranslate(): - # Hack because dict definition is not able to be - # translate... (need to be called in window '__init__') - table_headers["foo"] = _translate("My", "Foo") - table_headers["bar"] = _translate("My", "Bar") - table_headers["baz"] = _translate("My", "Baz") - # Table model definition (Table.py) class MyTableModel(PamhyrTableModel): def data(self, index, role): @@ -523,6 +543,7 @@ information than a classic QAbstractTableModel class. # Set VALUE at INDEX... # Table model creation (Window.py) + table_headers = self._trad.get_dict("table_headers") self._model = MyTableModel( table_view = table, # The table view object table_headers = table_headers, # The table column headers dict diff --git a/src/View/Network/Window.py b/src/View/Network/Window.py index a54c0527..f0458596 100644 --- a/src/View/Network/Window.py +++ b/src/View/Network/Window.py @@ -41,10 +41,7 @@ from Model.River import RiverNode, RiverReach, River from View.Tools.PamhyrWindow import PamhyrWindow from View.Network.GraphWidget import GraphWidget from View.Network.UndoCommand import * -from View.Network.translate import ( - table_headers_node, table_headers_edge, - retranslate, -) +from View.Network.translate import NetworkTranslate from View.Network.Table import ( ComboBoxDelegate, NodeTableModel, EdgeTableModel, ) @@ -63,24 +60,25 @@ class NetworkWindow(PamhyrWindow): title = self._pamhyr_name + " - " + study.name, study = study, config = config, + trad = NetworkTranslate(), options = ['undo'], parent=parent, ) self._graph = study.river + self._table_headers_node = self._trad.get_dict("table_headers_node") + self._table_headers_edge = self._trad.get_dict("table_headers_edge") self.setup_graph() self.setup_table() self.setup_connections() def setup_table(self): - retranslate() - # Nodes table table = self.find(QTableView, "tableView_nodes") self._nodes_model = NodeTableModel( table_view = table, - table_headers = table_headers_node, + table_headers = self._table_headers_node, editable_headers = ["name"], data = self._graph, undo = self._undo_stack, @@ -99,7 +97,7 @@ class NetworkWindow(PamhyrWindow): self._reachs_model = EdgeTableModel( table_view = table, - table_headers = table_headers_edge, + table_headers = self._table_headers_edge, editable_headers = ["name", "node1", "node2"], delegates = { "node1": self.delegate_combobox, diff --git a/src/View/Network/translate.py b/src/View/Network/translate.py index 24935e1b..44e97cf5 100644 --- a/src/View/Network/translate.py +++ b/src/View/Network/translate.py @@ -18,23 +18,21 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers_node = { - "name": _translate("Network", "Name"), - "type": _translate("Network", "Type"), -} +class NetworkTranslate(PamhyrTranslate): + def __init__(self): + super(NetworkTranslate, self).__init__() -table_headers_edge = { - "name": _translate("Network", "Name"), - "node1": _translate("Network", "Source node"), - "node2": _translate("Network", "Destination node"), -} + self._sub_dict["table_headers_node"] = { + "name": _translate("Network", "Name"), + "type": _translate("Network", "Type"), + } -def retranslate(): - table_headers_node["name"] = _translate("Network", "Name") - table_headers_node["type"] = _translate("Network", "Type") - - table_headers_edge["name"] = _translate("Network", "Name") - table_headers_edge["node1"] = _translate("Network", "Source node") - table_headers_edge["node2"] = _translate("Network", "Destination node") + self._sub_dict["table_headers_edge"] = { + "name": _translate("Network", "Name"), + "node1": _translate("Network", "Source node"), + "node2": _translate("Network", "Destination node"), + } diff --git a/src/View/Tools/PamhyrTranslate.py b/src/View/Tools/PamhyrTranslate.py new file mode 100644 index 00000000..83c8da64 --- /dev/null +++ b/src/View/Tools/PamhyrTranslate.py @@ -0,0 +1,56 @@ +# PamhyrTranslate.py -- Pamhyr +# Copyright (C) 2023 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate + + +class PamhyrTranslate(object): + def __init__(self): + # Module global dictionnary + self._dict = {} + # Module sub dictionnary + self._sub_dict = {} + + def __getitem__(self, index): + if index not in self._dict: + return None + return self._dict[index] + + def _get_in_sub_dict(self, dictionary, index): + if dictionary not in self._sub_dict: + return None + return self._sub_dict[index] + + def get(self, dictionary, index): + if dictionary == "global": + return self[index] + + return self._get_in_sub_dict(dictionary, index) + + def get_dict(self, dictionary): + _dict = None + + if dictionary == "global": + _dict = self._dict + + if dictionary in self._sub_dict: + _dict = self._sub_dict[dictionary] + + return _dict diff --git a/src/View/Tools/PamhyrWindow.py b/src/View/Tools/PamhyrWindow.py index 34ff547f..fb4bf1a1 100644 --- a/src/View/Tools/PamhyrWindow.py +++ b/src/View/Tools/PamhyrWindow.py @@ -140,12 +140,13 @@ class PamhyrWindow(ASubMainWindow, ListedSubWindow, PamhyrWindowTools): def __init__(self, title = "Pamhyr2", - study = None, config = None, + study = None, config = None, trad = None, options = ["undo", "copy"], parent = None): self._title = title self._study = study self._config = config + self._trad = trad self._parent = parent super(PamhyrWindow, self).__init__( @@ -166,21 +167,22 @@ class PamhyrDialog(ASubWindow, ListedSubWindow, PamhyrWindowTools): def __init__(self, title = "Pamhyr2", - study = None, config = None, + study = None, config = None, trad = None, options = ["undo", "copy"], parent = None): self._title = title self._study = study self._config = config + self._trad = trad self._parent = parent - logger.info(self._pamhyr_name) - logger.info(self._pamhyr_ui) - super(PamhyrDialog, self).__init__( name = self._pamhyr_name, ui = self._pamhyr_ui, parent = parent, ) + self._hash_data.append(self._study) + self._hash_data.append(self._config) + self._set_title() From f93f3fe12278b05d904cc6e2745dc3b6ed1bb743 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 09:00:18 +0200 Subject: [PATCH 19/44] doc: dev: Update Table and UndoCommand. --- doc/dev/documentation.org | 52 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index ed4bc222..f12426b2 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -485,6 +485,7 @@ generic value from optional parameters, for examples: #+begin_src python :python python3 :results output :noweb yes from View.Tools.PamhyrWindow import PamhyrWindow from View.My.Translate import MyTranslate + from View.My.Table import MyTableModel class MyWindow(PamhyrWindow): _pamhyr_ui = "MyUI" @@ -532,8 +533,11 @@ QAbstractTableModel shortly. In simple cases, there are only =data= and =setData= methode to implement, but the constructor needs more information than a classic QAbstractTableModel class. +#+NAME: table-src +#+CAPTION: Definition of a table model from =PamhyrTableModel= in a file =View/My/Table.py=. #+begin_src python :python python3 :results output :noweb yes - # Table model definition (Table.py) + from View.Tools.PamhyrTable import PamhyrTableModel + class MyTableModel(PamhyrTableModel): def data(self, index, role): # Retrun data at INDEX... @@ -541,8 +545,11 @@ information than a classic QAbstractTableModel class. @pyqtSlot() def setData(self, index, value, role=Qt.EditRole): # Set VALUE at INDEX... +#+end_src - # Table model creation (Window.py) +#+CAPTION: Using the table model defined in Listing [[table-src]] in window funtion =setup_table= defined Listing [[window]]. +#+begin_src python :python python3 :results output :noweb yes + # Table model creation (Window.py: setup_table) table_headers = self._trad.get_dict("table_headers") self._model = MyTableModel( table_view = table, # The table view object @@ -553,11 +560,48 @@ information than a classic QAbstractTableModel class. "bar": self.my_delegate, # Custom delegate for column 'bar' }, data = self._my_lst, # The data - undo = self._undo_stack, # The undo command stack + undo = self._undo_stack, # The window undo command stack + ) +#+end_src + +*** UndoCommand + +All model modification must be done by an QUndoCommand, this command +allow to undo and redo an action. This a Qt class wi can inherit to +define custom undo command (see example Listing [[undo-cmd]]) + +#+NAME: undo-cmd +#+CAPTION: Example of custom UndoCommand, this command allow to add a node to graph in river network window (method redo), and delete it to graph with undo method +#+begin_src python :python python3 :results output :noweb yes +class AddNodeCommand(QUndoCommand): + def __init__(self, graph, node): + QUndoCommand.__init__(self) + + self._graph = graph + self._node = node + + def undo(self): + self._graph.remove_node(self._node.name) + + def redo(self): + self._graph.insert_node(self._node) +#+end_src + +All undo command must be push into a =QUndoStack= (see Listing +[[undo-cmd-push]]) to perform the action and allow user undo and redo this +action. In PamhyrWindow (and PamhyrDialog) the undo stack is +automatically create if the option ="undo"= is activate at window +creation, this stack is accessible at =self._undo_stack=. + +#+begin_src python :python python3 :results output :noweb yes + self._undo_stack.push( + AddNodeCommand( + self._graph, + node + ) ) #+end_src -*** TODO UndoCommand *** TODO Plot *** TODO Mainwindow ** TODO Solver From a81564e582d46657aa7025a1a02239bc707b7fa6 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 09:20:17 +0200 Subject: [PATCH 20/44] doc: dev: Write Plot section. --- doc/dev/documentation.org | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index f12426b2..6ec31a3c 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -602,8 +602,35 @@ creation, this stack is accessible at =self._undo_stack=. ) #+end_src -*** TODO Plot -*** TODO Mainwindow +*** Plot + +To define a new plot you can create a class who inherit to APlot. The +creator need threee argument: + - A =canvas= of type =MplCanvas= + - A =data= used in =draw= and =update= to create and update the plot + - A optional =toolbar= of type =PamhyrToolbar= +This class must implement two method =draw= and =update=, the first +method to draw the plot from scratch, the second to update the plot if +data has changed. + +#+begin_src python :python python3 :results output :noweb yes + from View.Plot.APlot import APlot + + class MyPlot(APlot): + def __init__(self, canvas=None, data=None, toolbar=None): + super(MyPlot, self).__init__( + canvas=canvas, + data=data, + toolbar=toolbar + ) + + def draw(self): + # Draw function code... + + def update(self, ind = None): + # Update function code... +#+end_src + ** TODO Solver #+name: graph-pipeline From ab2e307e3d2d860f4a80a4f76f47301eae67b691 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 12:10:47 +0200 Subject: [PATCH 21/44] doc: dev: Update graphs. --- doc/dev/documentation.org | 265 ++++++++++++++++++++++++++++++++++---- 1 file changed, 242 insertions(+), 23 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 6ec31a3c..cb40b018 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -97,35 +97,68 @@ simulation results to model data. #+name: graph-architecture #+header: :results drawer #+header: :exports results -#+header: :post attr_wrap(width="9cm", data=*this*, name="graph-architecture", caption="Pamhyr2 Model/View architecture scheme (inspired by Qt Model/View architecture [[https://doc.qt.io/qt-5/model-view-programming.html]])", float="t") +#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-architecture", caption="Pamhyr2 Model/View architecture scheme (inspired by Qt Model/View architecture [[https://doc.qt.io/qt-5/model-view-programming.html]])", float="t") #+begin_src dot :file "images/graph-architecture.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=white]; + edge[colorscheme=set19,color=0]; + + root[style=invis]; + + subgraph cluster0 { + label="File System" + style=dashed; + + save[label="Pamhyr save",fillcolor="9",shape=note]; + sbin[label="Solver binary",fillcolor="9",shape=note]; + configfile[label="Pamhyr configuration file",fillcolor="9",shape=note]; + } - save[label="Pamhyr save",fillcolor="9",shape=note]; - model[label="Model",fillcolor="2"]; - view[label="View",fillcolor="1"]; - delegate[label="Delegate",fillcolor="3"]; - action[label="Action",fillcolor="3"]; - solver[label="Solver",fillcolor="6"]; - undocommand[label="Command",fillcolor="4"]; user[label="User",shape=ellipse]; - model -> save[label="Save"]; - save -> model[label="Load"]; + subgraph cluster1 { + label="Pamhyr2"; + + config[label="Configuration",fillcolor="5"]; + model[label="Model",fillcolor="2"]; + view[label="View",fillcolor="1"]; + delegate[label="Delegate",fillcolor="3"]; + action[label="Action",fillcolor="3"]; + solver[label="Solver",fillcolor="6"]; + undocommand[label="Command",fillcolor="4"]; + } + + root -> model[style=invis]; + root -> config[style=invis]; + + model -> save[dir=both,label="Save/Load"]; + config -> configfile[dir=both,label="Save/Load"]; + + undocommand -> solver[style=invis]; + action -> solver[style=invis]; + delegate -> solver[style=invis]; model -> view[label="Rendering"]; view -> delegate[label="Rendering"]; delegate -> undocommand[label="Create"]; - action -> undocommand[label="Create"]; + action -> undocommand[label="Create/use"]; action -> solver[label="Run"]; - solver -> model[label="Add results"]; + solver -> model[dir=both,label="Export/Results",labelangle=0,labelfloat=true,constraint=true]; + solver -> sbin[dir=both,label="Execute/Results"]; undocommand -> model[label="Modify"]; view -> user[label="Vizualize"]; user -> delegate[label="Modify"]; - user -> action[label="Activate"]; + user -> action[label="Triggere"]; + + config -> solver[label="Create",style=dashed,labelfloat=true,constraint=true]; + //model -> config[style=invis]; + config -> model[style=invis]; + delegate -> action[style=invis]; + save -> configfile[style=invis]; + root -> solver[style=invis]; + user -> solver[style=invis]; } #+end_src @@ -134,6 +167,12 @@ All the model source code are in the directory {{{file(src/Model)}}} delegate and command are in {{{file(src/View)}}} (see section [[View]]). Solvers classes are in {{{file(src/Solver)}}} (see section [[Solver]]). +The following sub section show examples of main {{{pamhyr}}} internal +class for view componants, but this documentation is not exhaustive, +be free to watch existing code for more details and examples. In, +addition some features are not factorise and must be implemented from +scratch (directly with Qt for example). + [fn:qt-mv] The Qt Model/View documentation web page: https://doc.qt.io/qt-5/model-view-programming.html @@ -419,7 +458,7 @@ A abstract class PamhyrModelDict is available and provide some of basic methods for object dictionary in Model. This class is like PamhyrModelList but use a dictionary instead of list. -** TODO View +** View Pamhyr2 use Qt as graphical user interface library with the application "Qt designer" for windows or widget creation (see [[UI file]]) @@ -593,6 +632,8 @@ action. In PamhyrWindow (and PamhyrDialog) the undo stack is automatically create if the option ="undo"= is activate at window creation, this stack is accessible at =self._undo_stack=. +#+NAME: undo-cmd-push +#+CAPTION: Example of UndoCommand push into an undo stack. #+begin_src python :python python3 :results output :noweb yes self._undo_stack.push( AddNodeCommand( @@ -633,18 +674,128 @@ data has changed. ** TODO Solver +#+name: graph-multi-solver +#+header: :results drawer +#+header: :exports results +#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-multi-solver", caption="Scheme of multiple solver configured, one Rubarbe solver and two Mage solver with one on local machine and one on a distant machine accessed over ssh", float="t") +#+begin_src dot :file "images/graph-multi-solver.png" :cache no + digraph { + bgcolor="transparent"; + node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; + edge[colorscheme=set19,color=0]; + + subgraph cluster00 { + label="User personal computer"; + style=solid; + + subgraph cluster0 { + label="Pamhyr2"; + style=solid; + + // subgraph cluster01 { + // label="Core"; + //model[label="Model", fillcolor=2]; + //view[label="View", fillcolor=1]; + config[label="Configuration", fillcolor=5]; + + // view -> model -> view; + // } + + subgraph cluster02 { + label="Solver"; + style=dashed; + subgraph cluster021 { + label="Solver Classes"; + classSolverM7[label="Mage7", fillcolor=6]; + classSolverM8[label="Mage8", fillcolor=6]; + classSolverR[label="RubarBE", fillcolor=6]; + } + //classSolverX[label="Solver X Binding", fillcolor=6]; + + subgraph cluster022 { + label="Solver Object"; + solverM[label="Mage", fillcolor=6]; + solverM2[label="Mage over ssh", fillcolor=6]; + solverR[label="RubarBE", fillcolor=6]; + //solverX[label="Solver X", fillcolor=6]; + } + + classSolverM8 -> solverM [style=dashed]; + classSolverM8 -> solverM2[style=dashed]; + classSolverR -> solverR[style=dashed]; + //classSolverX -> solverX[style=dashed]; + } + + //config -> solverM[style="dotted"]; + //config -> solverR[style="dotted"]; + //config -> solverX[style="dotted"]; + + //model -> solverM; + //model -> solverM2; + //model -> solverR; + //model -> solverX; + } + + subgraph cluster1 { + label="File System"; + style=dashed; + + mage[label="Mage Binary",shape=note]; + //X[label="Solver X Binary"]; + rubarbe[label="RubarBE Binary",shape=note]; + ssh[label="ssh",shape=note]; + } + } + + //config -> X[style=invis]; + //model -> config[style=invis]; + config -> solverM[label="",constraint=true]; + //config -> solverX[label="",constraint=true]; + config -> solverR[label="",constraint=true]; + config -> solverM2[label="",constraint=true]; + + subgraph cluster2 { + label="Distant server"; + style=solid; + + sshd[label="sshd"]; + + subgraph cluster21 { + label="File System"; + style=dashed; + + mage2[label="Mage Binary",shape=note]; + } + } + + solverM -> mage[label="", color=1]; + mage -> solverM[label="", color=2]; + + //solverX -> X[label="", color=1]; + //X -> solverX[label="", color=2]; + + solverR -> rubarbe[label="", color=1]; + rubarbe -> solverR[label="", color=2]; + + solverM2 -> ssh -> sshd -> mage2[label="", color=1]; + mage2 -> sshd -> ssh -> solverM2[label="", color=2]; + } +#+end_src + + #+name: graph-pipeline #+header: :results drawer #+header: :exports results -#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-pipeline", caption="Pamhyr2 solver execution pipeline architecture scheme", float="t") +#+header: :post attr_wrap(width="10cm", data=*this*, name="graph-pipeline", caption="Pamhyr2 solver execution pipeline architecture scheme", float="t") #+begin_src dot :file "images/graph-pipeline.png" :cache no digraph { bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; + edge[colorscheme=set19,color=0]; subgraph cluster0 { label="Pamhyr2" - config[label="Config",fillcolor=5]; + config[label="Configuration",fillcolor=5]; model[label="Model",fillcolor=2]; obj[label="Solver",fillcolor=6]; results[label="Results",fillcolor=2]; @@ -658,18 +809,86 @@ data has changed. obj -> model[style="dashed"]; subgraph cluster1{ - label="System"; + label="File System"; + style=dashed; in[label="Solver input files",shape=note]; out[label="Solver output files",shape=note]; bin[label="Solver binary",shape=note]; } - obj -> in[label="Write (1)"]; - obj -> bin[label="Execute (2)"]; - bin -> in[label="Read (2.1)"]; - bin -> out[label="Write (2.2)"]; - obj -> results[label="Create (3)"]; - obj -> out[label="Read (3.1)"]; + obj -> in[label="Write (1)",color=1]; + obj -> bin[label="Execute (2)",color=1]; + bin -> in[label="Read (2.1)",color=1]; + bin -> out[label="Write (2.2)",color=2]; + obj -> results[label="Create (3)",color=2]; + obj -> out[label="Read (3.1)", color=2]; + view -> model[style="dashed"]; + view -> results[style="dashed"]; + } +#+end_src + +#+name: graph-pipeline-generic +#+header: :results drawer +#+header: :exports results +#+header: :post attr_wrap(width="12cm", data=*this*, name="graph-pipeline-generic", caption="Pamhyr2 generic solver execution pipeline architecture scheme", float="t") +#+begin_src dot :file "images/graph-pipeline-generic.png" :cache no + digraph { + bgcolor="transparent"; + node[colorscheme=set19,shape=box,style="filled",fillcolor=9]; + edge[colorscheme=set19,color=0]; + + subgraph cluster0 { + label="Pamhyr2" + config[label="Configuration",fillcolor=5]; + model[label="Model",fillcolor=2]; + obj[label="Generic solver",fillcolor=6]; + results[label="Results",fillcolor=2]; + view[label="View",fillcolor=1]; + + results -> model[style="dashed"]; + results -> obj[style="dashed"]; + } + + config -> obj[label=""]; + obj -> model[style="dashed"]; + + subgraph cluster1{ + label="File System"; + style=dashed; + + gin[label="Generic input file", shape=note,fillcolor=white]; + ibin[label="Input formater", shape=note]; + in[label="Solver input files",shape=note,fillcolor=white]; + out[label="Solver output files",shape=note,fillcolor=white]; + gout[label="Generic results file",shape=note,fillcolor=white]; + obin[label="Output formater", shape=note]; + bin[label="Solver binary",shape=note]; + } + + gin -> ibin[style=invis]; + ibin -> bin -> obin[style=invis]; + in -> bin[style=invis]; + obin -> gout[style=invis]; + + // Input format + obj -> gin[label="Write (1)",color=1]; + obj -> ibin[label="Execute (2)",color=1,style=dashed]; + ibin -> gin[label="Read (2.1)",color=1]; + ibin -> in[label="Write (2.2)",color=1]; + + // Solve + obj -> bin[label="Execute (3)",color=1,style=dashed]; + bin -> in[label="Read (3.1)",color=1]; + bin -> out[label="Write (3.2)",color=2]; + + // Output format + obj -> obin[label="Execute (4)",color=2,style=dashed]; + obin -> out[label="Read (4.1)",color=2]; + obin -> gout[label="Write (4.2)",color=2]; + + // Read results + obj -> results[label="Create (5)",color=2]; + obj -> gout[label="Read (5.1)", color=2]; view -> model[style="dashed"]; view -> results[style="dashed"]; } From d20cdfc47f81b8d2ad4214fc7a0ac59ff8300264 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 15:55:45 +0200 Subject: [PATCH 22/44] doc: dev: Complete Architecture section. --- doc/dev/documentation.org | 87 ++++++++++++++++++++++++--- doc/dev/images/python-debug-repl.png | Bin 0 -> 61865 bytes 2 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 doc/dev/images/python-debug-repl.png diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index cb40b018..7aa30aca 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -73,7 +73,7 @@ https://doc.qt.io/qt-5/model-view-programming.html (last access [fn:org] The org-mode website: https://orgmode.org/ (last access 2023-09-15) -* TODO Architecture +* Architecture Pamhyr2's architecture is based on Qt Model/View, see Figure [[graph-architecture]]. It is made up of several different components: the @@ -672,7 +672,30 @@ data has changed. # Update function code... #+end_src -** TODO Solver +** Solver + +The Pamhyr2 architecture allow to define multiple solver. A solver is +define by a: + - type + - name + - description, + - path + - command line pattern + - (optional) input formater path + - (optional) input formater command line + - (optional) output formater path + - (optional) output formater command line + +Let see Figure [[graph-multi-solver]], the application can implement +different solver type, this solver type implement the code for export +study to solver input format, and read the solver output to study +results. There exists a generic solver with a generic input and output +format, the type could be use to use a solver not implemented in +Pamhyr2, but this solver must can read/write input and output generic +format or use external script. There is possible to define different +solver with the same type, for example two differents version of the +same solver. Finaly, with input and output formater is possible to +execute a code on distant computer, for example, over ssh. #+name: graph-multi-solver #+header: :results drawer @@ -782,6 +805,16 @@ data has changed. } #+end_src +Let see Figure [[graph-pipeline]] the temporal order of action to run a +solver and get results: + - (1) Write solver input file(s) using the study data + - (2) Run the solver + - (2.1) The solver read the input file(s) + - (2.2) The solver compute results and write it to solver output + file(s) + - (3) Pamhyr2 create a Reuslts + - (3.1) The Pamhyr2 solver class read solver output file(s) and + complete Results with readed data #+name: graph-pipeline #+header: :results drawer @@ -811,8 +844,8 @@ data has changed. subgraph cluster1{ label="File System"; style=dashed; - in[label="Solver input files",shape=note]; - out[label="Solver output files",shape=note]; + in[label="Solver input file(s)",shape=note,fillcolor=white]; + out[label="Solver output file(s)",shape=note,fillcolor=white]; bin[label="Solver binary",shape=note]; } @@ -827,6 +860,10 @@ data has changed. } #+end_src +In case of generic solver (or a solver with input and output formater) +the temporal order of action is prensented in Figure +[[graph-pipeline-generic]]. + #+name: graph-pipeline-generic #+header: :results drawer #+header: :exports results @@ -858,8 +895,8 @@ data has changed. gin[label="Generic input file", shape=note,fillcolor=white]; ibin[label="Input formater", shape=note]; - in[label="Solver input files",shape=note,fillcolor=white]; - out[label="Solver output files",shape=note,fillcolor=white]; + in[label="Solver input file(s)",shape=note,fillcolor=white]; + out[label="Solver output file(s)",shape=note,fillcolor=white]; gout[label="Generic results file",shape=note,fillcolor=white]; obin[label="Output formater", shape=note]; bin[label="Solver binary",shape=note]; @@ -894,8 +931,40 @@ data has changed. } #+end_src -** TODO Unit tests -** TODO The debug mode +To implement a Solver in Pamhyr2, there exists a abstract class +=Solver.AbstractSolver=. A class who herits this class, must implement +different methods: + - =export=: Export the study to solver input file(s) + - =input_param=: Return the solver input parameter(s) as string + - =log_file=: Return the solver log file name as string + - =results=: Read the solver output file(s) and return a + =Model.Results= object. + +** Unit tests + +The unit tests is actually not implemented in Pamhyr2, it is a *work +in progress*. + +** The debug mode + +To activate an deactivate the Pamhyr2 debug mode you can open the +configuration window and type "Ctrl+G" or run Pamhyr2 with command +line: +#+begin_src shell +./Pamhyr2 debug +#+end_src + +This mode add some log and add two action in main window menu: +"About/Debug" open a window with Python Repl in current Python +environement, and "About/Debug SQLite" who open the application +SQLiteBrowser (if installed) on current Study to explore the study +data base file. + +#+NAME: debug-repl +#+ATTR_LATEX: :width 16cm +#+CAPTION: Pamhyr2 debug Python REPL +[[./images/python-debug-repl.png]] + * TODO Build the project The project uses gitlab-ci runners to build packages, but it is possible @@ -942,6 +1011,8 @@ winetricks installed. [fn:nsis] The NSIS web site: https://sourceforge.net/projects/nsis/ ** TODO Setup the CI environment +*** Linux +*** Windows (Wine) * Documentation files This document and the user documentation are org files. This text file diff --git a/doc/dev/images/python-debug-repl.png b/doc/dev/images/python-debug-repl.png new file mode 100644 index 0000000000000000000000000000000000000000..5cc506026c104ac044d71300430c0884c8d35fc2 GIT binary patch literal 61865 zcmb5WbzD|y+ct_i2BLtX(qPew2#8AAC?Q>vN;lFC1`3FXiim)OA|N0Q5;vhzN+TlD z(k0!!kM+#V^M3FCe*2F-e>gMD&AQjR)_I-hQP=p%%U<2RlVK+r8QE^hYnK(t$hMS{ zk!{-8u?;_Y_GDig{`QyMMM>oy_{)8VfiM1@-u{ZZy^^(&z0)mQLo#D4YfHo9cKWu4 zhE{ea*7g%yip9yu4w6Y;zM$;<`cH?8^FEdQ#VPAlNsi%nnNJ^BJdN<~IByWZs+m{EWhzOPq=nftG$E;{`qBJ0VAPs8{vpE5wcpmAu7qL8yzNsyub47j7$6)UD!(L>FG@^EiI!= z1;b{aKBX39*zHK_$fRz!TF6WO_ZmBF9=5i&-af;0MI(7W|B?^o*o=G1(NZohu29){ zfx51)E(1Y^^tQ2I79VGnH2!nT_~R|@Std#gA7F{qH9{R4VsSHR(!fvrp^TeBDDzX0y%pWPfelDwf<}l0r^S{@EP}8h4tx0Up-N&_R+c)cLuVfY!^of`Dc116R}NH^F<~HADDmAX zS^k@L^Lc5_Eh@XOyilEZTac1=waV1gG<|txg~ODYvcf)Dbu_-oEM5JWaps?Y7XMHw z7X_uSfx)uOTbXYWp0rHVl2Tm?%BS|F=iTNScxe_@rIggPE)+h-9`Mb;_4`e}qg%eK zX#D$Ffl@EHLV0{TrLUbg$Z6cR?9*^gI9$Vy{D8SfNMl}y)oa;!iQVtavNa?h;?aB~ z6^F@p{)n1TB!2&#%jZqc9<$nWxF)};m>J66R!;Rguz|XvUyvbSTTWZV+ih>NG>-Fz zB+)+NL7A+Unx5#Uo$#nG9%p7Njp;t{%)|PeaadOorJ4>Hqe>Pbo zSt3?(u4yx_6VNs`9=w@?_(G{L11pz#Nh^BlhsEU&I$AGVwp?(uUVKD#PN(kKPnO8- zD#Ej#Bbl12RM&TAu4mlQJ zb=#^9{p?f0n!4^UTevovDD(bFHcFJH_?6Y;(YY}~c0FuJUp04G<%F+^B!==G*&m=dR39K)|@?b4cCS@bLm89V-l^>}Zl`3CEW&U3*lRyV3T zQpd_D-a$ zy^hM|NiIEwKG)ROHOE&gbeLLqt&e68hN`aZoblW^d&;W5?6KawS4X8v$zJlNUnXYn z-wIAxMlKDqGYFq$BaKj~uQaK>Al)h(;r`OKvAe{F>S*76MbSVCn^S*`$0~!FHJ!6J zavo0Z!7c)e1pJ@42jqOrOKl;b-yIao3oLzFSp6$F-GH&$c@w~yW zpo301k7*Jv?A1e&(eiAsQ1z%@-nPFX#31PXrmVHlsb8$2BBizEIl6tpCz7retlwgZ z2z@+Idc(4!Wwra9OMhXFx`5U1?yE*m-wo@X;+SSFUFBfye|c9(sCebv>YUelji!v-wdpUP2YVS>*L8gZWp7%U3)Be7 z53kibiS3idMmdwZ%ky)DXOWZn)+F&WU-vgEzT!I)tisClN05Hoj>S6CBhq2*IBZd^ zb|!}(KS=VS_sA9R3VCb}YP4r^AKkj1w@Sy`q_euc=$p1aFI)H8VahGP zr|RPH!z%@|^RgBkvOz*V8#hD;&w1s&U;nevJ~eH{w6SzKmf~oZ+|k77B7^G%0aI_R z5@#Hh*iVEK8;Itoc5kYE=z)tnY-}1OsQ1T}chui$**!8T?wxSza@UmH%dV*?(;sHa z4sX2emyaFi*k8e8lgLjlkPyAM{f<<}(*2wto9-?qy%rRFH9XwX>|k)Vt}x@-Z*$LY zV)`okJaQftas)Uz2B-Xf;5DEyvF)~g68AuU8a5~-yF+-!CPgs)5$WXXOicg@%r5=FHhRhh1CZIG~1X+VSyPO z=ZdT1FO#IyroV@~C}#^QdSrQLxN5p}^huq^O$RC)>MQtopE18|AFuyVO#hLb?4zU+ zfA^G28oop=DF1k~@l^7|EbhO&o8-Wsm1b!~48YQAT^IPlfLD$hu~k+~}^ zzVeaKJF1^!6Lx*xjj_B|f6;KfsxJ>TYIR7@8utvf4$S?e{grAZ`7z_k@a@Li(_;=j z>N~$*Ij3$=c%E%zv6+vBWXV#;8@4_7cHWQNk^B=RlFbq8!otvJ#rcm#jFS!8cu2vb z8kUteSKmo)EW9w)QXXG^$s9Q+6p~>+^OKjx$U*&pn$F;-#>qccB46TCofM9T?7+I$ z27OPAcvq^GxSFh(K*uNg95p}dI`+LYdH;Lmr{Y;uq|>U-{W(_?otCwH+G+n&Qnxit zKbV#Z-DH~aJSpKqHI;wGfAM!?zzY|C>6J|fHl}R;IMo<_pL4Qxx#K^OSmLg^%+LsnJpC~c5@wzBQbnsd(MbNx(Co0QXgybgR{`0S!}YE-;D$wIK{W!l2| zGOy>&syjE z#*&NCwW)Mh-ZXCOz?}I-%Kkn|N_=>jdz1i_uoeg~sqXedq4d*>BT&-6auX`NfR_{H;2z36B%S-?daIw7h8#AD zg;f|?-x`ZN?ZIBclDNLMLb`wN-tD|TL1Ky!P@$)S<4iQwkw)XahEh{!wGGz}3DNS7 zDe$*{|NcFadi%C*QatH@fAXR)YxYo@;NIn3Y~Np8j*cQH`wt!}V@hak7<<)C!}Xso zM_d!1Bt6RCqWpt-D7!!5LOC@75r}WgU3_2XwCSI3zUVvicVGX{ebBN=5*y<`zpnM- z=pmHLe}2hE;rr$@OiyOCDgU_!b9nwA;8awf-e}8oGHO;pBch^=Hq(e;q{TVIC z|JMbdd;b^VU6K6HWcm9JVrRmrN9~30-Md$7yeB`lc~l{(zv%8i_q6)sb+gO~cJ_yT zeFpv30TLy5Hf^C3aXi*r9&kiZaQ5*_uQPC4TwJ zm6P^E^)p(B|9Pr2#rfBLFI~K7Vru$%*#2$f@Z`e6xtPmMA<@wX&CJb#NIx58cH0Zf zx!vFnPNbD~?4}?mf0LfR=g#lnH~YK(UDNdMj&oO{#69TvZr*vW{)F|M&eFoKbp3Bt zRWnlR0TM@Woxiqi18(Nxuo6XPqabr#qrA+~;EmFv->c@8CRffO%{3vaRx#|}f$ic_=6lLc0N}P@DL3z1^<5=qh-1hVS($dRcN=jO? z&1uQ*Z>NgO&JOOa2z1Kt{Qg~0N=l0CcAi#;>On!l7q4E?IsW-|K~pol*;&`8o+X;)WaiI`_`aqPRcZSnN<{L$3pa{N+K zlb#?$OM#03TFKPdszjAGk@P_!~em+`vJ!u?1dh+D6Oao7V_LM$b?tjKYmYZek<+LdzD3_L( zD=I49E7SX|moBfMaKYKxIb_t1)RC)|!opTUCL2$atr76~=BMu4oQ89g$C5b6ZS3s@ zgM)*WRa9Ec7OyhOw+_`hg+$w<@ zo&R*1p7;+{2lS}!e~v!-8O>vPd094I=9)H~HvZsLs}wi;%QSJa*(5^0`m%d(oZVsbH#d0JT?KlTg`Za;tJ z%2wQJMfYt~&?DLSnB?T$q>d*g-Jh_vBV?ORn|rZGHgDN-)_zDnWdEf7Su0CR6^(1z z8plW>|(Mi2I9p_m$pnzP2tk6*`GEuo%4MC`Tq9CU%#$tYey(h8;3>BZ66UAG2RWra#YGr3<=bqrOva%ZZbaxBcK3dwwRNZqZ zl={)RUlv01V{Ja@IdO3hhlWh>j)bqXZ`rbioRV_>#$B{(Y`x%j%P3W;P%2h&84V2v z?%+pg7)n}N)p!C)Ik~~-(!85mH9v|`g(Rk?8g~_W9A#&J9v1efs{f0HkXyF5nyTva z*RN$ZgTC?PjX&eZ_m?U$^ll2!fN#^S8IiY{Nh8Wa_EqWAW{n`N^BAKkBF zFRulvrsb16cJF56;Yn(oK>NF*q||uh#HC`-wV}406GoY#Vs7)-`2R&0w7C>-AE+m* zx)p{EU3Mg`W#u#d7Ym%xmOoy~8zS8qPG}Is4ZT&1Xo-39FADIb_;FU!H$Xx^NABkQ zm1I>K28LFDw!QzIVIrgCHaBk`lsmU3c<9if_`=8S?OFph;mvLj{`;ARQH6`5Ha(Yh zT!+~XA3iMWvXwh{0ob zKut$?&Bo^BM9~VbgM$MZW&oeZvOP)#qS6PgyyrFHrx!*Z{`+iGjo&RVilR{qS+#$C z!YuMi+(Y=t)h7mHt=VLyrKR{z^|YIt%(BB?zC5svilsWsv{Ajl`Q+6nOos#nf-qtY zU4l5|*m-#Ti`T}i-#3|!=MAzXCMJHZsyc$%mS;DZVeoA)C5tHL`;MBL8ZwNKndQ;c zUPn^sSvz(CFI)Y1ub|ETdTwqG2+v_zTT_z} zYGlF6IO$JkA*o~`ARu6NxG}!%BB{eFbq-=5i^r^jhK5FdgxbH=NybV(h0ZfIGt+y* zYl-53|CJ@t)2BmD>X%P;`>;9+cP-D2F5$~$QBuH|qm%31*TaQ~%ENWur=*md2rBC8 z2A1jF0(Ps8Iv0iJN6B>NJa!a7k2ufm)kPch_g7+WLb~o#TXB2dK0Zpy%J$rC7yh}2 z(kpRQ-p+krpZ#giQyQ-Qla5vAEnc-nxwQ|wKX~xqm`0YbWDxsFlra88mR z<0v51Pyu5~Y+FGgp$wCT!^aL#xXu5$_3b&EmP38$hKQ zF3%2&PJY?~nHs3E=F0;LAcOdPv42-t>!>mT%tq&2_4M>=oo@X5bKlI`n|x_r8=hTq zgsSm3zVl4{KMBVF4?f`Ky0E+UN})3Me&Gm8lZ|8*?xq{sQU45y*NKY;I{8TTsB{Q({x(Z(F;#vO>ekDu-eq^Pys87Z+zacdnRs@NCFR z*gtn~A5QJ#?Op%<`@P2D%khof6BAw%eu*b8T)6P#=TF?%y&9vEVf)DW3{RO3J4+!( zHa520j`11o7k2EtyidZy2nfCjyyVTtk1um`bN|=1O-Rdc<)XxQ(ea&^JXR6Nn!4)t z=T~}Pf4>Xc2LRlxEI|x>SPhuX@@i^68p&~B&e$58uY;5Acy#ZK1bm*?P_g`da zc-J&FHFJWm$;z?|3o`^rTmlRrIZarlzE-U=4AI_pUQ&{rXmr>Q*w10%;hX4$Ea*_y zFM@+NjV1rU!UNSAG7u>Ec~DSG+O7N3EwLdwmE9#i2_HTvi_gr==zqGi2^{EcgUK1! zSx$ln7#q*5Ep!D%Mn2I~P&#_ivqHo?4zuM%@# zOl$r)lN~`y42ju{SLanWpQ@s@;+Sb59cwuCK;Z{7<3=C2-GP^_^|3oJz*O+U4DCw5p z`ztc5R^syIr+~G$ZXLoLPfO#wc=00H`{ZOcF)`+thHFYnAlaJ$qtUAiUcPA@<_ndv zXvw%TeAq)T)XdE6g-v$kv}Up@_VSxIZ{EelWjIaf;9rJ^hu^(>mj+ZJ8{&3Yx2;93 zkPM_nK0(={tB5(iQPRkWebc5*QcL;VN@{I7@r}3m=z#F1xno)^*bg2gi;xABQWHPd zd|F#ID4B)5u|boXo|-a6rP|K8wFlD)wU1+?oJbKos$&Ks;d+RrNKh zh32=;M~>HWba_v%b~G)HM~<8S)1Mj`^O-MwP)+()Tf425CKZbU94!2J)WY06COVp| zP)ANx_2u{EpWU~KJ{-G$*5dt#5APr(VEf^dCRSGE{k9IyW6#^h%7^Vi5}I!@9lryL zlE2d_4iNg?yQ9z)PKL^Wc15bX@NjcqyM8?uq6VoWk^6>cTtw$8@A9~y@XH2lO;BZTw%7nT3d1$PWhNlA&roo2RFw`QBS zWEfJ&#s`52rvu+ol1l*q2}j79TFxFlheDd2=wc1}<_*k^)>!ET2JGdv!P`~G2!1Uk z^{8&A^UH(=lNSdR0H&MF0FWButCLz`l$mbbCH}`z z`k9~w+{kdrHDkUWTuXG`k%(?JTsbdh+%aMWKmJkf-0+l_>5HYzqj9IYx%i}uCC_=JDD?bPT3eorLC1Vm zV>JJ#WhO}S;il)$pM!US2cfA;DJV41TPyjOT|~3`_U#+yCP0Ubic0X&lE1I-%NhXY z^=qa+zV_GHJWKIg$=90je$l-F|4=jN{MXC3{KLg+Nfe0>e)ZdOQKjvrrHpS($l4)Y zMTp5*m7a>Ys(6`yNgMS!LvflO;M71`UH50SI?i{0{rY965+_R^P|hM&cJ|n@$G*P4 zcJhb2rRC+Rq0AT;Fx~%L&r`Lh=i>K9XIg6N^IBT%XE~ou#^&@y$i^$_#B!ASDRT!; zvj;|Dk~emr3efiR_gB)1;i{#A>}X0906S#es*dMyrYZ}ooLgzB%N+lH!pNJb)D5MK0}ga>kOb5cxc)f7xB{->~JTG z`{d`wu$ObN24Jg%RAk>%; zX$K7#Ptx%l?4S~xtzY6TzhToGaN2flu6eob^({*{+j0Ieh~I{MMZX z0y?soVRGV2N6j1t6Rufn1?iehQQrLGO)fh3LrHXFbw(yqbgAG)x84tPeMBNoohVv7 z%<8##sLN}ku-S<{$ghIM?eRjFR}|WWlFmn|*pXoV_34n}qg-6%=rfq-7?2=V7bWL& zth;1^qEbI32>_2>WpVL_N&(CcBJ!st(Rgqcu_AJ48@7y7C9B&7dmriRN6Ay8#6I-xfR-V&c8&ShnB3M+>>a_Iq z%uIvX#soFx&YeBuq=~q9@4Ttbt;#Qno;@4NXq9`n$YVuOMWqZ^;pF0)y0I-nwqmYX zw=V5gN&T?BMQfHHc)zj>XDejuyw&L{^4+`pL5B(!I^D2mqSy8t;zizTo<49~qZAwO znxtfR{%Fb(US3MjG}O-GP)uL}%SR*r(A<&4B#^?l2?>6KQC^+qH?UiMSX_7bm%+(i zUl3^h89#c_*Qr_CiCIm{W-F%tz81HL>y|>d)Aj357Hd0gIyLkCMI1>H1qGt` zk9EVu0_~Y#zaDlOlGw)T;JLcWf=g4=-!DK|l$n z#Var>h~b3rjy*!OmNEDE(N^=+{_oN!`u#W}hbF^iqr@Yst7TBTewiQF$6u#9+r6FH zZ%5Lc2vl(z%oiwR2b1njA&vJq6|Z#M2wkv$@)?-hbEU1zanZdgQN?|}#TXrz-}L)Y z9nU4h%Brfuj|zCga~n%_H!=+Tnyp7iUH>SD%CH+%zq$bZ8oD(6f|ti`yj`WdziqeP zbl*1&jK=!p@E zAgT2Aaqva=>2Dksp#3OMi2#PO`&8mz?`@@kBpdHA<;sl=62{Omt=y;i`NB|Ga_3vk zms;vP%V+A&1-4iil|9-&=w3*h*!fF`gZ)LJm^EN? zM*E<PTzyKKNbergx^I8t7qnW|SB(GoZFQe5h?YFh5V0PlW_2nU@vk4C9mEzC zvhJ)NO)Zwy*4FmPsEH5>0R%gCpo-plw9(;EDG6&uY*K6`G$IZ$v4}S7;t0rB5%=zG zh9Z(*c+!^Lb+1#=@<`H5Mkhr>8Qny5q5q$rGXFxIt*^quDzPkA4GfN=XmzScYx8Xh zsi}jgr-S_b&q)(S;oyEir9TYm-OEVhdPAY5B8572cr({LJj9581a)7`^UhFpr~m|z zW9n&l4qbY98~bVMmwsSG_IC9!8Fm?VnhQU|_1JF<&W=&`1CVK8bo zF)7L8Wn;+r$n6xp4$~;+IajfkmKIb2N-&C=i3z&;TYy-ojz#Os=FuK9){c%sK9tPU z`R5-81`fuBTOIzJ7!&O-RfHTmixTf-Rb^$tPv*UQzd+!D&Re0Eo@U!$N$JFy4~GK; zkLW`fPOsASE1>UIqgCaOrjX!d8J9fVExNwgE4tJd1hZt*n6@iWgA2xpd&pqwsG5m*IK5@u>ncRdXW8MG_J zn$C_k2Zmea`$4rwD{zw*^N=(D;np4eudZjz;g_+?X_QwQ@Pq#M2%SKeLF3rGV zviOyD7dw;*%u!40bw(k=iFIATMFQv<(C++;i_ZbD{)T3x;@QS%U9iW*dQMD1U7fCY zwf{u$SeEOYS?Ylcx-(B$#KN!x*y@uZu0n0gbYHSHs(md9pm(Bhb}!sDk@4IuJ|&+; zCJOG?7%9C3_Pqe99&hJNy=VKTN^(!UQextO*H6}pOrY3IQu$%^L^qi&VWyG&$}*)< z({W)V`d-z`}v?F#9FE(sdA-0p4mTvNT5=j%&k&iLez%; zR~aAW*&5Sa(=z4=4aL;TiXdY!#rRF?4?^$G#n`16w&x@e6R{?`UTfKVdA99lDE)9F z(;0#}><)3BsT5+cM!G#Y-RZm~=vygzNd?|$Vl~Qe?MVRR=?su8oj|7|y|R|aPGs0& zGOy1(tFM5B78$^lqxhK`m?r2yF3<{?FOoEK`w$wI+LXP=FQxGNYLrlV0o{}$z6?ai}LQo@Z;>lgF-GDpN9a-VBb17`a+%H#sbI|F#lHRu6d09gFR-1?~QMvo7x=O^g2x4SQTqB%JS z7n4TQQ)+K$H?H>L8xO$Z!H|ytTf%L;#AItAYF|x47%!DRtK+iu@@ozU9xd_axPBMI>@9HKH5x8-tR>Wm>s)A5k z_wP@~f@5O55;WbL)8WV%C^k<=HZxcUcb2m&%K2(dqhUOx=zr`1DQW4#N&@-u=Jo9b zu)ZuUO#vUs0C!LTpj9bYR#sL3cz`JcpG2CF7+yXf_Y5H{0JQT?*U{tXb>zEtDG#jm z1he8Lu8tT?xR0gH##to%HfcYFGjH5xFkRM{098;c!}umdKZ+~UN>x^;N~<5heX z7QN`&Y$Nl_lP=Q+KR?`Pe3LYYjceX+m1FyTdA3n*lkm|}pbpso#H|t`jsHXrs99Jj zvz#cxM4Wh~UmhdG1DI@t3g+T+mgv!=Ka-&s&bo>rf`L*4xTP0!6@>nbsmOEuC<+jQ zoW39fltY;a;b8bt;QnI{kD=O5`J7wbWYTq2lr}LvJ-xDQ5M)ITdr`nRPEH5+b3a5+fMG5(Zjp_euxJDd^d=H^&5y9J!B1aDi*3<%ttvt^fnV z7?ju1k;RO_O-kjt3UoTq^3)m?NJ~rW<3Cqgi^0_&FW&Hsbf1>&r#l&5gRrG&~AuPK)8avf#-}yek2rNIxx@QPwDAE zgZlz4I+!AIH=__4b8p1o#yE^o&gLy<*~^c6h!$(#8zB`7jdMGA7YMN$w9;@POTGTe zV35W**$62KiESr}SNRE_Hnl*Jexnt#8ia%9bS(`HDU=KupjQtZqrQiXRyQG^z+IR# z$wfBBX!7@GQ0l$R%r&T=VDAzW!3@(z_8(@a2yn~x-1S!NwAOq5JYe)hN1Bk6)x{^mHidEcQUDB7Cayx*HVbx0qPLcYtId!$c zW4d$O2&)+A5Efv6{-6O4c0elRDrEVfY97+>ow6XH>wqAPG22gvxC-)gi5Q|MqV*vm zPzQyhaGU|#6b{iv7zf$e*;=l{u~7CSo*lV5-ELQh0GoX5W+cMZ#{Yto319bHb+tkD ztCPoWWO(B<8HQB{`ekPb3maXnzB7VILS1F`*atNh`-AXuZrr#bCgqEUCxQ0+5Xvhk zv&YK3gR4z}c{A@eN~W)nZON&qI6z1W-4^Ce)*=rK(U6=YhQOhAqX(Lft@QYx5PnR- z!^!yx%m8UEzzaG-lc&$=P#VN7pn|t&1&ZZj9Sa8_{HyY^h{{ckniZn)ms<#B0yRdj z$^OEOzK$V_)}*7q2d#&Q^^xz`@ySFG;fzZCK(U|0X96L*GcYjVqznf>hlO&=JD2H%6f~8_#^PzMX!M_OQ1rQW!TZ#hjVzfu`VpFQ; z6*Dt#yTRI6c!9t+dl?!1!9$XBI^W_41cF3nfY8ryiCk3j;`+*jNFl|Txh5+jJU}i4 z!-CCO5^%cH7ykcLPnou(?r4oOEi?0VXzaGHl~q-z(P8bNxDl>%)RZhV1N5#=uMH2j zB7){!xW8?V^AHP~5+)0Awh5ZNrAG$zRT~IW*hZ*mNb66r6(Ta7jnmaRxw-Xl)(EX$ z0HQ8tovDe*Z>+|6lQS(V>qMrku}EkC;9#a@n^5%H!&c-%=+E4ao^FBU8~keK^LExr zNJ@^*Z$}@{P2g3P9|;uwvV}_YypE2JkA9t6!RWDs;mUC9qHmAs1)P_MR!SsxxO@a(z1k;?*gRDA)kvi;lXe^lkOL5YcVcN3cyhGBZDEfpvXyJIwx9r_2wa*7{K_lgr}5!eLNs(vV$A z?KUs6&yHG3ENSs|&>ptA_(3sYs4<=j0mdIce%R+OV&BT==cQ<5KLQks1b9LOE+H}T zi(WdXT*WR5ikP^#@}zsPej#{m9-|f1b_kpjb(t&WRRP+p=rDzb^BLMA9kw>mLl0WR z9Jf!!cn2x;jLl&@E z1;z2h54^pfhK7EF7o8}Rs+vR^%j{}$Mcx-m-=E2w)N&NfsJl?UTbqelvo%mP39+Wl zq>j!Q9~Qw=r(CiX&%1^2#WcLvviQ8g{0@L6LVREJ$B!QyD`r=gwMBc+%Egl`w6vH& z#*P5j;AkJ5DK}BdI=yt~!bfE@E6(FOsDK7wqVwnPs5zYs1bm$z?{F)p>i$|+r&iwc z*Y@o*Ka}LSIDSHj0W}%fH8WpN1-d;|VbQz@LKRExeV3T{2tM^Ervl;tPr*W-V3o(z8jm|>1B``=3 z8bb`Mg2f*H2$obJOMLUTU3Ae*Nrqy?L$e-mwF8R$6s!}Q5QJm~Nb^0{%>Yx|3kesZ2mfUzcnZ;axn15IVwRM%4 zqI9pQ7w~hc@BE^l({>hb_uT5_7K~M-d&2MBxg%@6zDbJbWBhjdEbECNO^K1wQ6zCu z4^DZ3E|{f|8P6*y?18t&>`LaWr-QU7U=PMRvK=O|3m1XAx7~l!IOg8tX_c)3X;QoM z{3u|F$RJOr^XvcY(4vuds!5?I^|#?$p)x}wv9H|=bW|73$n*_xvxyi@LXudGsArmeX;77y3 zU4uiD*U347(!jb)lkS1XE6A?@lcv&mf^uuOIvps!JY>52J<_AG@Ll)wsPdt$6Y-=X z;3vvP*hh~+rl96ZI68jhuL(_IZ6%eS5)_<-bUU@uwGoMDc5GG51~wq1Y$Rupf~yq7 zw=fCY551szm-16nSGNw&>WqFSQmxNN_v*AYi~oV+dbnrA4QArct&tPSGw4|CK?!aC zrM2U6uH^*~{;noyLQg#(Lc1BwOh-RWG=D`K*@D8Kc6#*g+LJYrV&OO|JB23v+cxh- zinUm1&fB;5AjJ1i0Oub&;BR|~53i#?WfyC|#-&Z(+uM5sRaoD7${jH-14F}fAWj{Rxh7fCSfmN` zKVnPcY=9kx9-*b+vv7%}L}yA1Fbet1e;z{;@~^c2sw9R&rqBwIFHyju!0tDPb@5lq%V)n42}W)uL`u24yhFWA6r^` zwP6~=aL`WaRgQ2M``L81O07_Cpd~VLWBb;v1UgF7E8F=qRky1*UsExi*<4ByX=101gER{r%3T!bVxQXYzGk zBcTrX4J%*pQZ2$`BaAi3J0ecwe;StMh?64RRNBHffN>!s_n;!@Tr1-$BE{V9%&Rd# z<-E{x=(J^PDbiE);_heQjY5H8*_xd4lZ0DF!G?;u@MET8{kfcmWSNdQO^dXEl9Yp> z5G+(BT&&K=h&kcmEX<`rP)$y;B%Wl5|K3nYv7 zTq?G9k$%|Oxh2vVv+!NFJX4B)DxlOoRLM{t%~km0(I&hDatjuZSOL_ehVWsyJ(d!&B4r6J+8Wt9FRqL2NaM z@3MxR4XHr@n?BZJBdTzQ(tr8a`iA+i;jk}f^n1>0eryL1c&mT*y|eScVFk%!U6*TA zCJIf%E(e&Z>9xj`o9O-7dn)YL4#z9$PLCzX-}+uV;s4^n+S=#L8M_C^51QFd6h(2! z88{6J*bNBGL<-o|zMc_WO8M~NF$FCzAGSh@ZZU8BOj!7Y;I~=9#acwc;VH8qSoZ9a z#5^%C>AfKub&eIwE4!vOQ}mA}%9rAUZw$B;i76 zezdrS`0iH&{@~TOHQ$`d%ggn|7=bwPLmI}nanlj(l$P9G-}-H9BSfyEnj9S+?f?AR z1BdID4Ro?w$VEB3xZHwU&_f^lng<@nueLTKYHDF=`M})V+4+pqL}x$Tx|`5Lxp(%! zn{ggZ&SCf?5-KBMYs=5-KDi0k!N~qSFu;CHP%5FMaATA`Jf6LnX zM9N97H+>>ow%m8kFy~3Kq};Z3>!Dz&rKQ0rXr?7R@QzL8q+(FGh&RS=D?$ zn&aqWy6q{p^LnpL&@Q=0qYM{= zVq|Fd?MnmUo612R=+SeR=|0JZoAPW`|L_#ViX;&ZgQ7zyczP!b9LE^Yehy=sz?to{ zJ)2|IvDdQ?*3S{p5?D<7W@b_l|B*k%_P4`vxSpOKZ%tLa%k=E*WZJy_2O-)auY@_o zP@U|IuLCSoR%|y(+EB={>Ieg1P8zE~^WTdI7bde*sqFiF`F5<>Hz1f@?yAQT(>)9bT#IBqw9Ypb3uTpf;&LIUm( znkV9okY&7+9)8Tgg!rIa9EtY(sNl6!Qpw*?JN33X94`){x51Z$9=^@k*cjuwN#hB_ zeRS8~zkeURw^dAru2OmTmhF^;$^_*<-F{fn!otG2KUnSSw{P8$eSzSv<<`TBA$YsI zy82fHToz%CDDQLB)z!^+7=4Im#%m?wt-0FSri~KE_TfMjgnD_-NPy;Jnjary==%?z z04?XBmAdY+#z`x-i|wp=xJY1$6B&e{Gta)>YyCpS<93`;4%dB{6rB0W%C=L46#iuE-zGWsGmAvvvmN#PY0AUNKE z%9%dN$q3=lNWm^h8Ub^o&HmTxfmj!zSd?~GMu<2Kb&zs)e6(MhHas~Vc7K9br{G!$ zuQs8v;fw^#7|Aj=MVc;Co+MLB|iaoVoT$hnC0Bov` z5K;T8|{>@|HWs}zTEq{?mAS6v#MDt(tKZMZ|KMz?NuU8I(8JEIGO2Dq5; z6kdggD`{wOFQ&bFcR5*nvxC(z5pu$Jxed7M8yU@4pUpWWs*%bbiWoO$wElQ|9vuV2 zVfc(B`O~@#TdteyLfc2WiHOnB(bL;Zbc#WoVPF0Z5v7#9;plza>RqhjuR;EiU|vKM zK~!V^4KcVhXKed?E5&*t;6p8=2L(9vwv1Z!}tV%gXZ0zib!ELp*PaqNX{{DR)8Eo_&Zd*5J=lZzojKStyFvC*@ zyB9eH0|YFE`-A%OeGZv6r$l@yEq(VnXdJOKoB2OC_6roVxUTNMX=*BoE;;lwnGr^l z^Us=wUhADyR8-}GtaU3B#T&m4t<2T;N?xdQauuY-?S4k6h+%)M4wYCWyNrxXT6+4e zf$C7Aq6gg)U5BRl?E8i7?Cd-@)||!;^q@v@K(yrT6@PomzPCOsa(iL+5?Kvs!Zs6E zsbH0(>0!{$h2Iie�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 F{{?uG7>fV^ literal 0 HcmV?d00001 From e589fcdd3dcfd3c0cf16b4bad2861451d68fa9d5 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 16:42:42 +0200 Subject: [PATCH 23/44] doc: dev: Complete dev documentation. --- doc/dev/documentation.org | 62 +++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 7aa30aca..a68a3c09 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -201,8 +201,8 @@ other components are linked to one of these basic components. bgcolor="transparent"; node[colorscheme=set19,shape=box,style="filled",fillcolor="2"]; - subgraph cluster0 { - style=dashed; + //subgraph cluster0 { + // style=dashed; study[label="Study"]; river[label="River"]; @@ -220,7 +220,7 @@ other components are linked to one of these basic components. geocrosssection[label="Cross-section"]; geopoint[label="Point"]; } - } + //} //subgraph cluster1 { // style=dashed; @@ -294,6 +294,22 @@ other components are linked to one of these basic components. rreach -> georeach; rreach -> rcrosssection; rcrosssection -> geocrosssection; + + // river -> boundaryconditionlist -> boundarycondition -> results[style=invis]; + // river -> lateralcontributionlist -> lateralcontribution -> results[style=invis]; + // river -> initialconditionsdict -> initialconditions -> results[style=invis]; + // initialconditions -> results[style=invis]; + // river -> stricklerslist -> stricklers -> results[style=invis]; + // river -> solverparameterslist -> results[style=invis]; + // river -> sedimentlayerlist -> sedimentlayer -> layer -> results[style=invis]; + + geopoint -> boundaryconditionlist[style=invis]; + geopoint -> lateralcontributionlist[style=invis]; + geopoint -> initialconditionsdict[style=invis]; + geopoint -> initialconditions[style=invis]; + geopoint -> stricklerslist[style=invis]; + geopoint -> solverparameterslist[style=invis]; + geopoint -> sedimentlayerlist[style=invis]; } #+end_src @@ -965,7 +981,7 @@ data base file. #+CAPTION: Pamhyr2 debug Python REPL [[./images/python-debug-repl.png]] -* TODO Build the project +* Build the project The project uses gitlab-ci runners to build packages, but it is possible to build packages manually. @@ -1010,9 +1026,45 @@ winetricks installed. [fn:nsis] The NSIS web site: https://sourceforge.net/projects/nsis/ -** TODO Setup the CI environment +** Setup the CI environment + +Pamhyr2 need a Linux ci-runner and a Windows ci-runner for building +package. The windows ci-runner could run on a Wine environement. + *** Linux + +The Linux ci-runner need some software and dependencies in addtion of +gitlab-ci. + +#+begin_src shell + sudo apt install \ + emacs emacs-goodies-el \ + texlive-full \ + python3.8 python3.8-venv + sudo python3 -m pip install pyinstaller +#+end_src + *** Windows (Wine) + +The ci-runner environment for Wine need at least wine version 8, let +[[https://www.numetopia.fr/comment-installer-wine-sur-ubuntu-ou-linux-mint/][see who to add wine official depot to your linux distribution]]. + +#+begin_src shell +sudo apt install wine-stable winetricks +#+end_src + +In addition, the environment need windows version of: + - [[https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe][Python 3.8.10]] + - Git + - PowerShell + - Gitlab-ci + - [[https://freefr.dl.sourceforge.net/project/nsis/NSIS%203/3.08/nsis-3.08-setup.exe][Nsis]] + +Now, we can install =pyinstaller= on this windows environment: +#+begin_src shell +wine python -m pip install pyinstaller +#+end_src + * Documentation files This document and the user documentation are org files. This text file From dbc256be1a5fafc7fa2e40832613aea35aa928da Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 27 Sep 2023 16:49:00 +0200 Subject: [PATCH 24/44] ui, doc: Update Pamhyr2 debug repl. --- doc/dev/documentation.org | 2 +- doc/dev/images/python-debug-repl.png | Bin 61865 -> 76043 bytes src/View/ui/DebugRepl.ui | 13 ++++++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index a68a3c09..8ca3ff05 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -977,7 +977,7 @@ SQLiteBrowser (if installed) on current Study to explore the study data base file. #+NAME: debug-repl -#+ATTR_LATEX: :width 16cm +#+ATTR_LATEX: :width 12cm #+CAPTION: Pamhyr2 debug Python REPL [[./images/python-debug-repl.png]] diff --git a/doc/dev/images/python-debug-repl.png b/doc/dev/images/python-debug-repl.png index 5cc506026c104ac044d71300430c0884c8d35fc2..847f4afc6f94829f2ccc3abc729505e841fa6626 100644 GIT binary patch literal 76043 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 K0_GYX{J#Ly|L=PM literal 61865 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�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 F{{?uG7>fV^ diff --git a/src/View/ui/DebugRepl.ui b/src/View/ui/DebugRepl.ui index 03494a0c..c0fc844e 100644 --- a/src/View/ui/DebugRepl.ui +++ b/src/View/ui/DebugRepl.ui @@ -21,7 +21,13 @@ - + + + + Monospace + + + @@ -44,6 +50,11 @@ + + + Monospace + + true From 245a5057c38c430fb2a4b326f335b43eaf22dd82 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 29 Sep 2023 10:14:40 +0200 Subject: [PATCH 25/44] refactoring: Update BC. --- src/View/BoundaryCondition/Edit/Plot.py | 8 ++-- src/View/BoundaryCondition/Edit/Table.py | 1 - src/View/BoundaryCondition/Edit/Window.py | 10 +++-- src/View/BoundaryCondition/Edit/translate.py | 33 ++++++++--------- src/View/BoundaryCondition/Table.py | 22 ++++++++--- src/View/BoundaryCondition/Window.py | 9 +++-- src/View/BoundaryCondition/translate.py | 39 ++++++++++---------- src/View/Tools/PamhyrTable.py | 14 ++++--- 8 files changed, 78 insertions(+), 58 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py index b27380a5..a7815e60 100644 --- a/src/View/BoundaryCondition/Edit/Plot.py +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -27,7 +27,7 @@ from PyQt5.QtCore import ( QCoreApplication ) -from View.BoundaryCondition.Edit.translate import * +from View.BoundaryCondition.Edit.translate import BCETranslate _translate = QCoreApplication.translate @@ -42,6 +42,8 @@ class Plot(APlot): toolbar=toolbar ) + self._trad = BCETranslate() + self._table_headers = self._trad.get_dict("table_headers") self._mode = mode def custom_ticks(self): @@ -109,10 +111,10 @@ class Plot(APlot): # Plot label header = self.data.header self.canvas.axes.set_xlabel( - table_headers[header[0]], color='black', fontsize=10 + self._table_headers[header[0]], color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - table_headers[header[1]], color='black', fontsize=10 + self._table_headers[header[1]], color='black', fontsize=10 ) self.canvas.axes.autoscale_view(True, True, True) diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py index e89e6d0d..7f86fc8a 100644 --- a/src/View/BoundaryCondition/Edit/Table.py +++ b/src/View/BoundaryCondition/Edit/Table.py @@ -46,7 +46,6 @@ from View.BoundaryCondition.Edit.UndoCommand import ( SortCommand, MoveCommand, PasteCommand, DuplicateCommand, ) -from View.BoundaryCondition.Edit.translate import * _translate = QCoreApplication.translate diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 249a34cb..13d277e6 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -44,8 +44,7 @@ from PyQt5.QtWidgets import ( from View.Plot.MplCanvas import MplCanvas from View.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.BoundaryCondition.translate import long_types -from View.BoundaryCondition.Edit.translate import table_headers, retranslate +from View.BoundaryCondition.Edit.translate import BCETranslate from View.BoundaryCondition.Edit.UndoCommand import SetMetaDataCommand from View.BoundaryCondition.Edit.Table import TableModel from View.BoundaryCondition.Edit.Plot import Plot @@ -101,6 +100,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): def __init__(self, data=None, study=None, config=None, parent=None): self._data = data + trad = BCETranslate() + self._long_types = trad.get_dict("long_types") name = self._pamhyr_name if self._data is not None: @@ -110,13 +111,14 @@ class EditBoundaryConditionWindow(PamhyrWindow): _translate("Edit boundary condition", self._pamhyr_name) + f" - {study.name} " + f" - {self._data.name} ({self._data.id}) " + - f"({long_types[self._data.bctype]} - {node_name})" + f"({self._long_types[self._data.bctype]} - {node_name})" ) super(EditBoundaryConditionWindow, self).__init__( title = name, study = study, config = config, + trad = trad, parent = parent ) @@ -139,8 +141,8 @@ class EditBoundaryConditionWindow(PamhyrWindow): self._d50sigma.set_sigma(self._data.sigma) def setup_table(self): - retranslate() headers = {} + table_headers = self._trad.get_dict("table_headers") for h in self._data.header: headers[h] = table_headers[h] diff --git a/src/View/BoundaryCondition/Edit/translate.py b/src/View/BoundaryCondition/Edit/translate.py index 28b6ec98..d97ac6de 100644 --- a/src/View/BoundaryCondition/Edit/translate.py +++ b/src/View/BoundaryCondition/Edit/translate.py @@ -18,23 +18,22 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + +from View.BoundaryCondition.translate import BCTranslate + _translate = QCoreApplication.translate -table_headers = { - "x": _translate("BoundaryCondition", "X"), - "y": _translate("BoundaryCondition", "Y"), - "time": _translate("BoundaryCondition", "Time"), - "date": _translate("BoundaryCondition", "Date"), - "discharge": _translate("BoundaryCondition", "Discharge (m³/s)"), - "z": _translate("BoundaryCondition", "Z (m)"), - "solid": _translate("BoundaryCondition", "Solid (kg/s)"), -} +class BCETranslate(BCTranslate): + def __init__(self): + super(BCETranslate, self).__init__() -def retranslate(): - table_headers["x"] = _translate("BoundaryCondition", "X") - table_headers["y"] = _translate("BoundaryCondition", "Y") - table_headers["time"] = _translate("BoundaryCondition", "Time") - table_headers["date"] = _translate("BoundaryCondition", "Date") - table_headers["discharge"] = _translate("BoundaryCondition", "Discharge (m³/s)") - table_headers["z"] = _translate("BoundaryCondition", "Z (m)") - table_headers["solid"] = _translate("BoundaryCondition", "Solid (kg/s)") + self._sub_dict["table_headers"] = { + "x": _translate("BoundaryCondition", "X"), + "y": _translate("BoundaryCondition", "Y"), + "time": _translate("BoundaryCondition", "Time"), + "date": _translate("BoundaryCondition", "Date"), + "discharge": _translate("BoundaryCondition", "Discharge (m³/s)"), + "z": _translate("BoundaryCondition", "Z (m)"), + "solid": _translate("BoundaryCondition", "Solid (kg/s)"), + } diff --git a/src/View/BoundaryCondition/Table.py b/src/View/BoundaryCondition/Table.py index 377d9aab..77141dff 100644 --- a/src/View/BoundaryCondition/Table.py +++ b/src/View/BoundaryCondition/Table.py @@ -46,19 +46,24 @@ from View.BoundaryCondition.UndoCommand import ( AddCommand, DelCommand, SortCommand, MoveCommand, PasteCommand, DuplicateCommand, ) -from View.BoundaryCondition.translate import * +from View.BoundaryCondition.translate import BC_types logger = logging.getLogger() _translate = QCoreApplication.translate class ComboBoxDelegate(QItemDelegate): - def __init__(self, data=None, mode="type", tab="", parent=None): + def __init__(self, data=None, mode="type", tab="", trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._data = data self._mode = mode self._tab = tab + self._trad = trad + + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") def createEditor(self, parent, option, index): self.editor = QComboBox(parent) @@ -66,7 +71,7 @@ class ComboBoxDelegate(QItemDelegate): if self._mode == "type": lst = list( map( - lambda k: long_types[k], + lambda k: self._long_types[k], filter( lambda k: self._tab in BC_types[k].compatibility(), BC_types.keys() @@ -107,6 +112,13 @@ class ComboBoxDelegate(QItemDelegate): class TableModel(PamhyrTableModel): + def __init__(self, **kwargs): + super(TableModel, self).__init__(**kwargs) + + self._long_types = {} + if self._trad is not None: + self._long_types = self._trad.get_dict("long_types") + def _setup_lst(self): self._lst = self._data.boundary_condition self._tab = self._opt_data @@ -125,7 +137,7 @@ class TableModel(PamhyrTableModel): return self._lst.get(self._tab, row).name elif self._headers[column] == "type": t = self._lst.get(self._tab, row).bctype - return long_types[t] + return self._long_types[t] elif self._headers[column] == "node": n = self._lst.get(self._tab, row).node if n is None: @@ -149,7 +161,7 @@ class TableModel(PamhyrTableModel): ) ) elif self._headers[column] == "type": - key = next(k for k, v in long_types.items() if v == value) + key = next(k for k, v in self._long_types.items() if v == value) self._undo.push( SetTypeCommand( self._lst, self._tab,row, BC_types[key] diff --git a/src/View/BoundaryCondition/Window.py b/src/View/BoundaryCondition/Window.py index 5f08ecf8..99758c5a 100644 --- a/src/View/BoundaryCondition/Window.py +++ b/src/View/BoundaryCondition/Window.py @@ -55,7 +55,7 @@ from View.BoundaryCondition.Table import ( ) from View.Network.GraphWidget import GraphWidget -from View.BoundaryCondition.translate import table_headers, retranslate +from View.BoundaryCondition.translate import BCTranslate from View.BoundaryCondition.Edit.Window import EditBoundaryConditionWindow _translate = QCoreApplication.translate @@ -73,6 +73,7 @@ class BoundaryConditionWindow(PamhyrWindow): title = name, study = study, config = config, + trad = BCTranslate(), parent=parent ) @@ -85,17 +86,18 @@ class BoundaryConditionWindow(PamhyrWindow): self.ui.setWindowTitle(self._title) def setup_table(self): - retranslate() self._table = {} for t in ["liquid", "solid", "suspenssion"]: self._delegate_type = ComboBoxDelegate( + trad = self._trad, data = self._study.river, mode = "type", tab = t, parent=self ) self._delegate_node = ComboBoxDelegate( + trad = self._trad, data = self._study.river, mode = "node", tab = t, @@ -105,12 +107,13 @@ class BoundaryConditionWindow(PamhyrWindow): table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( table_view = table, - table_headers = table_headers, + table_headers = self._trad.get_dict("table_headers"), editable_headers = ["name", "type", "node"], delegates = { "type": self._delegate_type, "node": self._delegate_node, }, + trad = self._trad, data = self._study.river, undo = self._undo_stack, opt_data = t, diff --git a/src/View/BoundaryCondition/translate.py b/src/View/BoundaryCondition/translate.py index 78aaec07..00077f5f 100644 --- a/src/View/BoundaryCondition/translate.py +++ b/src/View/BoundaryCondition/translate.py @@ -18,6 +18,8 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + from Model.BoundaryCondition.BoundaryConditionTypes import ( NotDefined, PonctualContribution, TimeOverZ, TimeOverDischarge, ZOverDischarge, @@ -26,21 +28,6 @@ from Model.BoundaryCondition.BoundaryConditionTypes import ( _translate = QCoreApplication.translate -long_types = { - "ND": _translate("BoundaryCondition", "Not defined"), - "PC": _translate("BoundaryCondition", "Ponctual contribution"), - "TZ": _translate("BoundaryCondition", "TimeZ"), - "TD": _translate("BoundaryCondition", "TimeDischarge"), - "ZD": _translate("BoundaryCondition", "ZDischarge"), - "SL": _translate("BoundaryCondition", "Solid"), -} - -table_headers = { - "name": _translate("BoundaryCondition", "Name"), - "type": _translate("BoundaryCondition", "Type"), - "node": _translate("BoundaryCondition", "Node") -} - BC_types = { "ND": NotDefined, "PC": PonctualContribution, @@ -50,7 +37,21 @@ BC_types = { "SL": Solid, } -def retranslate(): - table_headers["name"] = _translate("BoundaryCondition", "Name") - table_headers["type"] = _translate("BoundaryCondition", "Type") - table_headers["node"] = _translate("BoundaryCondition", "Node") +class BCTranslate(PamhyrTranslate): + def __init__(self): + super(BCTranslate, self).__init__() + + self._sub_dict["long_types"] = { + "ND": _translate("BoundaryCondition", "Not defined"), + "PC": _translate("BoundaryCondition", "Ponctual contribution"), + "TZ": _translate("BoundaryCondition", "TimeZ"), + "TD": _translate("BoundaryCondition", "TimeDischarge"), + "ZD": _translate("BoundaryCondition", "ZDischarge"), + "SL": _translate("BoundaryCondition", "Solid"), + } + + self._sub_dict["table_headers"] = { + "name": _translate("BoundaryCondition", "Name"), + "type": _translate("BoundaryCondition", "Type"), + "node": _translate("BoundaryCondition", "Node") + } diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index 623ebd3b..08e36c84 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -75,13 +75,14 @@ class PamhyrTableModel(QAbstractTableModel): ) def __init__(self, - table_view=None, - table_headers={}, - editable_headers=[], + table_view = None, + table_headers = {}, + editable_headers = [], delegates = {}, - data=None, - undo=None, - opt_data=None): + trad = None, + data = None, + undo = None, + opt_data = None): super(PamhyrTableModel, self).__init__() self._table_view = table_view @@ -90,6 +91,7 @@ class PamhyrTableModel(QAbstractTableModel): self._headers = list(table_headers.keys()) self._editable_headers = editable_headers self._delegates = delegates + self._trad = trad self._data = data self._opt_data = opt_data From ef672798efefef2a21c26b378dda9d994fb4c366 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 29 Sep 2023 11:17:08 +0200 Subject: [PATCH 26/44] refactoring: Start refactoring plot. --- src/View/MainWindow.py | 24 +++++------ src/View/Tools/PamhyrPlot.py | 43 +++++++++++++++++++ src/View/{ => Tools}/Plot/APlot.py | 12 +----- .../Plot/OnPickEvent.py} | 0 .../Plot/PamhyrCanvas.py} | 1 - src/View/{ => Tools}/Plot/PamhyrToolbar.py | 0 6 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 src/View/Tools/PamhyrPlot.py rename src/View/{ => Tools}/Plot/APlot.py (84%) rename src/View/{Plot/mpl_canvas_onpick_event.py => Tools/Plot/OnPickEvent.py} (100%) rename src/View/{Plot/MplCanvas.py => Tools/Plot/PamhyrCanvas.py} (99%) rename src/View/{ => Tools}/Plot/PamhyrToolbar.py (100%) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index cb9d8b7e..58a2a031 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -44,18 +44,18 @@ from View.Configure.Window import ConfigureWindow from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow -from View.Geometry.Window import GeometryWindow -from View.BoundaryCondition.Window import BoundaryConditionWindow -from View.LateralContribution.Window import LateralContributionWindow -from View.InitialConditions.Window import InitialConditionsWindow -from View.Stricklers.Window import StricklersWindow -from View.Frictions.Window import FrictionsWindow -from View.SedimentLayers.Window import SedimentLayersWindow -from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow -from View.SolverParameters.Window import SolverParametersWindow -from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow -from View.CheckList.Window import CheckListWindow -from View.Results.Window import ResultsWindow +# from View.Geometry.Window import GeometryWindow +# from View.BoundaryCondition.Window import BoundaryConditionWindow +# from View.LateralContribution.Window import LateralContributionWindow +# from View.InitialConditions.Window import InitialConditionsWindow +# from View.Stricklers.Window import StricklersWindow +# from View.Frictions.Window import FrictionsWindow +# from View.SedimentLayers.Window import SedimentLayersWindow +# from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow +# from View.SolverParameters.Window import SolverParametersWindow +# from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow +# from View.CheckList.Window import CheckListWindow +# from View.Results.Window import ResultsWindow from View.Debug.Window import ReplWindow from Model.Study import Study diff --git a/src/View/Tools/PamhyrPlot.py b/src/View/Tools/PamhyrPlot.py new file mode 100644 index 00000000..b06ec2c2 --- /dev/null +++ b/src/View/Tools/PamhyrPlot.py @@ -0,0 +1,43 @@ +# PamhyrPlot.py -- Pamhyr +# Copyright (C) 2023 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from View.Tools.Plot.APlot import APlot +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +class PamhyrPlot(APlot): + def __init__(self, data=None, trad=None, + canvas=None, toolbar=None, + parent=None): + if canvas is None: + canvas = MplCanvas() + + self._trad = trad + self._canvas = canvas + self._toolbar = toolbar + self._parent = parent + + super(PamhyrPlot, self).__init__(data = data) + + @property + def canvas(self): + return self._canvas + + @property + def toolbar(self): + return self._toolbar diff --git a/src/View/Plot/APlot.py b/src/View/Tools/Plot/APlot.py similarity index 84% rename from src/View/Plot/APlot.py rename to src/View/Tools/Plot/APlot.py index a863f1e6..7d8de307 100644 --- a/src/View/Plot/APlot.py +++ b/src/View/Tools/Plot/APlot.py @@ -19,26 +19,16 @@ from Model.Except import NotImplementedMethodeError class APlot(object): - def __init__(self, canvas=None, data=None, toolbar=None): + def __init__(self, data=None): super(APlot, self).__init__() self._init = False - - self._canvas = canvas self._data = data - self._toolbar = toolbar - - @property - def canvas(self): - return self._canvas @property def data(self): return self._data - @property - def toolbar(self): - return self._toolbar def draw(self): """Draw plot diff --git a/src/View/Plot/mpl_canvas_onpick_event.py b/src/View/Tools/Plot/OnPickEvent.py similarity index 100% rename from src/View/Plot/mpl_canvas_onpick_event.py rename to src/View/Tools/Plot/OnPickEvent.py diff --git a/src/View/Plot/MplCanvas.py b/src/View/Tools/Plot/PamhyrCanvas.py similarity index 99% rename from src/View/Plot/MplCanvas.py rename to src/View/Tools/Plot/PamhyrCanvas.py index ee1f5373..c4bbf4d3 100644 --- a/src/View/Plot/MplCanvas.py +++ b/src/View/Tools/Plot/PamhyrCanvas.py @@ -17,7 +17,6 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg from matplotlib.figure import Figure - class MplCanvas(FigureCanvasQTAgg): def __init__(self, width=5, height=4, dpi=100): fig = Figure( diff --git a/src/View/Plot/PamhyrToolbar.py b/src/View/Tools/Plot/PamhyrToolbar.py similarity index 100% rename from src/View/Plot/PamhyrToolbar.py rename to src/View/Tools/Plot/PamhyrToolbar.py From 44ccf99d48220ff9dd83afd3daa1d309e20963bd Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 3 Oct 2023 15:10:46 +0200 Subject: [PATCH 27/44] Refactoring: Use ui file for geometry reach window. --- src/Model/Geometry/Reach.py | 3 + src/View/Geometry/PlotAC.py | 15 +- src/View/Geometry/PlotKPZ.py | 16 +- src/View/Geometry/PlotXY.py | 19 +- src/View/Geometry/Table.py | 118 +++--------- src/View/Geometry/Translate.py | 45 +++++ src/View/Geometry/Window.py | 287 +++++++++++++++------------- src/View/MainWindow.py | 4 +- src/View/Tools/PamhyrTable.py | 4 +- src/View/ui/GeometryCrossSection.ui | 134 +++++++++++++ src/View/ui/GeometryReach.ui | 231 ++++++++++++++++++++++ 11 files changed, 625 insertions(+), 251 deletions(-) create mode 100644 src/View/Geometry/Translate.py create mode 100644 src/View/ui/GeometryCrossSection.ui create mode 100644 src/View/ui/GeometryReach.ui diff --git a/src/Model/Geometry/Reach.py b/src/Model/Geometry/Reach.py index 26cdefc9..36fca25f 100644 --- a/src/Model/Geometry/Reach.py +++ b/src/Model/Geometry/Reach.py @@ -106,6 +106,9 @@ class Reach(SQLSubModel): return self._parent.name + def __len__(self): + return len(self._profiles) + @property def profiles(self): return self._profiles.copy() diff --git a/src/View/Geometry/PlotAC.py b/src/View/Geometry/PlotAC.py index e43f39e9..974706aa 100644 --- a/src/View/Geometry/PlotAC.py +++ b/src/View/Geometry/PlotAC.py @@ -19,7 +19,7 @@ import logging from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -29,12 +29,15 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotAC(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, plot_xy=None): +class PlotAC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + plot_xy=None, parent=None): super(PlotAC, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self.plot_xy = plot_xy diff --git a/src/View/Geometry/PlotKPZ.py b/src/View/Geometry/PlotKPZ.py index eb7d5e28..f216443c 100644 --- a/src/View/Geometry/PlotKPZ.py +++ b/src/View/Geometry/PlotKPZ.py @@ -19,7 +19,7 @@ import logging from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -29,13 +29,15 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotKPZ(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): +class PlotKPZ(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + display_current=True, parent=None): super(PlotKPZ, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self.display_current = display_current diff --git a/src/View/Geometry/PlotXY.py b/src/View/Geometry/PlotXY.py index 54e056a9..ce93fa24 100644 --- a/src/View/Geometry/PlotXY.py +++ b/src/View/Geometry/PlotXY.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,12 +25,15 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotXY(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, display_current=True): +class PlotXY(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + display_current=True, parent=None): super(PlotXY, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self.display_current = display_current @@ -60,11 +63,11 @@ class PlotXY(APlot): # Axes self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "X (m)"), + _translate("Geometry", "X (m)"), color='green', fontsize=12 ) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Y (m)"), + _translate("Geometry", "Y (m)"), color='green', fontsize=12 ) self.canvas.axes.axis("equal") diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 1b055209..0ff5e0c8 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -35,6 +35,8 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from Model.Geometry import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ from View.Geometry.UndoCommand import * @@ -44,53 +46,26 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableEditableModel(QAbstractTableModel): - def __init__(self, reach, headers=None, undo=None): - QAbstractTableModel.__init__(self) - data_list = [] - - self._undo_stack = undo - self._reach = reach - - # Hack for qtlinguist - _ = _translate("Geometry", "Name") - _ = _translate("Geometry", "Kp (m)") - _ = _translate("Geometry", "Type") - - if headers is None: - self.headers = [ - "Name", - "Kp (m)", - "Type" - ] - else: - self.headers = headers - - def rowCount(self, parent=QModelIndex()): - return self._reach.number_profiles - - def columnCount(self, parent=QModelIndex()): - return len(self.headers) - +class TableEditableModel(PamhyrTableModel): def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return QVariant() if role == Qt.DisplayRole and index.column() == 0: - return self._reach.profile(index.row()).name + return self._data.profile(index.row()).name if role == Qt.DisplayRole and index.column() == 1: - kp = self._reach.profile(index.row()).kp + kp = self._data.profile(index.row()).kp return f"{kp:.4f}" if role == Qt.DisplayRole and index.column() == 2: - return self._reach.profile(index.row()).profile_type + return self._data.profile(index.row()).profile_type if role == Qt.TextAlignmentRole: return Qt.AlignHCenter | Qt.AlignVCenter if role == Qt.ForegroundRole and index.column() == 0: - name = self._reach.profile(index.row()).name\ + name = self._data.profile(index.row()).name\ .strip()\ .lower() if (name == "upstream" or name == "up" or @@ -102,16 +77,6 @@ class TableEditableModel(QAbstractTableModel): return QVariant() - def headerData(self, section, orientation, role=Qt.DisplayRole): - if role == Qt.DisplayRole: - if orientation == Qt.Horizontal: - if section < len(self.headers): - return _translate("Geometry", self.headers[section]) - else: - return str(section + 1) - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): row = index.row() column = index.column() @@ -119,19 +84,19 @@ class TableEditableModel(QAbstractTableModel): if role == Qt.EditRole and index.column() != 2: try: if index.column() == 0: - self._undo_stack.push( + self._undo.push( SetNameCommand( - self._reach, index.row(), - self._reach.profile(index.row()).name, + self._data, index.row(), + self._data.profile(index.row()).name, value ) ) if index.column() == 1: - self._undo_stack.push( + self._undo.push( SetKPCommand( - self._reach, index.row(), - self._reach.profile(index.row()).kp, + self._data, index.row(), + self._data.profile(index.row()).kp, value ) ) @@ -149,27 +114,13 @@ class TableEditableModel(QAbstractTableModel): return False - def index(self, row, column, parent=QModelIndex()): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - return self.createIndex(row, column, QModelIndex()) - - def flags(self, index): - flg = Qt.ItemIsEnabled | Qt.ItemIsSelectable - - if index.column() == 2: - return flg - else: - return Qt.ItemIsEditable | flg - # @QtCore.pyqtSlot() def insert_row(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row - 1) - self._undo_stack.push( + self._undo.push( AddCommand( - self._reach, row + self._data, row ) ) @@ -179,9 +130,9 @@ class TableEditableModel(QAbstractTableModel): def remove_rows(self, rows, parent=QModelIndex()): self.beginRemoveRows(parent, rows[0], rows[-1]) - self._undo_stack.push( + self._undo.push( DelCommand( - self._reach, rows + self._data, rows ) ) @@ -191,9 +142,9 @@ class TableEditableModel(QAbstractTableModel): def sort_profiles(self, _reverse): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( SortCommand( - self._reach, _reverse + self._data, _reverse ) ) @@ -209,9 +160,9 @@ class TableEditableModel(QAbstractTableModel): self.beginMoveRows(parent, row - 1, row - 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._reach, "up", row + self._data, "up", row ) ) @@ -219,16 +170,16 @@ class TableEditableModel(QAbstractTableModel): self.layoutChanged.emit() def move_row_down(self, row, parent=QModelIndex()): - if row > self._reach.number_profiles: + if row > self._data.number_profiles: return target = row self.beginMoveRows(parent, row + 1, row + 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._reach, "down", row + self._data, "down", row ) ) @@ -238,9 +189,9 @@ class TableEditableModel(QAbstractTableModel): def duplicate(self, rows, profiles): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( DuplicateCommand( - self._reach, rows, + self._data, rows, profiles ) ) @@ -250,7 +201,7 @@ class TableEditableModel(QAbstractTableModel): def paste(self, row, header, data): - if row > self._reach.number_profiles: + if row > self._data.number_profiles: return if len(data) == 0: @@ -258,9 +209,9 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( PasteCommand( - self._reach, row, + self._data, row, list( map( lambda d: ProfileXYZ.from_data(header, d), @@ -273,16 +224,7 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() self.layoutChanged.emit() - - def undo(self): - self._undo_stack.undo() - self.layoutChanged.emit() - - def redo(self): - self._undo_stack.redo() - self.layoutChanged.emit() - - +# TODO: Delete useless delegate class Delegate(QStyledItemDelegate): def __init__(self, parent=None, setModelDataEvent=None): super(Delegate, self).__init__(parent) diff --git a/src/View/Geometry/Translate.py b/src/View/Geometry/Translate.py new file mode 100644 index 00000000..c583732d --- /dev/null +++ b/src/View/Geometry/Translate.py @@ -0,0 +1,45 @@ +# Translate.py -- Pamhyr +# Copyright (C) 2023 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + +class GeometryTranslate(PamhyrTranslate): + def __init__(self): + super(GeometryTranslate, self).__init__() + + self._dict["open_file"] = _translate("Geometry", "Open a file") + self._dict["file_st"] = _translate("Geometry", "File mage geometry (*.ST)") + self._dict["file_m"] = _translate("Geometry", "File mage meshed geometry (*.M)") + self._dict["file_all"] = _translate("Geometry", "All file (*)") + + self._dict["reach"] = _translate("Geometry", "reach") + self._dict["reachs"] = _translate("Geometry", "reachs") + + self._dict["cross_section"] = _translate("Geometry", "cross-section") + self._dict["cross_sections"] = _translate("Geometry", "cross-sections") + self._dict["profile"] = _translate("Geometry", "cross-section") + self._dict["profiles"] = _translate("Geometry", "cross-sections") + + self._sub_dict["table_headers"] = { + "name": _translate("Geometry", "Name"), + "kp": _translate("Geometry", "KP"), + } diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index a66ed781..1b334bb7 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -34,118 +34,168 @@ from PyQt5.QtCore import ( ) from PyQt5.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QCheckBox, - QUndoStack, QShortcut, + QUndoStack, QShortcut, QTableView, QHeaderView, + QAction, QSlider, QPushButton, QVBoxLayout, + QLabel, ) from View.Geometry.PlotXY import PlotXY from View.Geometry.PlotKPZ import PlotKPZ from View.Geometry.PlotAC import PlotAC -from View.Tools.ASubWindow import ASubMainWindow, WindowToolKit -from View.Tools.ListedSubWindow import ListedSubWindow +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas -from View.Geometry.mainwindow_ui_reach import Ui_MainWindow from View.Geometry.Table import * -from View.Geometry.Profile.Window import ProfileWindow +from View.Geometry.Translate import GeometryTranslate +# from View.Geometry.Profile.Window import ProfileWindow _translate = QCoreApplication.translate -class GeometryWindow(ASubMainWindow, ListedSubWindow): - def __init__(self, model=None, title="Geometry", parent=None): - self._title = title - self.parent = parent +class GeometryWindow(PamhyrWindow): + _pamhyr_ui = "GeometryReach" + _pamhyr_name = "Geometry" + + def __init__(self, reach=None, study=None, config=None, parent=None): + if reach is None: + self._reach = study.river.current_reach().reach + else: + self._reach = reach + + name = f"{self._pamhyr_name} - {self._reach.name}" + super(GeometryWindow, self).__init__( - name=self._title, - parent=parent + title = name, + study = study, + config = config, + trad = GeometryTranslate(), + parent = parent ) - self._model = model - self._reach = model.river.current_reach().reach - - self.ui = Ui_MainWindow() - self.ui.setupUi(self) - - self.tableView = self.ui.tableView - self.tableView_header = self.ui.tableView_header - self._tablemodel = None self._profile_window = [] - self._clipboard = None - self.setup_window() - self.setup_sc() - self.setup_model() + self.setup_table() self.setup_plots() + self.setup_statusbar() self.setup_connections() self.changed_slider_value() - def setup_window(self): - self._title = f"{self.ui.mainwindow_title} - {self._reach.name}" - self.setWindowTitle(self._title) + def setup_table(self): + table_headers = self._trad.get_dict("table_headers") - def setup_sc(self): - self._undo_stack = QUndoStack() - - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def setup_model(self): + table = self.find(QTableView, "tableView") self._tablemodel = TableEditableModel( - headers = self.ui.tableView_header, - reach = self._reach, + table_view = table, + table_headers = table_headers, + editable_headers = ["name", "kp"], + data = self._reach, undo = self._undo_stack ) - self.tableView.setModel(self._tablemodel) - self.tableView.setItemDelegate(Delegate()) + table.setModel(self._tablemodel) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) def setup_plots(self): + self._canvas_xy = MplCanvas(width=3, height=4, dpi=100) + self._canvas_xy.setObjectName("canvas_xy") + self._toolbar_xy = PamhyrPlotToolbar( + self._canvas_xy, self, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout_xy = self.find(QVBoxLayout, "verticalLayout") + self._plot_layout_xy.addWidget(self._toolbar_xy) + self._plot_layout_xy.addWidget(self._canvas_xy) self.plot_xy() + + self._canvas_kpc = MplCanvas(width=6, height=4, dpi=100) + self._canvas_kpc.setObjectName("canvas_kpc") + self._toolbar_kpc = PamhyrPlotToolbar( + self._canvas_kpc, self, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout_kpc = self.find(QVBoxLayout, "verticalLayout_2") + self._plot_layout_kpc.addWidget(self._toolbar_kpc) + self._plot_layout_kpc.addWidget(self._canvas_kpc) self.plot_kpc() + + self._canvas_ac = MplCanvas(width=9, height=4, dpi=100) + self._canvas_ac.setObjectName("canvas_ac") + self._toolbar_ac = PamhyrPlotToolbar( + self._canvas_ac, self, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout_ac = self.find(QVBoxLayout, "verticalLayout_3") + self._plot_layout_ac.addWidget(self._toolbar_ac) + self._plot_layout_ac.addWidget(self._canvas_ac) self.plot_ac() + def _compute_status_label(self): + row = self.index_selected_row() + profile = self._reach.profile(row) + + name = profile.name + " " + str(profile.kp) + + return ( + f"{self._trad['reach']}: {self._reach.name}" + " - " + f"{self._trad['cross_section']}: {name}" + ) + + def setup_statusbar(self): + txt = "" + self._status_label = QLabel(txt) + self.statusbar.addPermanentWidget(self._status_label) + + def update_statusbar(self): + txt = self._compute_status_label() + self._status_label.setText(txt) + def setup_connections(self): - self.ui.btn_open.triggered.connect(self.open_file_dialog) - self.ui.btn_sort_asc.triggered.connect(self.sort_ascending) - self.ui.btn_sort_desc.triggered.connect(self.sort_descending) - self.ui.btn_move_up.triggered.connect(self.move_row_up) - self.ui.btn_move_down.triggered.connect(self.move_row_down) - # self.ui.btn_end_editing.triggered.connect(self.handleSave) - self.ui.btn_add.triggered.connect(self.insert_row) - self.ui.btn_delete.triggered.connect(self.delete_rows) - self.ui.btn_edit.triggered.connect(self.edit_profile) - self.ui.verticalSlider.valueChanged.connect(self.changed_slider_value) + actions = { + "action_import": self.import_from_file, + # "action_export": self.export_to_file, + "action_sort_asc": self.sort_ascending, + "action_sort_des": self.sort_descending, + "action_up": self.move_up, + "action_down": self.move_down, + "action_add": self.add, + "action_delete": self.delete, + "action_edit": self.edit_profile, + } - self.ui.btn_slider_up.clicked.connect(self.decrement_value_slider) - self.ui.btn_slider_down.clicked.connect(self.increment_value_slider) - self.ui.btn_move_up.triggered.connect(self.changed_profile_slot) + for action in actions: + self.find(QAction, action)\ + .triggered.connect(actions[action]) - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) + self.find(QSlider, "verticalSlider").valueChanged.connect(self.changed_slider_value) + self.find(QPushButton, "pushButton_up").clicked.connect(self.decrement_value_slider) + self.find(QPushButton, "pushButton_down").clicked.connect(self.increment_value_slider) # Profile selection when line change in table - self.tableView.selectionModel()\ - .selectionChanged\ - .connect(self.select_current_profile) + self.find(QTableView, "tableView").selectionModel()\ + .selectionChanged\ + .connect(self.select_current_profile) - def open_file_dialog(self): + + def import_from_file(self): options = QFileDialog.Options() settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', ) options |= QFileDialog.DontUseNativeDialog + file_types = [ + self._trad["file_st"], + self._trad["file_m"], + self._trad["file_all"], + ] + filename, _ = QtWidgets.QFileDialog.getOpenFileName( self, - _translate("MainWindow_reach", "Ouvrir un fichier"), + self._trad["open_file"], "", - _translate("MainWindow_reach", "Fichiers .ST (*.ST)") + - ";; " + - _translate("MainWindow_reach", "Fichiers .M (*.M)") + - ";; " + - _translate("MainWindow_reach", "Tous les fichiers (*)"), + ";; ".join(file_types), options=options ) @@ -159,20 +209,6 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.plot_kpc() self.plot_ac() - def messagebox_profile_editing(self): - msg_box = QtWidgets.QMessageBox() - msg_box.setIcon(QtWidgets.QMessageBox.Information) - msg_box.setWindowTitle(_translate("MainWindow_reach", - "Édition des profils sélectionnés")) - msg_box.setText(_translate("MainWindow_reach", - "Vous avez sélectionné plus de 5 profils." - " \nSeuls les 5 premiers seront édités.")) - msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok) - - return_value = msg_box.exec() - # if return_value == QtWidgets.QMessageBox.Ok: - # print('OK clicked') - def edit_profile(self): self.tableView.model().blockSignals(True) @@ -215,9 +251,9 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) self._plot_xy = PlotXY( - canvas = self.ui.canvas_1, + canvas = self._canvas_xy, data = self._reach, - toolbar = self.ui.toolbar_1 + toolbar = self._toolbar_xy ) self._plot_xy.draw() @@ -232,9 +268,9 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) self._plot_kpc = PlotKPZ( - canvas = self.ui.canvas_2, + canvas = self._canvas_kpc, data = self._reach, - toolbar = self.ui.toolbar_2 + toolbar = self._toolbar_kpc ) self._plot_kpc.draw() @@ -249,9 +285,9 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) self._plot_ac = PlotAC( - canvas = self.ui.canvas_3, + canvas = self._canvas_ac, data = self._reach, - toolbar = self.ui.toolbar_3, + toolbar = self._toolbar_ac, plot_xy = self._plot_xy ) self._plot_ac.draw() @@ -304,7 +340,7 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): if len(self.tableView.selectedIndexes()) > 0: row = self.index_selected_row() - self.ui.verticalSlider.setValue(row) + self.find(QSlider, "verticalSlider").setValue(row) self.select_plot_xy(row) self.select_plot_kpc(row) self.select_plot_ac(row) @@ -315,41 +351,37 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.tableView.model().blockSignals(True) if self._tablemodel.rowCount() != 0: - self.ui.verticalSlider.setMaximum(self._tablemodel.rowCount() - 1) + slider = self.find(QSlider, "verticalSlider") + slider.setMaximum(self._tablemodel.rowCount() - 1) - slider_value = self.ui.verticalSlider.value() + slider_value = slider.value() kp = self._reach.profile(slider_value).kp - self.ui.vertical_slider_label.setText( - _translate("MainWindow_reach", "Kp : ") + - f"{kp}" + "\n" + - _translate("MainWindow_reach", - "Profil N° : ") + - f"{slider_value + 1}" - ) - self.select_plot_xy(slider_value) self.select_plot_kpc(slider_value) self.select_row_profile_slider(slider_value) + self.update_statusbar() self.tableView.model().blockSignals(False) def increment_value_slider(self): - if 0 <= self.ui.verticalSlider.value() < self._tablemodel.rowCount() - 1: - self.ui.verticalSlider.setValue(self.ui.verticalSlider.value() + 1) + slider = self.find(QSlider, "verticalSlider") + if 0 <= slider.value() < self._tablemodel.rowCount() - 1: + slider.setValue(slider.value() + 1) def decrement_value_slider(self): - if 0 < self.ui.verticalSlider.value() < self._tablemodel.rowCount(): - self.ui.verticalSlider.setValue(self.ui.verticalSlider.value() - 1) + slider = self.find(QSlider, "verticalSlider") + if 0 < slider.value() < self._tablemodel.rowCount(): + slider.setValue(slider.value() - 1) - def insert_row(self): + def add(self): if len(self.tableView.selectedIndexes()) == 0: - self._tablemodel.insert_row(self._tablemodel.rowCount()) + self._tablemodel.add(self._tablemodel.rowCount()) else: row = self.index_selected_row() - self._tablemodel.insert_row(row + 1) + self._tablemodel.add(row + 1) - def delete_rows(self): + def delete(self): rows = sorted( list( set( @@ -384,14 +416,14 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.select_current_profile() self.changed_slider_value() - def move_row_up(self): + def move_up(self): row = self.index_selected_row() - self._tablemodel.move_row_up(row) + self._tablemodel.move_up(row) self.select_current_profile() - def move_row_down(self): + def move_down(self): row = self.index_selected_row() - self._tablemodel.move_row_down(row) + self._tablemodel.move_down(row) self.select_current_profile() def duplicate(self): @@ -412,7 +444,7 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self._tablemodel.duplicate(rows, profiles) self.select_current_profile() - def copy(self): + def _copy(self): rows = self.tableView\ .selectionModel()\ .selectedRows() @@ -428,7 +460,7 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self.copyTableIntoClipboard(table) - def paste(self): + def _paste(self): header, data = self.parseClipboardTable() if len(data) == 0: @@ -443,19 +475,19 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): self._tablemodel.paste(row, header, data) self.select_current_profile() - def undo(self): + def _undo(self): self._tablemodel.undo() self.select_current_profile() self.update_plot_xy() self.update_plot_kpc() - def redo(self): + def _redo(self): self._tablemodel.redo() self.select_current_profile() self.update_plot_xy() self.update_plot_kpc() - def handleSave(self): + def export_to_file(self): options = QFileDialog.Options() DEFAULT_DIRECTORY = '/home/' settings = QSettings(QSettings.IniFormat, QSettings.UserScope, 'MyOrg', ) @@ -464,32 +496,11 @@ class GeometryWindow(ASubMainWindow, ListedSubWindow): filename, filters = QFileDialog.getSaveFileName( self, - filter=_translate("MainWindow_reach", - "Files .ST(*.ST or *.st)") - + ";; " + - _translate("MainWindow_reach", "All files " - "(*)"), - options=options + filter = self._trad["file_st"] + ";; " + self._trad["file_all"], + options = options ) current_dir = os.path.split(filename)[0] or DEFAULT_DIRECTORY if filename != '': self._tablemodel.export_reach(filename) - - def handleOpen(self): - filename, filterName = QFileDialog.getOpenFileName(self) - - if filename != '': - with open(filename, 'r') as f: - reader = csv.reader(f, delimiter='\t') - header = next(reader) - buf = [] - for row in reader: - row[0] = QCheckBox("-") - buf.append(row) - - self._tablemodel = None - self._tablemodel = TableEditableModel(buf) - self.tableView.setModel(self._tablemodel) - filename = '' diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 58a2a031..8162807e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -44,7 +44,7 @@ from View.Configure.Window import ConfigureWindow from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow -# from View.Geometry.Window import GeometryWindow +from View.Geometry.Window import GeometryWindow # from View.BoundaryCondition.Window import BoundaryConditionWindow # from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow @@ -534,7 +534,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): ) if geometry is None: - geometry = GeometryWindow(model=self._study, parent=self) + geometry = GeometryWindow(study=self._study, config=self.conf, parent=self) geometry.show() else: geometry.activateWindow() diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index 08e36c84..65cd7687 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -115,10 +115,10 @@ class PamhyrTableModel(QAbstractTableModel): return options - def rowCount(self, parent): + def rowCount(self, parent=QModelIndex()): return len(self._lst) - def columnCount(self, parent): + def columnCount(self, parent=QModelIndex()): return len(self._headers) def headerData(self, section, orientation, role): diff --git a/src/View/ui/GeometryCrossSection.ui b/src/View/ui/GeometryCrossSection.ui new file mode 100644 index 00000000..b0c12393 --- /dev/null +++ b/src/View/ui/GeometryCrossSection.ui @@ -0,0 +1,134 @@ + + + MainWindow + + + + 0 + 0 + 1120 + 630 + + + + MainWindow + + + + + + + Qt::Horizontal + + + + + + + + + + + + + 0 + 0 + 1120 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + add + + + Add a point on cross-section + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + delete + + + Delete selected point(s) + + + + + + ressources/up.pngressources/up.png + + + up + + + Move up selected point(s) + + + + + + ressources/down.pngressources/down.png + + + down + + + Mode down selected point(s) + + + + + + ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png + + + sort + + + Sort points by nearest neighbor + + + + + + ressources/gtk-sort-descending.pngressources/gtk-sort-descending.png + + + resort + + + Sort reversed points by nearest neighbor + + + + + + diff --git a/src/View/ui/GeometryReach.ui b/src/View/ui/GeometryReach.ui new file mode 100644 index 00000000..e09d9d35 --- /dev/null +++ b/src/View/ui/GeometryReach.ui @@ -0,0 +1,231 @@ + + + MainWindow + + + + 0 + 0 + 1280 + 720 + + + + MainWindow + + + + + + + + + + + + Qt::Horizontal + + + + + Qt::Vertical + + + + Qt::Horizontal + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + true + + + true + + + + + + + + + + + + + ressources/go-up2.pngressources/go-up2.png + + + + + + + + + + + ressources/go-down1.pngressources/go-down1.png + + + + + + + + + + + + + 0 + 0 + 1280 + 22 + + + + + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + + + + + + + import + + + Import geometry + + + + + export + + + Export geometry + + + + + + ressources/gtk-add.pngressources/gtk-add.png + + + add + + + Add a cross-section + + + + + + ressources/gtk-remove.pngressources/gtk-remove.png + + + delete + + + Delete selected cross-section(s) + + + + + + ressources/edit.pngressources/edit.png + + + edit + + + Edit selected cross section(s) + + + + + + ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png + + + sort_asc + + + Sort cross-sections by ascending KP + + + + + + ressources/gtk-sort-descending.pngressources/gtk-sort-descending.png + + + sort_des + + + Sort cross-sections by descending KP + + + + + + ressources/up.pngressources/up.png + + + up + + + Move up selected cross-section(s) + + + + + + ressources/down.pngressources/down.png + + + down + + + Move down selected cross-section(s) + + + + + + From 11a7486a9ce1a82dc2e8e660c1529e827660f761 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 3 Oct 2023 16:26:50 +0200 Subject: [PATCH 28/44] Refactoring: Use ui file in Geometry profile edit window. --- src/Model/Geometry/Profile.py | 3 + src/View/Geometry/Profile/Plot.py | 17 +- src/View/Geometry/Profile/Table.py | 150 ++++-------- src/View/Geometry/Profile/Translate.py | 36 +++ src/View/Geometry/Profile/Window.py | 325 +++++++------------------ src/View/Geometry/Table.py | 61 +---- src/View/Geometry/Window.py | 13 +- src/View/ui/GeometryCrossSection.ui | 12 +- 8 files changed, 198 insertions(+), 419 deletions(-) create mode 100644 src/View/Geometry/Profile/Translate.py diff --git a/src/Model/Geometry/Profile.py b/src/Model/Geometry/Profile.py index d8137932..606b2e29 100644 --- a/src/Model/Geometry/Profile.py +++ b/src/Model/Geometry/Profile.py @@ -56,6 +56,9 @@ class Profile(object): self._profile_type = _type + def __len__(self): + return len(self._points) + @property def number_points(self): return len(self._points) diff --git a/src/View/Geometry/Profile/Plot.py b/src/View/Geometry/Profile/Plot.py index 076c2703..12bb9aee 100644 --- a/src/View/Geometry/Profile/Plot.py +++ b/src/View/Geometry/Profile/Plot.py @@ -19,8 +19,8 @@ import logging from tools import timer, trace -from View.Plot.APlot import APlot -from View.Plot.mpl_canvas_onpick_event import OnpickEvent +from View.Tools.PamhyrPlot import PamhyrPlot +from View.Tools.Plot.OnPickEvent import OnpickEvent from PyQt5.QtCore import ( QCoreApplication @@ -30,12 +30,15 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, table=None): +class Plot(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + table=None, parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self._table = table diff --git a/src/View/Geometry/Profile/Table.py b/src/View/Geometry/Profile/Table.py index 2de665aa..d1a670ac 100644 --- a/src/View/Geometry/Profile/Table.py +++ b/src/View/Geometry/Profile/Table.py @@ -29,10 +29,11 @@ from PyQt5.QtWidgets import ( QMessageBox, QStyledItemDelegate, QLineEdit ) from PyQt5.QtCore import ( - QModelIndex, Qt, QAbstractTableModel, - QVariant, QCoreApplication + QModelIndex, Qt, QVariant, QCoreApplication ) +from View.Tools.PamhyrTable import PamhyrTableModel + from Model.Geometry.PointXYZ import PointXYZ from Model.Geometry.ProfileXYZ import ProfileXYZ @@ -43,43 +44,22 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableEditableModel(QAbstractTableModel): - def __init__(self, profile: ProfileXYZ, header=None, undo=None): - QAbstractTableModel.__init__(self) - - self._undo_stack = undo - self._profile = profile - - if header is None: - self._header = [ - "X (m)", "Y (m)", "Z (m)", - _translate("MainWindowProfile", "Nom"), - _translate("MainWindowProfile", "Abs en travers (m)") - ] - else: - self._header = header - - def rowCount(self, parent=QModelIndex()): - return self._profile.number_points - - def columnCount(self, parent=QModelIndex()): - return len(self._header) - +class GeometryProfileTableModel(PamhyrTableModel): def data(self, index, role=Qt.DisplayRole): if index.isValid(): if role == Qt.DisplayRole: value = "" if index.column() == 0: - value = self._profile.point(index.row()).x + value = self._data.point(index.row()).x elif index.column() == 1: - value = self._profile.point(index.row()).y + value = self._data.point(index.row()).y elif index.column() == 2: - value = self._profile.point(index.row()).z + value = self._data.point(index.row()).z elif index.column() == 3: - value = self._profile.point(index.row()).name + value = self._data.point(index.row()).name elif index.column() == 4: - station = self._profile.get_station() + station = self._data.get_station() if station is None: return "-" else: @@ -95,25 +75,25 @@ class TableEditableModel(QAbstractTableModel): return Qt.AlignHCenter | Qt.AlignVCenter if index.column() == 2: - value = self._profile.point(index.row()).z + value = self._data.point(index.row()).z if role == Qt.ForegroundRole: - if value == self._profile.z_min(): + if value == self._data.z_min(): return QColor("red") - elif value == self._profile.z_max(): + elif value == self._data.z_max(): return QColor("blue") if role == Qt.ToolTipRole: - if value == self._profile.z_min(): + if value == self._data.z_min(): return _translate("MainWindowProfile", "La cote du fond", "Z minimale") - elif value == self._profile.z_max(): + elif value == self._data.z_max(): return _translate("MainWindowProfile", "La cote maximale", "Z maximale") if index.column() == 3: - value = self._profile.point(index.row()).name + value = self._data.point(index.row()).name if value.strip().upper() in ["RG", "RD"]: if role == Qt.FontRole: @@ -132,21 +112,6 @@ class TableEditableModel(QAbstractTableModel): return QVariant() - def headerData(self, section, orientation, role=Qt.DisplayRole): - if orientation == Qt.Horizontal and role == Qt.DisplayRole: - return self._header[section] - elif orientation == Qt.Vertical and role == Qt.DisplayRole: - return str(section + 1) - - if role == Qt.ToolTipRole and section == 4: - return _translate( - "MainWindowProfile", - "Abscisse en travers calculée en projétant les points" - " \nsur le plan défini par les deux points nommés extrêmes " - ) - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): row = index.row() column = index.column() @@ -154,34 +119,34 @@ class TableEditableModel(QAbstractTableModel): if role == Qt.EditRole: try: if column == 0: - self._undo_stack.push( + self._undo.push( SetXCommand( - self._profile, row, - self._profile.point(row).x, + self._data, row, + self._data.point(row).x, value ) ) elif column == 1: - self._undo_stack.push( + self._undo.push( SetYCommand( - self._profile, row, - self._profile.point(row).y, + self._data, row, + self._data.point(row).y, value ) ) elif column == 2: - self._undo_stack.push( + self._undo.push( SetZCommand( - self._profile, row, - self._profile.point(row).z, + self._data, row, + self._data.point(row).z, value ) ) elif column == 3: - self._undo_stack.push( + self._undo.push( SetNameCommand( - self._profile, row, - self._profile.point(row).name, + self._data, row, + self._data.point(row).name, value ) ) @@ -213,9 +178,9 @@ class TableEditableModel(QAbstractTableModel): def insert_row(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row - 1) - self._undo_stack.push( + self._undo.push( AddCommand( - self._profile, row + self._data, row ) ) @@ -225,9 +190,9 @@ class TableEditableModel(QAbstractTableModel): def remove_rows(self, rows, parent=QModelIndex()): self.beginRemoveRows(parent, rows[0], rows[-1]) - self._undo_stack.push( + self._undo.push( DelCommand( - self._profile, rows + self._data, rows ) ) @@ -239,15 +204,15 @@ class TableEditableModel(QAbstractTableModel): reverse = (order != Qt.AscendingOrder) - self._undo_stack.push( + self._undo.push( SortCommand( - self._profile, column, reverse + self._data, column, reverse ) ) self.layoutChanged.emit() - def move_row_up(self, row, parent=QModelIndex()): + def move_up(self, row, parent=QModelIndex()): if row <= 0: return @@ -255,26 +220,26 @@ class TableEditableModel(QAbstractTableModel): self.beginMoveRows(parent, row - 1, row - 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._profile, "up", row + self._data, "up", row ) ) self.endMoveRows() self.layoutChanged.emit() - def move_row_down(self, row_to_move, parent=QModelIndex()): - if row > self._profile.number_points: + def move_down(self, row_to_move, parent=QModelIndex()): + if row > self._data.number_points: return target = row self.beginMoveRows(parent, row + 1, row + 1, parent, target) - self._undo_stack.push( + self._undo.push( MoveCommand( - self._profile, "down", row + self._data, "down", row ) ) @@ -282,7 +247,7 @@ class TableEditableModel(QAbstractTableModel): self.layoutChanged.emit() def paste(self, row, header, data): - if row > self._profile.number_points: + if row > self._data.number_points: return if len(data) == 0: @@ -290,12 +255,12 @@ class TableEditableModel(QAbstractTableModel): self.layoutAboutToBeChanged.emit() - self._undo_stack.push( + self._undo.push( PasteCommand( - self._profile, row, + self._data, row, list( map( - lambda d: self._profile.point_from_data(header, d), + lambda d: self._data.point_from_data(header, d), data ) ) @@ -306,32 +271,9 @@ class TableEditableModel(QAbstractTableModel): self.layoutChanged.emit() def undo(self): - self._undo_stack.undo() + self._undo.undo() self.layoutChanged.emit() def redo(self): - self._undo_stack.redo() + self._undo.redo() self.layoutChanged.emit() - - -class Delegate(QStyledItemDelegate): - def __init__(self, parent=None, setModelDataEvent=None): - super(Delegate, self).__init__(parent) - self.setModelDataEvent = setModelDataEvent - - def createEditor(self, parent, option, index): - index.model().data(index, Qt.DisplayRole) - return QLineEdit(parent) - - def setEditorData(self, editor, index): - value = index.model().data(index, Qt.DisplayRole) - editor.setText(str(value)) - - def setModelData(self, editor, model, index): - model.setData(index, editor.text()) - - if not self.setModelDataEvent is None: - self.setModelDataEvent() - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) diff --git a/src/View/Geometry/Profile/Translate.py b/src/View/Geometry/Profile/Translate.py new file mode 100644 index 00000000..148426aa --- /dev/null +++ b/src/View/Geometry/Profile/Translate.py @@ -0,0 +1,36 @@ +# Translate.py -- Pamhyr +# Copyright (C) 2023 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate +from View.Geometry.Translate import GeometryTranslate + +_translate = QCoreApplication.translate + +class GeometryProfileTranslate(GeometryTranslate): + def __init__(self): + super(GeometryProfileTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "x": _translate("Geometry", "X (m)"), + "y": _translate("Geometry", "Y (m)"), + "z": _translate("Geometry", "Z (m)"), + "name": _translate("Geometry", "Name"), + "abs": _translate("Geometry", "Traversal abs (m)"), + } diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index eca6e7af..90fe1f44 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -31,206 +31,184 @@ from PyQt5.QtCore import ( ) from PyQt5.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QCheckBox, - QUndoStack, QShortcut, + QUndoStack, QShortcut, QTableView, QAbstractItemView, + QHeaderView, QVBoxLayout, QAction, ) from Model.Geometry.Reach import Reach from Model.Geometry.ProfileXYZ import ProfileXYZ -from View.Tools.ASubWindow import ASubMainWindow +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas -from View.Geometry.Profile.mainwindow_ui_profile import Ui_MainWindow from View.Geometry.Profile.Plot import Plot -from View.Geometry.Profile.Table import * +from View.Geometry.Profile.Table import GeometryProfileTableModel +from View.Geometry.Profile.Translate import GeometryProfileTranslate _translate = QCoreApplication.translate -class ProfileWindow(ASubMainWindow): - def __init__(self, profile=None, title="Profile", parent=None): - self._title = title - self.parent = parent - super(ProfileWindow, self).__init__( - name=self._title, - parent=self.parent - ) - - self.ui = Ui_MainWindow() - self.ui.setupUi(self) +class ProfileWindow(PamhyrWindow): + _pamhyr_ui = "GeometryCrossSection" + _pamhyr_name = "Geometry cross-section" + def __init__(self, profile=None, study=None, config=None, parent=None): self._profile = profile - self._model = None - self.setup_window() - self.setup_sc() - self.setup_model() - self.setup_connections() - self.plot() - - self._model.dataChanged.connect(self.update_plot) - self.fileName = None - - # self.ui.tableView.installEventFilter(self) - # self._model.dataChanged.connect(self.tableview_is_modified) - - # self.ui.btn_go_back.setEnabled(False) - # self.ui.btn_check.setEnabled(False) - # self._model.dataChanged.connect(self.set_enable_cancel_btn) - # self._model.dataChanged.connect(self.set_enable_validate_changes_btn) - # self.ui.btn_reset.setEnabled(False) - # self._model.dataChanged.connect(self.set_enable_go_back_initial_state_btn) - - def setup_window(self): - header = _translate("MainWindowProfile", "Profile") - - name = self._profile.name - if (name is None) or (name == ""): - name = _translate("MainWindowProfile", "(no name)") - - self._title = ( - header + " - " + - f"{self._profile.reach.name}" + " - " + - f"{name} ({self._profile.kp})" + name = f"{self._pamhyr_name} - {self._profile.name} {self._profile.kp}" + super(ProfileWindow, self).__init__( + title = name, + study = study, + config = config, + trad = GeometryProfileTranslate(), + parent = parent ) - self.setWindowTitle(self._title) + self.setup_table() + self.setup_plot() + self.setup_connections() - def setup_sc(self): - self._undo_stack = QUndoStack() + def setup_table(self): + table_headers = self._trad.get_dict("table_headers") - self.undo_sc = QShortcut(QKeySequence.Undo, self) - self.redo_sc = QShortcut(QKeySequence.Redo, self) - self.copy_sc = QShortcut(QKeySequence.Copy, self) - self.paste_sc = QShortcut(QKeySequence.Paste, self) - - def setup_model(self): - self._model = TableEditableModel( - profile = self._profile, + table = self.find(QTableView, "tableView") + self._tablemodel = GeometryProfileTableModel( + table_view = table, + table_headers = table_headers, + editable_headers = ["name", "x", "y", "z"], + data = self._profile, undo = self._undo_stack ) + table.setModel(self._tablemodel) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) - self.ui.tableView.setModel(self._model) - self.ui.tableView.setItemDelegate(Delegate()) + def setup_plot(self): + self._tablemodel.blockSignals(True) - def setup_connections(self): - self.ui.btn_sort_asc_x.triggered.connect(self.sort_X_ascending) - self.ui.btn_sort_desc_x.triggered.connect(self.sort_X_descending) - self.ui.btn_sort_asc_y.triggered.connect(self.sort_Y_ascending) - self.ui.btn_sort_desc_y.triggered.connect(self.sort_Y_descending) - self.ui.btn_move_up.triggered.connect(self.move_row_up) - self.ui.btn_move_down.triggered.connect(self.move_row_down) - self.ui.btn_export.triggered.connect(self.handleSave) - self.ui.btn_add.triggered.connect(self.insert_row) - self.ui.btn_delete.triggered.connect(self.delete_row) - # self.ui.btn_copy.clicked.connect(self.copyTable) - # self.ui.btn_paste.clicked.connect(self.pasteTable) - # self.ui.btn_check.clicked.connect(self.validate_changes) - # self.ui.btn_go_back.clicked.connect(self.cancel_validate_changes) - # self.ui.btn_reset.clicked.connect(self.go_back_to_initial_state) - - self.undo_sc.activated.connect(self.undo) - self.redo_sc.activated.connect(self.redo) - self.copy_sc.activated.connect(self.copy) - self.paste_sc.activated.connect(self.paste) - - def plot(self): - self.ui.tableView.model().blockSignals(True) + self._canvas = MplCanvas(width=3, height=4, dpi=100) + self._canvas.setObjectName("canvas") + self._toolbar = PamhyrPlotToolbar( + self._canvas, self, + items = ["home", "zoom", "save", "iso", "back/forward", "move"] + ) + self._plot_layout = self.find(QVBoxLayout, "verticalLayout") + self._plot_layout.addWidget(self._toolbar) + self._plot_layout.addWidget(self._canvas) self._plot = Plot( - canvas = self.ui.canvas, + canvas = self._canvas, data = self._profile, - toolbar = None, - table = self.ui.tableView, + toolbar = self._toolbar, + table = self.find(QTableView, "tableView") ) self._plot.draw() - self.ui.tableView.model().blockSignals(False) + self._tablemodel.blockSignals(False) + + def setup_connections(self): + actions = { + "action_sort_asc": self.sort_X_ascending, + "action_sort_des": self.sort_X_descending, + "action_up": self.move_up, + "action_down": self.move_down, + "action_add": self.add, + "action_delete": self.delete, + } + + for action in actions: + self.find(QAction, action)\ + .triggered.connect(actions[action]) + + self._tablemodel.dataChanged.connect(self.update_plot) def update_plot(self): - self.ui.tableView.model().blockSignals(True) + self._tablemodel.blockSignals(True) # TODO: Do not rebuild all graph self._plot.update() - self.ui.tableView.model().blockSignals(False) + self._tablemodel.blockSignals(False) def index_selected_row(self): - rows = self.ui.tableView\ + rows = self._tablemodel\ .selectionModel()\ .selectedRows() if len(rows) == 0: return 0 - return self.ui.tableView\ + return self._tablemodel\ .selectionModel()\ .selectedRows()[0]\ .row() - def insert_row(self): - if len(self.ui.tableView.selectedIndexes()) == 0: - self._model.insert_row(self._model.rowCount()) + def add(self): + if len(self._tablemodel.selectedIndexes()) == 0: + self._tablemodel.insert_row(self._tablemodel.rowCount()) else: row = self.index_selected_row() - self._model.insert_row(row + 1) + self._tablemodel.insert_row(row + 1) self.update_plot() - def delete_row(self): + def delete(self): rows = sorted( list( set( - [index.row() for index in self.ui.tableView.selectedIndexes()] + [index.row() for index in self._tablemodel.selectedIndexes()] ) ) ) if len(rows) > 0: - self._model.remove_rows(rows) + self._tablemodel.remove_rows(rows) self.update_plot() def sort_X_ascending(self): - self._model.sort('x', order=Qt.AscendingOrder) + self._tablemodel.sort('x', order=Qt.AscendingOrder) self.update_plot() def sort_X_descending(self): - self._model.sort('x', order=Qt.DescendingOrder) + self._tablemodel.sort('x', order=Qt.DescendingOrder) self.update_plot() def sort_Y_ascending(self): - self._model.sort('y', order=Qt.AscendingOrder) + self._tablemodel.sort('y', order=Qt.AscendingOrder) self.update_plot() def sort_Y_descending(self): - self._model.sort('y', order=Qt.DescendingOrder) + self._tablemodel.sort('y', order=Qt.DescendingOrder) self.update_plot() - def move_row_down(self): + def move_down(self): rows = list( set( - [index.row() for index in self.ui.tableView.selectedIndexes()] + [index.row() for index in self._tablemodel.selectedIndexes()] ) ) for row in rows: - if row < self._model.rowCount() - 1: - self._model.move_row_down(row) + if row < self._tablemodel.rowCount() - 1: + self._tablemodel.move_down(row) self.update_plot() - def move_row_up(self): + def move_up(self): rows = list( set( - [index.row() for index in self.ui.tableView.selectedIndexes()] + [index.row() for index in self._tablemodel.selectedIndexes()] ) ) for row in rows: if 0 < row: - self._model.move_row_up(row) + self._tablemodel.move_up(row) self.update_plot() def copy(self): - rows = self.ui.tableView\ + rows = self._tablemodel\ .selectionModel()\ .selectedRows() table = [] @@ -258,136 +236,13 @@ class ProfileWindow(ASubMainWindow): row.append(self._profile) row = self.index_selected_row() - self._model.paste(row, header, data) + self._tablemodel.paste(row, header, data) self.update_plot() def undo(self): - self._model.undo() + self._tablemodel.undo() self.update_plot() def redo(self): - self._model.redo() + self._tablemodel.redo() self.update_plot() - - def handleSave(self): - if self.fileName is None or self.fileName == '': - self.fileName, self.filters = QFileDialog.getSaveFileName( - self, filter="CSV files (*.csv)" - ) - - if self.fileName != '': - with open(self.fileName, 'w') as stream: - csvout = csv.writer(stream, delimiter='\t', quotechar=' ', - escapechar=None, - quoting=csv.QUOTE_NONNUMERIC, - lineterminator='\n') - - for row in range(self._model.rowCount(QModelIndex())): - rowdata = [] - for column in range(self._model.columnCount(QModelIndex())): - item = self._model.index( - row, column, QModelIndex() - ).data(Qt.DisplayRole) - - if item is not None: - rowdata.append(item) - - if item == 'nan': - rowdata.remove(item) - - csvout.writerow(rowdata) - - def handleOpen(self): - self.fileName, self.filterName = QFileDialog.getOpenFileName(self) - - if self.fileName != '': - with open(self.fileName, 'r') as f: - reader = csv.reader(f, delimiter='\t') - header = next(reader) - - buf = [] - for row in reader: - row[0] = QCheckBox("-") - buf.append(row) - - self._model = None - self._model = TableEditableModel(buf) - self.ui.tableView.setModel(self._model) - self.fileName = '' - - def set_enable_validate_changes_btn(self): - self.ui.btn_check.setEnabled(True) - - def set_enable_cancel_btn(self): - self.ui.btn_go_back.setEnabled(True) - - def set_enable_go_back_initial_state_btn(self): - self.ui.btn_reset.setEnabled(True) - - def delete_empty_rows(self): - if self._model.data_contains_nan(): - buttonReply = QtWidgets.QMessageBox.question( - self, - _translate("MainWindowProfile", - "Suppression les lignes incomplètes"), - _translate("MainWindowProfile", - "Supprimer les lignes des cellules" - " non renseignées ?"), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - - if buttonReply == QtWidgets.QMessageBox.Yes: - self._model.delete_empty_rows() - - if buttonReply == QtWidgets.QMessageBox.No: - pass - - def remove_duplicates_point_names(self): - counter_list = [] - list_deleted_names = [] - - for ind, name_point in enumerate(self._model.name): - if name_point not in counter_list: - counter_list.append(name_point) - elif len(name_point.strip()) > 0 and name_point in counter_list: - if name_point not in list_deleted_names: - list_deleted_names.append(name_point) - - if list_deleted_names: - self.msg_box_check_duplication_names(list_deleted_names) - - def msg_box_check_duplication_names(self, list_deleted_names): - if len(list_deleted_names) == 1: - text = _translate("MainWindowProfile", - "Le nom ''{}'' est dupliqué." - " \n\nYes : Ne garder que la première occurrence. \nNo :" - " Annuler la suppression.".format(list_deleted_names[0])) - else: - text = _translate("MainWindowProfile", - "Les noms suivants : \n{} sont dupliqués" - " \n\nYes : Ne garder que la première occurrence de " - "chaque nom. \nNo :" - " Annuler la suppression.".format(list_deleted_names)) - - buttonReply = QtWidgets.QMessageBox.question( - self, _translate("MainWindowProfile", - "Suppression des noms répétés"), - text, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - - if buttonReply == QtWidgets.QMessageBox.Yes: - self._model.remove_duplicates_names() - - def ask_quit(self): - choice = QtWidgets.QMessageBox.question( - self, - _translate("MainWindowProfile", "Quittez ?"), - _translate("MainWindowProfile", - "Etes-vous sûr de vouloir quitter ?"), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - return choice == QtWidgets.QMessageBox.Yes - - def initial_state_model(self, profile_initial_state): - return profile_initial_state diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 0ff5e0c8..89fafafa 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -46,7 +46,7 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableEditableModel(PamhyrTableModel): +class GeometryReachTableModel(PamhyrTableModel): def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return QVariant() @@ -223,62 +223,3 @@ class TableEditableModel(PamhyrTableModel): self.layoutAboutToBeChanged.emit() self.layoutChanged.emit() - -# TODO: Delete useless delegate -class Delegate(QStyledItemDelegate): - def __init__(self, parent=None, setModelDataEvent=None): - super(Delegate, self).__init__(parent) - self.setModelDataEvent = setModelDataEvent - - def createEditor(self, parent, option, index): - index.model().data(index, Qt.DisplayRole) - return QLineEdit(parent) - - def setEditorData(self, editor, index): - value = index.model().data(index, Qt.DisplayRole) - editor.setText(str(value)) - - def setModelData(self, editor, model, index): - model.setData(index, editor.text()) - - if not self.setModelDataEvent is None: - self.setModelDataEvent() - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) - -class Delegate1(QStyledItemDelegate): - def __init__(self, owner, choices): - super().__init__(owner) - self.items = choices - - def paint(self, painter, option, index): - if isinstance(self.parent(), QAbstractItemView): - self.parent().openPersistentEditor(index) - super(Delegate1, self).paint(painter, option, index) - - def createEditor(self, parent, option, index): - editor = QComboBox(parent) - # editor.currentIndexChanged.connect(self.commit_editor) - editor.addItems(self.items) - return editor - - def setEditorData(self, editor, index): - editor.blockSignals(True) - text = index.model().data(index, Qt.DisplayRole) - try: - i = self.items.index(text) - except ValueError: - i = 0 - editor.setCurrentIndex(i) - editor.blockSignals(False) - - def setModelData(self, editor, model, index): - model.setData(index, editor.currentText(), Qt.DisplayRole) - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) - - @pyqtSlot() - def currentIndexChanged(self): - self.commitData.emit(self.sender()) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 1b334bb7..2fccde41 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -39,17 +39,16 @@ from PyQt5.QtWidgets import ( QLabel, ) -from View.Geometry.PlotXY import PlotXY -from View.Geometry.PlotKPZ import PlotKPZ -from View.Geometry.PlotAC import PlotAC - from View.Tools.PamhyrWindow import PamhyrWindow from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.Tools.Plot.PamhyrCanvas import MplCanvas -from View.Geometry.Table import * +from View.Geometry.Table import GeometryReachTableModel +from View.Geometry.PlotXY import PlotXY +from View.Geometry.PlotAC import PlotAC +from View.Geometry.PlotKPZ import PlotKPZ from View.Geometry.Translate import GeometryTranslate -# from View.Geometry.Profile.Window import ProfileWindow +from View.Geometry.Profile.Window import ProfileWindow _translate = QCoreApplication.translate @@ -87,7 +86,7 @@ class GeometryWindow(PamhyrWindow): table_headers = self._trad.get_dict("table_headers") table = self.find(QTableView, "tableView") - self._tablemodel = TableEditableModel( + self._tablemodel = GeometryReachTableModel( table_view = table, table_headers = table_headers, editable_headers = ["name", "kp"], diff --git a/src/View/ui/GeometryCrossSection.ui b/src/View/ui/GeometryCrossSection.ui index b0c12393..67e06ced 100644 --- a/src/View/ui/GeometryCrossSection.ui +++ b/src/View/ui/GeometryCrossSection.ui @@ -51,8 +51,8 @@ - - + + @@ -104,25 +104,25 @@ Mode down selected point(s) - + ressources/gtk-sort-ascending.pngressources/gtk-sort-ascending.png - sort + sort_asc Sort points by nearest neighbor - + ressources/gtk-sort-descending.pngressources/gtk-sort-descending.png - resort + sort_des Sort reversed points by nearest neighbor From c744797c6fa77872614a14178f7a70519b6afd26 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 3 Oct 2023 16:39:31 +0200 Subject: [PATCH 29/44] refactoring: Minor fix in geometry. --- src/View/Geometry/Profile/Window.py | 12 ++++++++---- src/View/Geometry/Window.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/View/Geometry/Profile/Window.py b/src/View/Geometry/Profile/Window.py index 90fe1f44..e4112f40 100644 --- a/src/View/Geometry/Profile/Window.py +++ b/src/View/Geometry/Profile/Window.py @@ -145,7 +145,8 @@ class ProfileWindow(PamhyrWindow): .row() def add(self): - if len(self._tablemodel.selectedIndexes()) == 0: + table = self.find(QTableView, "tableView") + if len(table.selectedIndexes()) == 0: self._tablemodel.insert_row(self._tablemodel.rowCount()) else: row = self.index_selected_row() @@ -153,10 +154,11 @@ class ProfileWindow(PamhyrWindow): self.update_plot() def delete(self): + table = self.find(QTableView, "tableView") rows = sorted( list( set( - [index.row() for index in self._tablemodel.selectedIndexes()] + [index.row() for index in table.selectedIndexes()] ) ) ) @@ -184,7 +186,8 @@ class ProfileWindow(PamhyrWindow): def move_down(self): rows = list( set( - [index.row() for index in self._tablemodel.selectedIndexes()] + [index.row() for index in + self.find(QTableView, "tableView").selectedIndexes()] ) ) @@ -197,7 +200,8 @@ class ProfileWindow(PamhyrWindow): def move_up(self): rows = list( set( - [index.row() for index in self._tablemodel.selectedIndexes()] + [index.row() for index in + self.find(QTableView, "tableView").selectedIndexes()] ) ) diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index 2fccde41..af27773f 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -36,7 +36,7 @@ from PyQt5.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QCheckBox, QUndoStack, QShortcut, QTableView, QHeaderView, QAction, QSlider, QPushButton, QVBoxLayout, - QLabel, + QLabel, QAbstractItemView, ) from View.Tools.PamhyrWindow import PamhyrWindow From 7a5b4779e9ca999df83f04392ab85487dfc9cb56 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Tue, 3 Oct 2023 17:11:06 +0200 Subject: [PATCH 30/44] refactoring: Minor fix in geometry. --- src/View/Geometry/Table.py | 8 ++++---- src/View/Geometry/Window.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/View/Geometry/Table.py b/src/View/Geometry/Table.py index 89fafafa..e9f6eac6 100644 --- a/src/View/Geometry/Table.py +++ b/src/View/Geometry/Table.py @@ -115,7 +115,7 @@ class GeometryReachTableModel(PamhyrTableModel): return False # @QtCore.pyqtSlot() - def insert_row(self, row, parent=QModelIndex()): + def add(self, row, parent=QModelIndex()): self.beginInsertRows(parent, row, row - 1) self._undo.push( @@ -127,7 +127,7 @@ class GeometryReachTableModel(PamhyrTableModel): self.endInsertRows() self.layoutChanged.emit() - def remove_rows(self, rows, parent=QModelIndex()): + def delete(self, rows, parent=QModelIndex()): self.beginRemoveRows(parent, rows[0], rows[-1]) self._undo.push( @@ -152,7 +152,7 @@ class GeometryReachTableModel(PamhyrTableModel): self.layoutChanged.emit() - def move_row_up(self, row, parent=QModelIndex()): + def move_up(self, row, parent=QModelIndex()): if row <= 0: return @@ -169,7 +169,7 @@ class GeometryReachTableModel(PamhyrTableModel): self.endMoveRows() self.layoutChanged.emit() - def move_row_down(self, row, parent=QModelIndex()): + def move_down(self, row, parent=QModelIndex()): if row > self._data.number_profiles: return diff --git a/src/View/Geometry/Window.py b/src/View/Geometry/Window.py index af27773f..516bf66f 100644 --- a/src/View/Geometry/Window.py +++ b/src/View/Geometry/Window.py @@ -390,7 +390,7 @@ class GeometryWindow(PamhyrWindow): ) if len(rows) > 0: - self._tablemodel.remove_rows(rows) + self._tablemodel.delete(rows) self.update_plot_xy() self.select_current_profile() @@ -405,12 +405,12 @@ class GeometryWindow(PamhyrWindow): .row() def sort_ascending(self): - self._tablemodel.sort_profiles(True) + self._tablemodel.sort_profiles(False) self.select_current_profile() self.changed_slider_value() def sort_descending(self): - self._tablemodel.sort_profiles(False) + self._tablemodel.sort_profiles(True) self.select_current_profile() self.changed_slider_value() From ea3654f1f84bf85ef8f219e3fc71438f5b14c069 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 10:35:54 +0200 Subject: [PATCH 31/44] refactoring: refacto BC. --- src/View/BoundaryCondition/Edit/Plot.py | 18 ++++++++++-------- src/View/BoundaryCondition/Edit/Window.py | 7 +++---- src/View/MainWindow.py | 2 +- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/View/BoundaryCondition/Edit/Plot.py b/src/View/BoundaryCondition/Edit/Plot.py index a7815e60..e5e80c1e 100644 --- a/src/View/BoundaryCondition/Edit/Plot.py +++ b/src/View/BoundaryCondition/Edit/Plot.py @@ -21,7 +21,7 @@ import logging from datetime import datetime from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -33,16 +33,18 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, - mode = "time", toolbar=None): +class Plot(PamhyrPlot): + def __init__(self, mode = "time", data=None, + trad=None, canvas=None, toolbar=None, + parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) - self._trad = BCETranslate() self._table_headers = self._trad.get_dict("table_headers") self._mode = mode diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py index 13d277e6..5661d6bc 100644 --- a/src/View/BoundaryCondition/Edit/Window.py +++ b/src/View/BoundaryCondition/Edit/Window.py @@ -41,8 +41,8 @@ from PyQt5.QtWidgets import ( QHeaderView, QDoubleSpinBox, QVBoxLayout, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.BoundaryCondition.Edit.translate import BCETranslate from View.BoundaryCondition.Edit.UndoCommand import SetMetaDataCommand @@ -122,8 +122,6 @@ class EditBoundaryConditionWindow(PamhyrWindow): parent = parent ) - self.ui.setWindowTitle(self._title) - self.setup_table() self.setup_plot() self.setup_data() @@ -183,6 +181,7 @@ class EditBoundaryConditionWindow(PamhyrWindow): canvas = self.canvas, data = self._data, mode = self._study.time_system, + trad = self._trad, toolbar = self.toolbar, ) self.plot.draw() diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 8162807e..266dc321 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -45,7 +45,7 @@ from View.Study.Window import NewStudyWindow from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow -# from View.BoundaryCondition.Window import BoundaryConditionWindow +from View.BoundaryCondition.Window import BoundaryConditionWindow # from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow From 500885ec4d05d45bed7e356ba9417940c162b6d2 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 11:26:43 +0200 Subject: [PATCH 32/44] refactoring: refacto LC. --- src/View/LateralContribution/Edit/Plot.py | 22 ++++++---- src/View/LateralContribution/Edit/Window.py | 24 +++++----- .../LateralContribution/Edit/translate.py | 29 ++++++------ src/View/LateralContribution/Table.py | 11 +++-- src/View/LateralContribution/Window.py | 14 +++--- src/View/LateralContribution/translate.py | 44 ++++++++----------- src/View/MainWindow.py | 2 +- src/View/Tools/PamhyrWidget.py | 2 + 8 files changed, 77 insertions(+), 71 deletions(-) diff --git a/src/View/LateralContribution/Edit/Plot.py b/src/View/LateralContribution/Edit/Plot.py index 5843ee56..ea1c0454 100644 --- a/src/View/LateralContribution/Edit/Plot.py +++ b/src/View/LateralContribution/Edit/Plot.py @@ -19,7 +19,7 @@ from datetime import datetime from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -29,15 +29,19 @@ from View.LateralContribution.Edit.translate import * _translate = QCoreApplication.translate -class Plot(APlot): - def __init__(self, canvas=None, data=None, - mode = "time", toolbar=None): +class Plot(PamhyrPlot): + def __init__(self, mode = "time", data=None, + canvas=None, trad=None, toolbar=None, + parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) + self._table_headers = self._trad.get_dict("table_headers") self._mode = mode def custom_ticks(self): @@ -105,10 +109,10 @@ class Plot(APlot): # Plot label header = self.data.header self.canvas.axes.set_xlabel( - table_headers[header[0]], color='black', fontsize=10 + self._table_headers[header[0]], color='black', fontsize=10 ) self.canvas.axes.set_ylabel( - table_headers[header[1]], color='black', fontsize=10 + self._table_headers[header[1]], color='black', fontsize=10 ) self.canvas.axes.autoscale_view(True, True, True) diff --git a/src/View/LateralContribution/Edit/Window.py b/src/View/LateralContribution/Edit/Window.py index f37c1759..55f1307f 100644 --- a/src/View/LateralContribution/Edit/Window.py +++ b/src/View/LateralContribution/Edit/Window.py @@ -36,11 +36,10 @@ from PyQt5.QtWidgets import ( QHeaderView, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.LateralContribution.translate import long_types -from View.LateralContribution.Edit.translate import table_headers, retranslate +from View.LateralContribution.Edit.translate import LCETranslate from View.LateralContribution.Edit.Table import TableModel from View.LateralContribution.Edit.Plot import Plot @@ -54,6 +53,7 @@ class EditLateralContributionWindow(PamhyrWindow): study=None, config=None, parent=None): self._data = data + trad = LCETranslate() name = self._pamhyr_name if self._data is not None: @@ -63,13 +63,14 @@ class EditLateralContributionWindow(PamhyrWindow): _translate("Edit lateral contribution", self._pamhyr_name) + f" - {study.name} " + f" - {self._data.name} ({self._data.id}) " + - f"({long_types[self._data.lctype]} - {edge_name})" + f"({trad.get_dict('long_types')[self._data.lctype]} - {edge_name})" ) super(EditLateralContributionWindow, self).__init__( title = name, study = study, config = config, + trad = trad, parent = parent ) @@ -78,17 +79,17 @@ class EditLateralContributionWindow(PamhyrWindow): self.setup_connections() def setup_table(self): - retranslate() - headers = {} - for h in self._data.header: - headers[h] = table_headers[h] - self._delegate_time = PamhyrExTimeDelegate( data = self._data, mode = self._study.time_system, parent = self ) + headers = {} + table_headers = self._trad.get_dict("table_headers") + for h in self._data.header: + headers[h] = table_headers[h] + table = self.find(QTableView, "tableView") self._table = TableModel( table_view = table, @@ -99,6 +100,7 @@ class EditLateralContributionWindow(PamhyrWindow): }, data = self._data, undo = self._undo_stack, + trad = self._trad, opt_data = self._study.time_system ) @@ -121,6 +123,8 @@ class EditLateralContributionWindow(PamhyrWindow): data = self._data, mode = self._study.time_system, toolbar = self.toolbar, + trad = self._trad, + parent = self ) self.plot.draw() diff --git a/src/View/LateralContribution/Edit/translate.py b/src/View/LateralContribution/Edit/translate.py index f04534e0..247cdc4e 100644 --- a/src/View/LateralContribution/Edit/translate.py +++ b/src/View/LateralContribution/Edit/translate.py @@ -18,21 +18,20 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate +from View.LateralContribution.translate import LCTranslate + _translate = QCoreApplication.translate -table_headers = { - "x": _translate("LateralContribution", "X"), - "y": _translate("LateralContribution", "Y"), - "time": _translate("LateralContribution", "Time"), - "date": _translate("LateralContribution", "Date"), - "discharge": _translate("LateralContribution", "Discharge (m³/s)"), - "z": _translate("LateralContribution", "Z (m)") -} +class LCETranslate(LCTranslate): + def __init__(self): + super(LCETranslate, self).__init__() -def retranslate(): - table_headers["x"] = _translate("LateralContribution", "X") - table_headers["y"] = _translate("LateralContribution", "Y") - table_headers["time"] = _translate("LateralContribution", "Time") - table_headers["date"] = _translate("LateralContribution", "Date") - table_headers["discharge"] = _translate("LateralContribution", "Discharge (m³/s)") - table_headers["z"] = _translate("LateralContribution", "Z (m)") + self._sub_dict["table_headers"] = { + "x": _translate("LateralContribution", "X"), + "y": _translate("LateralContribution", "Y"), + "time": _translate("LateralContribution", "Time"), + "date": _translate("LateralContribution", "Date"), + "discharge": _translate("LateralContribution", "Discharge (m³/s)"), + "z": _translate("LateralContribution", "Z (m)") + } diff --git a/src/View/LateralContribution/Table.py b/src/View/LateralContribution/Table.py index 2a3f4196..3c5f5cb7 100644 --- a/src/View/LateralContribution/Table.py +++ b/src/View/LateralContribution/Table.py @@ -46,22 +46,24 @@ from Model.LateralContribution.LateralContributionTypes import ( ) from View.Tools.PamhyrTable import PamhyrTableModel -from View.LateralContribution.translate import * +from View.LateralContribution.translate import LC_types logger = logging.getLogger() _translate = QCoreApplication.translate class ComboBoxDelegate(QItemDelegate): - def __init__(self, data=None, mode="type", tab="", parent=None): + def __init__(self, data=None, mode="type", tab="", trad = None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._data = data self._mode = mode self._tab = tab + self._trad = trad def createEditor(self, parent, option, index): self.editor = QComboBox(parent) + long_types = self._trad.get_dict("long_types") if self._mode == "type": lst = list( @@ -108,6 +110,7 @@ class TableModel(PamhyrTableModel): def _setup_lst(self): self._lst = self._data.lateral_contribution self._tab = self._opt_data + self._long_types = self._trad.get_dict("long_types") def rowCount(self, parent): return self._lst.len(self._tab) @@ -123,7 +126,7 @@ class TableModel(PamhyrTableModel): return self._lst.get(self._tab, row).name elif self._headers[column] == "type": t = self._lst.get(self._tab, row).lctype - return long_types[t] + return self._long_types[t] elif self._headers[column] == "edge": n = self._lst.get(self._tab, row).edge if n is None: @@ -151,7 +154,7 @@ class TableModel(PamhyrTableModel): ) ) elif self._headers[column] == "type": - key = next(k for k, v in long_types.items() if v == value) + key = next(k for k, v in self._long_types.items() if v == value) self._undo.push( SetTypeCommand( self._lst, self._tab, row, LC_types[key] diff --git a/src/View/LateralContribution/Window.py b/src/View/LateralContribution/Window.py index 2793ee36..467f2a4b 100644 --- a/src/View/LateralContribution/Window.py +++ b/src/View/LateralContribution/Window.py @@ -53,16 +53,13 @@ from View.LateralContribution.Table import ( TableModel, ComboBoxDelegate ) -from View.Plot.MplCanvas import MplCanvas +from View.Tools.Plot.PamhyrCanvas import MplCanvas from View.Geometry.PlotXY import PlotXY from View.LateralContribution.translate import ( - long_types, table_headers, LC_types, - retranslate, + LC_types, LCTranslate, ) from View.LateralContribution.Edit.Window import EditLateralContributionWindow -_translate = QCoreApplication.translate - logger = logging.getLogger() class LateralContributionWindow(PamhyrWindow): @@ -75,6 +72,7 @@ class LateralContributionWindow(PamhyrWindow): super(LateralContributionWindow, self).__init__( title = name, study = study, + trad = LCTranslate(), config = config, parent=parent ) @@ -86,7 +84,6 @@ class LateralContributionWindow(PamhyrWindow): self.setup_connections() def setup_table(self): - retranslate() self._table = {} for t in ["liquid", "solid", "suspenssion"]: @@ -94,19 +91,21 @@ class LateralContributionWindow(PamhyrWindow): data = self._study.river, mode = "type", tab = t, + trad = self._trad, parent=self ) self._delegate_edge = ComboBoxDelegate( data = self._study.river, mode = "edge", tab = t, + trad = self._trad, parent=self ) table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( table_view = table, - table_headers = table_headers, + table_headers = self._trad.get_dict("table_headers"), editable_headers = True, delegates = { "type": self._delegate_type, @@ -114,6 +113,7 @@ class LateralContributionWindow(PamhyrWindow): }, data = self._study.river, undo = self._undo_stack, + trad = self._trad, opt_data = t, ) table.setModel(self._table[t]) diff --git a/src/View/LateralContribution/translate.py b/src/View/LateralContribution/translate.py index cf430824..aaf44798 100644 --- a/src/View/LateralContribution/translate.py +++ b/src/View/LateralContribution/translate.py @@ -18,27 +18,14 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + from Model.LateralContribution.LateralContributionTypes import ( NotDefined, LateralContrib, Rain, Evaporation, ) _translate = QCoreApplication.translate -long_types = { - "ND": _translate("LateralContribution", "Not defined"), - "LC": _translate("LateralContribution", "Lateral contribution"), - "RA": _translate("LateralContribution", "Rain"), - "EV": _translate("LateralContribution", "Evaporation"), -} - -table_headers = { - "name": _translate("LateralContribution", "Name"), - "type": _translate("LateralContribution", "Type"), - "edge": _translate("LateralContribution", "Reach"), - "begin_kp": _translate("LateralContribution", "Begin kp (m)"), - "end_kp": _translate("LateralContribution", "End kp (m)") -} - LC_types = { "ND": NotDefined, "LC": LateralContrib, @@ -46,14 +33,21 @@ LC_types = { "EV": Evaporation, } -def retranslate(): - table_headers["name"] = _translate("LateralContribution", "Name") - table_headers["type"] = _translate("LateralContribution", "Type") - table_headers["edge"] = _translate("LateralContribution", "Reach") - table_headers["begin_kp"] = _translate("LateralContribution", "Begin kp (m)") - table_headers["end_kp"] = _translate("LateralContribution", "End kp (m)") +class LCTranslate(PamhyrTranslate): + def __init__(self): + super(LCTranslate, self).__init__() - long_types["ND"] = _translate("LateralContribution", "Not defined") - long_types["LC"] = _translate("LateralContribution", "Lateral contribution") - long_types["RA"] = _translate("LateralContribution", "Rain") - long_types["EV"] = _translate("LateralContribution", "Evaporation") + self._sub_dict["long_types"] = { + "ND": _translate("LateralContribution", "Not defined"), + "LC": _translate("LateralContribution", "Lateral contribution"), + "RA": _translate("LateralContribution", "Rain"), + "EV": _translate("LateralContribution", "Evaporation"), + } + + self._sub_dict["table_headers"] = { + "name": _translate("LateralContribution", "Name"), + "type": _translate("LateralContribution", "Type"), + "edge": _translate("LateralContribution", "Reach"), + "begin_kp": _translate("LateralContribution", "Begin kp (m)"), + "end_kp": _translate("LateralContribution", "End kp (m)") + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 266dc321..5ee7a382 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -46,7 +46,7 @@ from View.About.Window import AboutWindow from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow -# from View.LateralContribution.Window import LateralContributionWindow +from View.LateralContribution.Window import LateralContributionWindow # from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow # from View.Frictions.Window import FrictionsWindow diff --git a/src/View/Tools/PamhyrWidget.py b/src/View/Tools/PamhyrWidget.py index 8c85b1c1..f906beed 100644 --- a/src/View/Tools/PamhyrWidget.py +++ b/src/View/Tools/PamhyrWidget.py @@ -18,6 +18,8 @@ import logging +from datetime import datetime, timedelta + from PyQt5.QtCore import ( QModelIndex, QRect, QTime, QDateTime, ) From b430cbb62bf63254234dd5d9bb033ba0c57be41e Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 13:51:01 +0200 Subject: [PATCH 33/44] refactoring: refacto IC. --- src/View/InitialConditions/DialogDischarge.py | 2 +- src/View/InitialConditions/DialogHeight.py | 2 +- src/View/InitialConditions/PlotDKP.py | 15 ++--- src/View/InitialConditions/PlotDischarge.py | 21 ++----- src/View/InitialConditions/Table.py | 56 ++++++------------- src/View/InitialConditions/Window.py | 41 +++++++------- src/View/InitialConditions/translate.py | 35 ++++++------ src/View/MainWindow.py | 2 +- src/View/Tools/PamhyrTranslate.py | 2 + 9 files changed, 70 insertions(+), 106 deletions(-) diff --git a/src/View/InitialConditions/DialogDischarge.py b/src/View/InitialConditions/DialogDischarge.py index 7bbe8abd..7be75baf 100644 --- a/src/View/InitialConditions/DialogDischarge.py +++ b/src/View/InitialConditions/DialogDischarge.py @@ -37,7 +37,7 @@ class DischargeDialog(PamhyrDialog): def __init__(self, title="Discharge", parent=None): super(DischargeDialog, self).__init__( - title = _pamhyr_name, + title = self._pamhyr_name, options = [], parent = parent ) diff --git a/src/View/InitialConditions/DialogHeight.py b/src/View/InitialConditions/DialogHeight.py index 9ccadbd6..8a2a3c12 100644 --- a/src/View/InitialConditions/DialogHeight.py +++ b/src/View/InitialConditions/DialogHeight.py @@ -37,7 +37,7 @@ class HeightDialog(PamhyrDialog): def __init__(self, parent=None): super(HeightDialog, self).__init__( - title = name, + title = self._pamhyr_name, options = [], parent = parent ) diff --git a/src/View/InitialConditions/PlotDKP.py b/src/View/InitialConditions/PlotDKP.py index 58a83219..d604179a 100644 --- a/src/View/InitialConditions/PlotDKP.py +++ b/src/View/InitialConditions/PlotDKP.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,7 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotDKP(APlot): - def __init__(self, canvas=None, data=None, toolbar=None): - super(PlotDKP, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar - ) - +class PlotDKP(PamhyrPlot): @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -42,11 +35,11 @@ class PlotDKP(APlot): return self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Elevation (m)"), + self._trad["elevation"], color='green', fontsize=11 ) self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "KP (m)"), + self._trad["kp"], color='green', fontsize=11 ) diff --git a/src/View/InitialConditions/PlotDischarge.py b/src/View/InitialConditions/PlotDischarge.py index 1fe05def..1ad2a266 100644 --- a/src/View/InitialConditions/PlotDischarge.py +++ b/src/View/InitialConditions/PlotDischarge.py @@ -17,22 +17,9 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot - -from PyQt5.QtCore import ( - QCoreApplication -) - -_translate = QCoreApplication.translate - -class PlotDischarge(APlot): - def __init__(self, canvas=None, data=None, toolbar=None): - super(PlotDischarge, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar - ) +from View.Tools.PamhyrPlot import PamhyrPlot +class PlotDischarge(PamhyrPlot): @timer def draw(self, highlight=None): self.canvas.axes.cla() @@ -42,11 +29,11 @@ class PlotDischarge(APlot): return self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Discharge (m^3/s)"), + self._trad["discharge"], color='green', fontsize=11 ) self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "KP (m)"), + self._trad["kp"], color='green', fontsize=11 ) diff --git a/src/View/InitialConditions/Table.py b/src/View/InitialConditions/Table.py index 5e704ba9..f7ad1020 100644 --- a/src/View/InitialConditions/Table.py +++ b/src/View/InitialConditions/Table.py @@ -33,14 +33,14 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.InitialConditions.UndoCommand import ( SetCommand, AddCommand, DelCommand, SortCommand, MoveCommand, PasteCommand, DuplicateCommand, GenerateCommand, ) -from View.InitialConditions.translate import * - logger = logging.getLogger() _translate = QCoreApplication.translate @@ -87,29 +87,13 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, river=None, reach=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._river = river +class InitialConditionTableModel(PamhyrTableModel): + def __init__(self, reach=None, **kwargs): self._reach = reach - self._undo = undo - self._ics = self._river.initial_conditions.get(reach) + super(InitialConditionTableModel, self).__init__(**kwargs) - def flags(self, index): - column = index.column() - - options = Qt.ItemIsSelectable - if self._headers[column] != "speed": - options |= Qt.ItemIsEnabled | Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._ics) - - def columnCount(self, parent): - return len(self._headers) + def _setup_lst(self): + self._lst = self._data.river.initial_conditions.get(self._reach) def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -119,16 +103,10 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] not in ["name", "comment"]: - v = self._ics.get(row)[self._headers[column]] + v = self._lst.get(row)[self._headers[column]] return f"{v:.4f}" else: - return self._ics.get(row)[self._headers[column]] - - return QVariant() - - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[section]] + return self._lst.get(row)[self._headers[column]] return QVariant() @@ -143,7 +121,7 @@ class TableModel(QAbstractTableModel): if self._headers[column] is not None: self._undo.push( SetCommand( - self._ics, row, self._headers[column], value + self._lst, row, self._headers[column], value ) ) except Exception as e: @@ -158,7 +136,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._ics, row + self._lst, row ) ) @@ -170,7 +148,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._ics, rows + self._lst, rows ) ) @@ -182,7 +160,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SortCommand( - self._ics, False + self._lst, False ) ) @@ -199,7 +177,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._ics, "up", row + self._lst, "up", row ) ) @@ -207,7 +185,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._ics): + if row > len(self._lst): return target = row @@ -216,7 +194,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._ics, "down", row + self._lst, "down", row ) ) @@ -234,7 +212,7 @@ class TableModel(QAbstractTableModel): def generate(self, generator, param): self._undo.push( GenerateCommand( - self._ics, generator, param + self._lst, generator, param ) ) self.layoutChanged.emit() diff --git a/src/View/InitialConditions/Window.py b/src/View/InitialConditions/Window.py index 3f5ee1d8..90cf8e08 100644 --- a/src/View/InitialConditions/Window.py +++ b/src/View/InitialConditions/Window.py @@ -45,14 +45,14 @@ from View.InitialConditions.UndoCommand import ( DuplicateCommand, ) -from View.InitialConditions.Table import TableModel, ComboBoxDelegate +from View.InitialConditions.Table import InitialConditionTableModel, ComboBoxDelegate -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.InitialConditions.PlotDKP import PlotDKP from View.InitialConditions.PlotDischarge import PlotDischarge -from View.InitialConditions.translate import table_headers, retranslate +from View.InitialConditions.translate import ICTranslate from View.InitialConditions.DialogHeight import HeightDialog from View.InitialConditions.DialogDischarge import DischargeDialog @@ -80,43 +80,42 @@ class InitialConditionsWindow(PamhyrWindow): title = name, study = study, config = config, + trad = ICTranslate(), parent = parent ) self._ics = study.river.initial_conditions.get(self._reach) self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_connections() self.ui.setWindowTitle(self._title) def setup_table(self): - retranslate() - table = self.find(QTableView, f"tableView") - self._table = TableModel( - river = self._study.river, - reach = self._reach, - undo = self._undo_stack, - ) - table.setModel(self._table) - self._delegate_kp = ComboBoxDelegate( reach = self._reach, - parent=self + parent = self ) - table.setItemDelegateForColumn( - list(table_headers).index("kp"), - self._delegate_kp + self._table = InitialConditionTableModel( + reach = self._reach, + table_view = table, + table_headers = self._trad.get_dict("table_headers"), + editable_headers = ["kp", "discharge", "elevation", "height"], + delegates = {"kp": self._delegate_kp}, + data = self._study, + undo = self._undo_stack, + trad = self._trad ) + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) - def setup_graph(self): + def setup_plot(self): self.canvas_1 = MplCanvas(width=5, height=4, dpi=100) self.canvas_1.setObjectName("canvas_1") self.toolbar_1 = PamhyrPlotToolbar( @@ -129,7 +128,9 @@ class InitialConditionsWindow(PamhyrWindow): self.plot_1 = PlotDKP( canvas = self.canvas_1, data = self._ics, + trad = self._trad, toolbar = self.toolbar_1, + parent = self ) self.plot_1.draw() @@ -145,7 +146,9 @@ class InitialConditionsWindow(PamhyrWindow): self.plot_2 = PlotDischarge( canvas = self.canvas_2, data = self._ics, + trad = self._trad, toolbar = self.toolbar_2, + parent = self ) self.plot_2.draw() diff --git a/src/View/InitialConditions/translate.py b/src/View/InitialConditions/translate.py index 54f194e9..f65306cb 100644 --- a/src/View/InitialConditions/translate.py +++ b/src/View/InitialConditions/translate.py @@ -18,23 +18,24 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = { - # "name": _translate("LateralContribution", "Name"), - "kp": _translate("LateralContribution", "KP (m)"), - # "speed": _translate("LateralContribution", "Speed (m/s)"), - "discharge": _translate("LateralContribution", "Discharge (m³/s)"), - "elevation": _translate("LateralContribution", "Elevation (m)"), - "height": _translate("LateralContribution", "Height (m)"), - # "comment": _translate("LateralContribution", "Comment"), -} +class ICTranslate(PamhyrTranslate): + def __init__(self): + super(ICTranslate, self).__init__() -def retranslate(): - # table_headers["name"] = _translate("LateralContribution", "Name") - table_headers["kp"] = _translate("LateralContribution", "KP (m)") - # table_headers["speed"] = _translate("LateralContribution", "Speed (m/s)") - table_headers["discharge"] = _translate("LateralContribution", "Discharge (m³/s)") - table_headers["elevation"] = _translate("LateralContribution", "Elevation (m)") - table_headers["height"] = _translate("LateralContribution", "Height (m)") - # table_headers["comment"] = _translate("LateralContribution", "Comment") + self._dict["elevation"] = _translate("InitialCondition", "Elevation (m)") + self._dict["discharge"] = _translate("InitialCondition", "Discharge (m³/s)") + self._dict["kp"] = _translate("InitialCondition", "KP (m)") + + self._sub_dict["table_headers"] = { + # "name": _translate("InitialCondition", "Name"), + "kp": _translate("InitialCondition", "KP (m)"), + # "speed": _translate("InitialCondition", "Speed (m/s)"), + "discharge": _translate("InitialCondition", "Discharge (m³/s)"), + "elevation": _translate("InitialCondition", "Elevation (m)"), + "height": _translate("InitialCondition", "Height (m)"), + # "comment": _translate("InitialCondition", "Comment"), + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 5ee7a382..c3ec403c 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -47,7 +47,7 @@ from View.Network.Window import NetworkWindow from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.LateralContribution.Window import LateralContributionWindow -# from View.InitialConditions.Window import InitialConditionsWindow +from View.InitialConditions.Window import InitialConditionsWindow # from View.Stricklers.Window import StricklersWindow # from View.Frictions.Window import FrictionsWindow # from View.SedimentLayers.Window import SedimentLayersWindow diff --git a/src/View/Tools/PamhyrTranslate.py b/src/View/Tools/PamhyrTranslate.py index 83c8da64..67befa23 100644 --- a/src/View/Tools/PamhyrTranslate.py +++ b/src/View/Tools/PamhyrTranslate.py @@ -28,6 +28,8 @@ class PamhyrTranslate(object): # Module sub dictionnary self._sub_dict = {} + self._dict["pamhyr"] = _translate("Pamhyr", "Pamhyr2") + def __getitem__(self, index): if index not in self._dict: return None From 4ce597a2a07031ef9135e48bbe57fae34b8fac69 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 14:34:07 +0200 Subject: [PATCH 34/44] refactoring: refacto frictions & stricklers. --- src/View/Frictions/PlotStricklers.py | 11 +---- src/View/Frictions/Table.py | 64 ++++++++++------------------ src/View/Frictions/Window.py | 38 ++++++++--------- src/View/Frictions/translate.py | 28 ++++++------ src/View/MainWindow.py | 4 +- src/View/Stricklers/Window.py | 6 +-- src/View/Stricklers/translate.py | 25 +++++------ 7 files changed, 72 insertions(+), 104 deletions(-) diff --git a/src/View/Frictions/PlotStricklers.py b/src/View/Frictions/PlotStricklers.py index 71ff6e6f..c6b58dcd 100644 --- a/src/View/Frictions/PlotStricklers.py +++ b/src/View/Frictions/PlotStricklers.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer, flatten -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,7 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotStricklers(APlot): - def __init__(self, canvas=None, data=None, toolbar=None): - super(PlotStricklers, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar - ) - +class PlotStricklers(PamhyrPlot): def draw_frictions(self, frictions, color="r"): lst = frictions lst.sort(key = lambda s: s.begin_kp) diff --git a/src/View/Frictions/Table.py b/src/View/Frictions/Table.py index ef9dc32f..f4ad4f8e 100644 --- a/src/View/Frictions/Table.py +++ b/src/View/Frictions/Table.py @@ -41,6 +41,8 @@ from View.Frictions.UndoCommand import ( MoveCommand, PasteCommand, DuplicateCommand, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.Frictions.translate import * logger = logging.getLogger() @@ -93,26 +95,10 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, data=None, study=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._data = data - self._study = study - self._undo = undo - self._frictions = self._data.frictions - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._frictions) - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data.frictions + self._study = self._opt_data def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -122,30 +108,24 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._frictions.get(row).name + return self._lst.get(row).name elif self._headers[column] == "begin_kp": - return self._frictions.get(row).begin_kp + return self._lst.get(row).begin_kp elif self._headers[column] == "end_kp": - return self._frictions.get(row).end_kp + return self._lst.get(row).end_kp elif self._headers[column] == "begin_strickler": - value = self._frictions.get(row).begin_strickler + value = self._lst.get(row).begin_strickler if value == None: return _translate("Frictions", "Not defined") return str(value) elif self._headers[column] == "end_strickler": - value = self._frictions.get(row).end_strickler + value = self._lst.get(row).end_strickler if value == None: return _translate("Frictions", "Not defined") return str(value) return QVariant() - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False @@ -157,31 +137,31 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._frictions, row, value + self._lst, row, value ) ) elif self._headers[column] == "begin_kp": self._undo.push( SetBeginCommand( - self._frictions, row, value + self._lst, row, value ) ) elif self._headers[column] == "end_kp": self._undo.push( SetEndCommand( - self._frictions, row, value + self._lst, row, value ) ) elif self._headers[column] == "begin_strickler": self._undo.push( SetBeginStricklerCommand( - self._frictions, row, self._study.river.strickler(value) + self._lst, row, self._study.river.strickler(value) ) ) elif self._headers[column] == "end_strickler": self._undo.push( SetEndStricklerCommand( - self._frictions, row, self._study.river.strickler(value) + self._lst, row, self._study.river.strickler(value) ) ) except Exception as e: @@ -196,7 +176,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._frictions, row, self._data + self._lst, row, self._data ) ) @@ -208,7 +188,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._frictions, rows + self._lst, rows ) ) @@ -220,7 +200,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SortCommand( - self._frictions, False + self._lst, False ) ) @@ -237,7 +217,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._frictions, "up", row + self._lst, "up", row ) ) @@ -245,7 +225,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._frictions): + if row > len(self._lst): return target = row @@ -254,7 +234,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._frictions, "down", row + self._lst, "down", row ) ) diff --git a/src/View/Frictions/Window.py b/src/View/Frictions/Window.py index 2a547528..74d96b63 100644 --- a/src/View/Frictions/Window.py +++ b/src/View/Frictions/Window.py @@ -47,15 +47,13 @@ from View.Frictions.Table import ( TableModel, ComboBoxDelegate ) -from View.Plot.MplCanvas import MplCanvas +from View.Tools.Plot.PamhyrCanvas import MplCanvas from View.Geometry.PlotKPZ import PlotKPZ from View.Frictions.PlotStricklers import PlotStricklers -from View.Frictions.translate import table_headers, retranslate +from View.Frictions.translate import FrictionsTranslate from View.Stricklers.Window import StricklersWindow -_translate = QCoreApplication.translate - logger = logging.getLogger() class FrictionsWindow(PamhyrWindow): @@ -80,6 +78,7 @@ class FrictionsWindow(PamhyrWindow): title = name, study = study, config = config, + trad = FrictionsTranslate(), parent = parent ) @@ -89,17 +88,8 @@ class FrictionsWindow(PamhyrWindow): def setup_table(self): - retranslate() self._table = {} - table = self.find(QTableView, f"tableView") - self._table = TableModel( - data = self._reach, - study = self._study, - undo = self._undo_stack, - ) - table.setModel(self._table) - self._delegate_stricklers = ComboBoxDelegate( data = self._reach, study = self._study, @@ -107,13 +97,23 @@ class FrictionsWindow(PamhyrWindow): parent=self ) - table.setItemDelegateForColumn( - 3, self._delegate_stricklers + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view = table, + table_headers = self._trad.get_dict("table_headers"), + editable_headers = [ + "name", "begin_kp", "end_kp", + "begin_strickler", "end_strickler" + ], + delegates = { + "begin_strickler": self._delegate_stricklers, + "end_strickler": self._delegate_stricklers, + }, + data = self._reach, + undo = self._undo_stack, + opt_data = self._study ) - table.setItemDelegateForColumn( - 4, self._delegate_stricklers - ) - + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) diff --git a/src/View/Frictions/translate.py b/src/View/Frictions/translate.py index 68bebb96..1014d8c1 100644 --- a/src/View/Frictions/translate.py +++ b/src/View/Frictions/translate.py @@ -18,21 +18,19 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("Frictions", "Name"), - # "edge": _translate("Frictions", "Reach"), - "begin_kp": _translate("Frictions", "Begin kp (m)"), - "end_kp": _translate("Frictions", "End kp (m)"), - "begin_strickler": _translate("Frictions", "Begin strickler"), - "end_strickler": _translate("Frictions", "End strickler"), -} +class FrictionsTranslate(PamhyrTranslate): + def __init__(self): + super(FrictionsTranslate, self).__init__() -def retranslate(): - table_headers["name"] = _translate("Frictions", "Name") - # table_headers["edge"] = _translate("Frictions", "Reach") - table_headers["begin_kp"] = _translate("Frictions", "Begin kp (m)") - table_headers["end_kp"] = _translate("Frictions", "End kp (m)") - table_headers["begin_strickler"] = _translate("Frictions", "Begin strickler") - table_headers["end_strickler"] = _translate("Frictions", "End strickler") + self._sub_dict["table_headers"] = { + "name": _translate("Frictions", "Name"), + # "edge": _translate("Frictions", "Reach"), + "begin_kp": _translate("Frictions", "Begin kp (m)"), + "end_kp": _translate("Frictions", "End kp (m)"), + "begin_strickler": _translate("Frictions", "Begin strickler"), + "end_strickler": _translate("Frictions", "End strickler"), + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index c3ec403c..9df2d45e 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -48,8 +48,8 @@ from View.Geometry.Window import GeometryWindow from View.BoundaryCondition.Window import BoundaryConditionWindow from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow -# from View.Stricklers.Window import StricklersWindow -# from View.Frictions.Window import FrictionsWindow +from View.Stricklers.Window import StricklersWindow +from View.Frictions.Window import FrictionsWindow # from View.SedimentLayers.Window import SedimentLayersWindow # from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow diff --git a/src/View/Stricklers/Window.py b/src/View/Stricklers/Window.py index d9128d00..792ed4e3 100644 --- a/src/View/Stricklers/Window.py +++ b/src/View/Stricklers/Window.py @@ -43,7 +43,7 @@ from Model.Stricklers.Stricklers import Stricklers from View.Stricklers.UndoCommand import PasteCommand from View.Stricklers.Table import TableModel -from View.Stricklers.translate import table_headers, retranslate +from View.Stricklers.translate import StricklersTranslate _translate = QCoreApplication.translate @@ -60,6 +60,7 @@ class StricklersWindow(PamhyrWindow): title = name, study = study, config = config, + trad = StricklersTranslate(), parent = parent ) @@ -67,7 +68,6 @@ class StricklersWindow(PamhyrWindow): self.setup_connections() def setup_table(self): - retranslate() self._table = {} for t in ["app", "study"]: @@ -79,7 +79,7 @@ class StricklersWindow(PamhyrWindow): self._table[t] = TableModel( table_view = table, - table_headers = table_headers, + table_headers = self._trad.get_dict("table_headers"), editable_headers = ["name", "comment", "minor", "medium"], data = data, undo = self._undo_stack, diff --git a/src/View/Stricklers/translate.py b/src/View/Stricklers/translate.py index 9102ac6d..489094cf 100644 --- a/src/View/Stricklers/translate.py +++ b/src/View/Stricklers/translate.py @@ -18,21 +18,18 @@ from PyQt5.QtCore import QCoreApplication -from Model.LateralContribution.LateralContributionTypes import ( - NotDefined, LateralContrib, Rain, Evaporation, -) +from View.Tools.PamhyrTranslate import PamhyrTranslate _translate = QCoreApplication.translate -table_headers = { - "name": _translate("LateralContribution", "Name"), - "minor": _translate("LateralContribution", "Minor bed"), - "medium": _translate("LateralContribution", "Medium bed"), - "comment": _translate("LateralContribution", "Comment"), -} -def retranslate(): - table_headers["name"] = _translate("LateralContribution", "Name") - table_headers["minor"] = _translate("LateralContribution", "Minor bed") - table_headers["medium"] = _translate("LateralContribution", "Medium bed") - table_headers["comment"] = _translate("LateralContribution", "Comment") +class StricklersTranslate(PamhyrTranslate): + def __init__(self): + super(StricklersTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("LateralContribution", "Name"), + "minor": _translate("LateralContribution", "Minor bed"), + "medium": _translate("LateralContribution", "Medium bed"), + "comment": _translate("LateralContribution", "Comment"), + } From 1d45ccb8b80e091be5454614db36b60f761a54ab Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 15:19:27 +0200 Subject: [PATCH 35/44] refactoring: refacto SL list & edit. --- src/View/MainWindow.py | 2 +- src/View/SedimentLayers/Edit/Plot.py | 28 ++++------ src/View/SedimentLayers/Edit/Table.py | 64 +++++++---------------- src/View/SedimentLayers/Edit/Window.py | 22 ++++---- src/View/SedimentLayers/Edit/translate.py | 30 +++++------ src/View/SedimentLayers/Table.py | 50 +++++------------- src/View/SedimentLayers/Window.py | 28 +++++----- src/View/SedimentLayers/translate.py | 16 +++--- 8 files changed, 91 insertions(+), 149 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 9df2d45e..20ab32e2 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -50,7 +50,7 @@ from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow -# from View.SedimentLayers.Window import SedimentLayersWindow +from View.SedimentLayers.Window import SedimentLayersWindow # from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow # from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow diff --git a/src/View/SedimentLayers/Edit/Plot.py b/src/View/SedimentLayers/Edit/Plot.py index e48ec5eb..a6fa2185 100644 --- a/src/View/SedimentLayers/Edit/Plot.py +++ b/src/View/SedimentLayers/Edit/Plot.py @@ -5,23 +5,19 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot - -from PyQt5.QtCore import ( - QCoreApplication -) - -_translate = QCoreApplication.translate +from View.Tools.PamhyrPlot import PamhyrPlot logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): +class Plot(PamhyrPlot): + def __init__(self, canvas=None, trad=None, data=None, toolbar=None, + display_current=True, parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self._display_current = display_current @@ -34,13 +30,9 @@ class Plot(APlot): self.canvas.axes.cla() self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) - # self.canvas.axes.set_xlabel( - # _translate("MainWindow_reach", "X (m)"), - # color='green', fontsize=12 - # ) self.canvas.axes.axes.get_xaxis().set_visible(False) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Height (m)"), + self._trad["height"], color='green', fontsize=12 ) diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py index e1a41cb2..ea7cd934 100644 --- a/src/View/SedimentLayers/Edit/Table.py +++ b/src/View/SedimentLayers/Edit/Table.py @@ -15,32 +15,14 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.SedimentLayers.Edit.UndoCommand import * -from View.SedimentLayers.Edit.translate import * _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, study=None, sl=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._sl = sl - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._sl) - - def columnCount(self, parent): - return len(self._headers) - +class TableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: return QVariant() @@ -49,23 +31,17 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._sl.get(row).name + return self._data.get(row).name elif self._headers[column] == "type": - return self._sl.get(row).type + return self._data.get(row).type elif self._headers[column] == "height": - return self._sl.get(row).height + return self._data.get(row).height elif self._headers[column] == "d50": - return self._sl.get(row).d50 + return self._data.get(row).d50 elif self._headers[column] == "sigma": - return self._sl.get(row).sigma + return self._data.get(row).sigma elif self._headers[column] == "critical_constraint": - return self._sl.get(row).critical_constraint - - return QVariant() - - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] + return self._data.get(row).critical_constraint return QVariant() @@ -79,37 +55,37 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "type": self._undo.push( SetTypeCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "height": self._undo.push( SetHeightCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "d50": self._undo.push( SetD50Command( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "sigma": self._undo.push( SetSigmaCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "critical_constraint": self._undo.push( SetCriticalConstraintCommand( - self._sl, row, value + self._data, row, value ) ) @@ -121,7 +97,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._sl, row + self._data, row ) ) @@ -133,7 +109,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._sl, rows + self._data, rows ) ) @@ -150,7 +126,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._sl, "up", row + self._data, "up", row ) ) @@ -158,7 +134,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, row, parent=QModelIndex()): - if row + 1 >= len(self._sl): + if row + 1 >= len(self._data): return target = row @@ -167,7 +143,7 @@ class TableModel(QAbstractTableModel): self._undo.push( MoveCommand( - self._sl, "down", row + self._data, "down", row ) ) diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py index ebe3aece..e5835c97 100644 --- a/src/View/SedimentLayers/Edit/Window.py +++ b/src/View/SedimentLayers/Edit/Window.py @@ -39,13 +39,13 @@ from PyQt5.QtWidgets import ( QComboBox, QVBoxLayout, QHeaderView, QTabWidget, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.SedimentLayers.Edit.UndoCommand import * from View.SedimentLayers.Edit.Table import * from View.SedimentLayers.Edit.Plot import Plot -from View.SedimentLayers.Edit.translate import table_headers, retranslate +from View.SedimentLayers.Edit.translate import SedimentEditTranslate _translate = QCoreApplication.translate @@ -71,7 +71,8 @@ class EditSedimentLayersWindow(PamhyrWindow): title = name, study = study, config = config, - parent=parent + trad = SedimentEditTranslate(), + parent = parent ) self.setup_table() @@ -79,17 +80,17 @@ class EditSedimentLayersWindow(PamhyrWindow): self.setup_connections() def setup_table(self): - retranslate() - self._table = {} - + table_headers = self._trad.get_dict("table_headers") table = self.find(QTableView, f"tableView") self._table = TableModel( - study = self._study, - sl = self._sl, + table_view = table, + table_headers = table_headers, + editable_headers = list(table_headers), + data = self._sl, + trad = self._trad, undo = self._undo_stack, ) table.setModel(self._table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) @@ -110,6 +111,7 @@ class EditSedimentLayersWindow(PamhyrWindow): canvas = self.canvas, data = self._sl, toolbar = self.toolbar, + trad = self._trad, display_current = False ) self.plot.draw() diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py index a6b77897..658d4f23 100644 --- a/src/View/SedimentLayers/Edit/translate.py +++ b/src/View/SedimentLayers/Edit/translate.py @@ -2,21 +2,21 @@ from PyQt5.QtCore import QCoreApplication +from View.SedimentLayers.translate import SedimentTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("SedimentLayers", "Name"), - # "type": _translate("SedimentLayers", "Type"), - "height": _translate("SedimentLayers", "Height"), - "d50": _translate("SedimentLayers", "D50"), - "sigma": _translate("SedimentLayers", "Sigma"), - "critical_constraint": _translate("SedimentLayers", "Critical constraint"), -} +class SedimentEditTranslate(SedimentTranslate): + def __init__(self): + super(SedimentEditTranslate, self).__init__() -def retranslate(): - table_headers["name"] = _translate("SedimentLayers", "Name") - # table_headers["type"] = _translate("SedimentLayers", "Type") - table_headers["height"] = _translate("SedimentLayers", "Height") - table_headers["d50"] = _translate("SedimentLayers", "D50") - table_headers["sigma"] = _translate("SedimentLayers", "Sigma") - table_headers["critical_constraint"] = _translate("SedimentLayers", "Critical constraint") + self._dict["height"] = _translate("SedimentLayers", "Height (m)"), + + self._sub_dict["table_headers"] = { + "name": _translate("SedimentLayers", "Name"), + # "type": _translate("SedimentLayers", "Type"), + "height": _translate("SedimentLayers", "Height"), + "d50": _translate("SedimentLayers", "D50"), + "sigma": _translate("SedimentLayers", "Sigma"), + "critical_constraint": _translate("SedimentLayers", "Critical constraint"), + } diff --git a/src/View/SedimentLayers/Table.py b/src/View/SedimentLayers/Table.py index f375c318..2b7a00f6 100644 --- a/src/View/SedimentLayers/Table.py +++ b/src/View/SedimentLayers/Table.py @@ -15,32 +15,12 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.SedimentLayers.UndoCommand import * -from View.SedimentLayers.translate import * - -_translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, study=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._sl = self._study.river.sediment_layers - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._sl) - - def columnCount(self, parent): - return len(self._headers) - +class TableModel(PamhyrTableModel): def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: return QVariant() @@ -49,15 +29,9 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._sl.get(row).name + return self._data.get(row).name elif self._headers[column] == "comment": - return self._sl.get(row).comment - - return QVariant() - - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] + return self._data.get(row).comment return QVariant() @@ -71,13 +45,13 @@ class TableModel(QAbstractTableModel): if self._headers[column] == "name": self._undo.push( SetNameCommand( - self._sl, row, value + self._data, row, value ) ) if self._headers[column] == "comment": self._undo.push( SetCommentCommand( - self._sl, row, value + self._data, row, value ) ) @@ -89,7 +63,7 @@ class TableModel(QAbstractTableModel): self._undo.push( AddCommand( - self._sl, row + self._data, row ) ) @@ -101,7 +75,7 @@ class TableModel(QAbstractTableModel): self._undo.push( DelCommand( - self._sl, rows + self._data, rows ) ) @@ -118,7 +92,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._sl, "up", row + self._data, "up", row ) ) @@ -126,7 +100,7 @@ class TableModel(QAbstractTableModel): self.layoutChanged.emit() def move_down(self, index, parent=QModelIndex()): - if row > len(self._sl): + if row > len(self._data): return target = row @@ -135,7 +109,7 @@ class TableModel(QAbstractTableModel): self._undo_stack.push( MoveCommand( - self._sl, "down", row + self._data, "down", row ) ) diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py index c4010745..3c394de0 100644 --- a/src/View/SedimentLayers/Window.py +++ b/src/View/SedimentLayers/Window.py @@ -44,13 +44,11 @@ from View.SedimentLayers.Table import * from View.SedimentLayers.Edit.Plot import Plot -from View.Plot.MplCanvas import MplCanvas -from View.SedimentLayers.translate import table_headers, retranslate +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.SedimentLayers.translate import SedimentTranslate from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow -_translate = QCoreApplication.translate - logger = logging.getLogger() class SedimentLayersWindow(PamhyrWindow): @@ -68,34 +66,31 @@ class SedimentLayersWindow(PamhyrWindow): title = name, study = study, config = config, + trad = SedimentTranslate(), parent=parent ) self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_connections() - def setup_title(self, title): - self._title = ( - title + " - " + self._study.name - ) - def setup_table(self): - retranslate() - table = self.find(QTableView, f"tableView") self._table = TableModel( - study = self._study, + table_view = table, + table_headers = self._trad.get_dict("table_headers"), + editable_headers = ["name", "comment"], + data = self._study.river.sediment_layers, + trad = self._trad, undo = self._undo_stack, ) table.setModel(self._table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) - def setup_graph(self): - self.canvas = MplCanvas(width=5, height=4, dpi=100) + def setup_plot(self): + self.canvas = MplCanvas(width=2, height=4, dpi=100) self.canvas.setObjectName("canvas") self.plot_layout = self.find(QVBoxLayout, "verticalLayout") self.plot_layout.addWidget(self.canvas) @@ -122,6 +117,7 @@ class SedimentLayersWindow(PamhyrWindow): self.plot = Plot( canvas = self.canvas, data = self._sediment_layers.get(rows[0]), + trad = self._trad, toolbar = None, display_current = False ) diff --git a/src/View/SedimentLayers/translate.py b/src/View/SedimentLayers/translate.py index 29053e17..42d59f92 100644 --- a/src/View/SedimentLayers/translate.py +++ b/src/View/SedimentLayers/translate.py @@ -2,13 +2,15 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("SedimentLayers", "Name"), - "comment": _translate("SedimentLayers", "Comment"), -} +class SedimentTranslate(PamhyrTranslate): + def __init__(self): + super(SedimentTranslate, self).__init__() -def retranslate(): - table_headers["name"] = _translate("SedimentLayers", "Name") - table_headers["comment"] = _translate("SedimentLayers", "Comment") + self._sub_dict["table_headers"] = { + "name": _translate("SedimentLayers", "Name"), + "comment": _translate("SedimentLayers", "Comment"), + } From 63d6c114c0f36f6a41fee6f70b7407e4f23a9c68 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 16:16:37 +0200 Subject: [PATCH 36/44] refactoring: refacto SL reach and profile window. --- src/View/MainWindow.py | 2 +- src/View/SedimentLayers/Reach/Plot.py | 21 ++++---- src/View/SedimentLayers/Reach/Profile/Plot.py | 17 ++++--- .../SedimentLayers/Reach/Profile/Table.py | 46 +++++------------ .../SedimentLayers/Reach/Profile/Window.py | 36 ++++++------- .../SedimentLayers/Reach/Profile/translate.py | 25 +++++----- src/View/SedimentLayers/Reach/Table.py | 50 ++++++------------- src/View/SedimentLayers/Reach/Window.py | 40 ++++++++------- src/View/SedimentLayers/Reach/translate.py | 23 +++++---- 9 files changed, 114 insertions(+), 146 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 20ab32e2..9fee3a76 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -51,7 +51,7 @@ from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow from View.SedimentLayers.Window import SedimentLayersWindow -# from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow +from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow # from View.SolverParameters.Window import SolverParametersWindow # from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow # from View.CheckList.Window import CheckListWindow diff --git a/src/View/SedimentLayers/Reach/Plot.py b/src/View/SedimentLayers/Reach/Plot.py index a312a8ce..069a09d7 100644 --- a/src/View/SedimentLayers/Reach/Plot.py +++ b/src/View/SedimentLayers/Reach/Plot.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,13 +15,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): +class Plot(PamhyrPlot): + def __init__(self, data=None, display_current=True, + canvas=None, trad=None, toolbar=None, + parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self._display_current = display_current @@ -41,11 +44,11 @@ class Plot(APlot): return self.canvas.axes.set_xlabel( - _translate("MainWindow_reach", "Kp (m)"), + self._trad["kp"], color='green', fontsize=12 ) self.canvas.axes.set_ylabel( - _translate("MainWindow_reach", "Height (m)"), + self._trad["height"], color='green', fontsize=12 ) diff --git a/src/View/SedimentLayers/Reach/Profile/Plot.py b/src/View/SedimentLayers/Reach/Profile/Plot.py index 50b4bba2..01b2fe24 100644 --- a/src/View/SedimentLayers/Reach/Profile/Plot.py +++ b/src/View/SedimentLayers/Reach/Profile/Plot.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,13 +15,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class Plot(APlot): - def __init__(self, canvas=None, data=None, toolbar=None, - display_current=True): +class Plot(PamhyrPlot): + def __init__(self, data=None, display_current=True, + canvas=None, trad=None, toolbar=None, + parent=None): super(Plot, self).__init__( - canvas=canvas, - data=data, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = data, + toolbar = toolbar, + parent = parent ) self._display_current = display_current diff --git a/src/View/SedimentLayers/Reach/Profile/Table.py b/src/View/SedimentLayers/Reach/Profile/Table.py index 102bec32..3550940b 100644 --- a/src/View/SedimentLayers/Reach/Profile/Table.py +++ b/src/View/SedimentLayers/Reach/Profile/Table.py @@ -20,6 +20,8 @@ from PyQt5.QtWidgets import ( from View.SedimentLayers.Reach.Profile.UndoCommand import * from View.SedimentLayers.Reach.Profile.translate import * +from View.Tools.PamhyrTable import PamhyrTableModel + _translate = QCoreApplication.translate logger = logging.getLogger() @@ -67,28 +69,10 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, study=None, profile=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._profile = profile - - def flags(self, index): - column = index.column() - - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if self._headers[column] == "sl": - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return self._profile.number_points - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data + self._study = self._opt_data def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -98,25 +82,19 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._profile.point(row).name + return self._data.point(row).name elif self._headers[column] == "sl": - value = self._profile.point(row).sl + value = self._data.point(row).sl if value == None: text = _translate("SedimentLayers", "Not defined") return text return str(value) elif self._headers[column] == "x": - return self._profile.point(row).x + return self._data.point(row).x elif self._headers[column] == "y": - return self._profile.point(row).y + return self._data.point(row).y elif self._headers[column] == "z": - return self._profile.point(row).z - - return QVariant() - - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] + return self._data.point(row).z return QVariant() @@ -139,7 +117,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SetSLCommand( - self._profile, row, new + self._data, row, new ) ) diff --git a/src/View/SedimentLayers/Reach/Profile/Window.py b/src/View/SedimentLayers/Reach/Profile/Window.py index f424e06a..e2c2464b 100644 --- a/src/View/SedimentLayers/Reach/Profile/Window.py +++ b/src/View/SedimentLayers/Reach/Profile/Window.py @@ -27,10 +27,10 @@ from View.SedimentLayers.Reach.Profile.UndoCommand import * from View.SedimentLayers.Reach.Profile.Table import * from View.SedimentLayers.Reach.Profile.Plot import Plot -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.SedimentLayers.Reach.Profile.translate import table_headers, retranslate +from View.SedimentLayers.Reach.Profile.translate import SedimentProfileTranslate from View.SedimentLayers.Window import SedimentLayersWindow _translate = QCoreApplication.translate @@ -52,7 +52,8 @@ class ProfileSedimentLayersWindow(PamhyrWindow): title = name, study = study, config = config, - parent=parent + trad = SedimentProfileTranslate(), + parent = parent ) self.setup_table() @@ -80,26 +81,25 @@ class ProfileSedimentLayersWindow(PamhyrWindow): ) def setup_table(self): - retranslate() + table_headers = self._trad.get_dict("table_headers") - table = self.find(QTableView, f"tableView") - self._table = TableModel( - study = self._study, - profile = self._profile, - undo = self._undo_stack, - ) - table.setModel(self._table) - - self._delegate_stricklers = ComboBoxDelegate( + self._delegate_sl = ComboBoxDelegate( study = self._study, parent=self ) - table.setItemDelegateForColumn( - list(table_headers).index("sl"), - self._delegate_stricklers + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view = table, + data = self._profile, + opt_data = self._study, + table_headers = table_headers, + editable_headers = ["sl"], + delegates = {"sl": self._delegate_sl}, + trad = self._trad, + undo = self._undo_stack, ) - + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) diff --git a/src/View/SedimentLayers/Reach/Profile/translate.py b/src/View/SedimentLayers/Reach/Profile/translate.py index da256cbe..b1c12405 100644 --- a/src/View/SedimentLayers/Reach/Profile/translate.py +++ b/src/View/SedimentLayers/Reach/Profile/translate.py @@ -2,19 +2,18 @@ from PyQt5.QtCore import QCoreApplication +from View.SedimentLayers.Reach.translate import SedimentReachTranslate + _translate = QCoreApplication.translate -table_headers = { - "x": _translate("SedimentLayers", "X (m)"), - "y": _translate("SedimentLayers", "Y (m)"), - "z": _translate("SedimentLayers", "Z (m)"), - "name": _translate("SedimentLayers", "Name"), - "sl": _translate("SedimentLayers", "Sediment layers"), -} +class SedimentProfileTranslate(SedimentReachTranslate): + def __init__(self): + super(SedimentProfileTranslate, self).__init__() -def retranslate(): - table_headers["x"] = _translate("SedimentLayers", "X (m)") - table_headers["y"] = _translate("SedimentLayers", "Y (m)") - table_headers["z"] = _translate("SedimentLayers", "Z (m)") - table_headers["name"] = _translate("SedimentLayers", "Name") - table_headers["sl"] = _translate("SedimentLayers", "Sediment layers") + self._sub_dict["table_headers"] = { + "x": _translate("SedimentLayers", "X (m)"), + "y": _translate("SedimentLayers", "Y (m)"), + "z": _translate("SedimentLayers", "Z (m)"), + "name": _translate("SedimentLayers", "Name"), + "sl": _translate("SedimentLayers", "Sediment layers"), + } diff --git a/src/View/SedimentLayers/Reach/Table.py b/src/View/SedimentLayers/Reach/Table.py index 8fdb8d7e..250a4735 100644 --- a/src/View/SedimentLayers/Reach/Table.py +++ b/src/View/SedimentLayers/Reach/Table.py @@ -18,23 +18,25 @@ from PyQt5.QtWidgets import ( ) from View.SedimentLayers.Reach.UndoCommand import * -from View.SedimentLayers.Reach.translate import * + +from View.Tools.PamhyrTable import PamhyrTableModel _translate = QCoreApplication.translate logger = logging.getLogger() class ComboBoxDelegate(QItemDelegate): - def __init__(self, study=None, parent=None): + def __init__(self, study=None, trad=None, parent=None): super(ComboBoxDelegate, self).__init__(parent) self._study = study + self._trad = trad def createEditor(self, parent, option, index): self.editor = QComboBox(parent) self.editor.addItems( - [_translate("SedimentLayers", "Not defined")] + + [self._trad["nd"]] + list( map( lambda sl: str(sl), @@ -67,28 +69,10 @@ class ComboBoxDelegate(QItemDelegate): self.commitData.emit(self.sender()) -class TableModel(QAbstractTableModel): - def __init__(self, study=None, reach=None, undo=None): - super(QAbstractTableModel, self).__init__() - self._headers = list(table_headers.keys()) - self._study = study - self._undo = undo - self._reach = reach - - def flags(self, index): - column = index.column() - - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - if self._headers[column] == "sl": - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return self._reach.number_profiles - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data + self._study = self._opt_data def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -98,11 +82,11 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self._reach.profile(row).name + return self._data.profile(row).name if self._headers[column] == "kp": - return self._reach.profile(row).kp + return self._data.profile(row).kp if self._headers[column] == "sl": - value = self._reach.profile(row).sl + value = self._data.profile(row).sl if value == None: text = _translate("SedimentLayers", "Not defined") return text @@ -110,12 +94,6 @@ class TableModel(QAbstractTableModel): return QVariant() - def headerData(self, friction, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return table_headers[self._headers[friction]] - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False @@ -135,7 +113,7 @@ class TableModel(QAbstractTableModel): self._undo.push( SetSLCommand( - self._reach, row, new + self._data, row, new ) ) @@ -145,7 +123,7 @@ class TableModel(QAbstractTableModel): def apply_sl_each_profile(self, sl): self._undo.push( ApplySLCommand( - self._reach, sl + self._data, sl ) ) self.layoutChanged.emit() diff --git a/src/View/SedimentLayers/Reach/Window.py b/src/View/SedimentLayers/Reach/Window.py index 2a655b7c..829bf035 100644 --- a/src/View/SedimentLayers/Reach/Window.py +++ b/src/View/SedimentLayers/Reach/Window.py @@ -28,10 +28,10 @@ from View.SedimentLayers.Reach.Table import * from View.SedimentLayers.Reach.Plot import Plot from View.SedimentLayers.Reach.SLDialog import SLDialog -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar -from View.SedimentLayers.Reach.translate import table_headers, retranslate +from View.SedimentLayers.Reach.translate import SedimentReachTranslate from View.SedimentLayers.Window import SedimentLayersWindow from View.SedimentLayers.Reach.Profile.Window import ProfileSedimentLayersWindow @@ -57,39 +57,40 @@ class ReachSedimentLayersWindow(PamhyrWindow): title = name, study = study, config = config, + trad = SedimentReachTranslate(), parent = parent ) self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_connections() def setup_table(self): - retranslate() + table_headers = self._trad.get_dict("table_headers") - table = self.find(QTableView, f"tableView") - self._table = TableModel( - study = self._study, - reach = self._reach, - undo = self._undo_stack, - ) - table.setModel(self._table) - - self._delegate_stricklers = ComboBoxDelegate( + self._delegate_sl = ComboBoxDelegate( study = self._study, + trad = self._trad, parent=self ) - table.setItemDelegateForColumn( - list(table_headers).index("sl"), - self._delegate_stricklers + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view = table, + data = self._reach, + opt_data = self._study, + table_headers = table_headers, + editable_headers = ["sl"], + delegates = {"sl": self._delegate_sl}, + trad = self._trad, + undo = self._undo_stack, ) - + table.setModel(self._table) table.setSelectionBehavior(QAbstractItemView.SelectRows) table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) table.setAlternatingRowColors(True) - def setup_graph(self): + def setup_plot(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") self.toolbar = PamhyrPlotToolbar( @@ -106,6 +107,7 @@ class ReachSedimentLayersWindow(PamhyrWindow): canvas = self.canvas, data = self._reach, toolbar = self.toolbar, + trad = self._trad, display_current = False ) self.plot.draw() diff --git a/src/View/SedimentLayers/Reach/translate.py b/src/View/SedimentLayers/Reach/translate.py index ef45f076..34785f32 100644 --- a/src/View/SedimentLayers/Reach/translate.py +++ b/src/View/SedimentLayers/Reach/translate.py @@ -2,15 +2,20 @@ from PyQt5.QtCore import QCoreApplication +from View.SedimentLayers.translate import SedimentTranslate + _translate = QCoreApplication.translate -table_headers = { - "name": _translate("SedimentLayers", "Name"), - "kp": _translate("SedimentLayers", "KP (m)"), - "sl": _translate("SedimentLayers", "Sediment layers"), -} +class SedimentReachTranslate(SedimentTranslate): + def __init__(self): + super(SedimentReachTranslate, self).__init__() -def retranslate(): - table_headers["name"] = _translate("SedimentLayers", "Name") - table_headers["kp"] = _translate("SedimentLayers", "KP (m)") - table_headers["sl"] = _translate("SedimentLayers", "Sediment layers") + self._dict["nd"] = _translate("SedimentLayers", "Not defined") + self._dict["kp"] = _translate("SedimentLayers", "Kp (m)") + self._dict["height"] = _translate("SedimentLayers", "Height (m)") + + self._sub_dict["table_headers"] = { + "name": _translate("SedimentLayers", "Name"), + "kp": _translate("SedimentLayers", "KP (m)"), + "sl": _translate("SedimentLayers", "Sediment layers"), + } From a64a9867ef22bed1a328668b096fce02c4c95267 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Wed, 4 Oct 2023 17:01:31 +0200 Subject: [PATCH 37/44] refactoring: refacto solver parameters --- src/Solver/Solvers.py | 6 +- src/View/MainWindow.py | 2 +- src/View/SolverParameters/Table.py | 69 ++++++---------- src/View/SolverParameters/Window.py | 15 ++-- src/View/SolverParameters/translate.py | 108 ++++++++++++------------- 5 files changed, 85 insertions(+), 115 deletions(-) diff --git a/src/Solver/Solvers.py b/src/Solver/Solvers.py index 221f1ff4..514e06c6 100644 --- a/src/Solver/Solvers.py +++ b/src/Solver/Solvers.py @@ -24,9 +24,9 @@ from Solver.Mage import Mage7, Mage8 _translate = QCoreApplication.translate solver_long_name = { - "generic": _translate("Solvers", "Generic"), - # "mage7": _translate("Solvers", "Mage version 7"), - "mage8": _translate("Solvers", "Mage version 8"), + "generic": "Generic", + # "mage7": "Mage v7", + "mage8": "Mage v8", } solver_type_list = { diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 9fee3a76..94acbe39 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -52,7 +52,7 @@ from View.Stricklers.Window import StricklersWindow from View.Frictions.Window import FrictionsWindow from View.SedimentLayers.Window import SedimentLayersWindow from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow -# from View.SolverParameters.Window import SolverParametersWindow +from View.SolverParameters.Window import SolverParametersWindow # from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow # from View.CheckList.Window import CheckListWindow # from View.Results.Window import ResultsWindow diff --git a/src/View/SolverParameters/Table.py b/src/View/SolverParameters/Table.py index 5aa488ab..47fa0ae9 100644 --- a/src/View/SolverParameters/Table.py +++ b/src/View/SolverParameters/Table.py @@ -34,37 +34,20 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + from View.SolverParameters.UndoCommand import * -from View.SolverParameters import translate as tr from Solver.Solvers import solver_long_name, solver_type_list +logger = logging.getLogger() + _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, data=None, undo=None, tab=""): - super(QAbstractTableModel, self).__init__() - self._headers = list(tr.table_headers.keys()) - self._data = data - self._undo = undo - self._tab = tab - self._params = self._data.get_params(self._tab) - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - - column = index.column() - - if self._headers[column] == "value": - options |= Qt.ItemIsEditable - - return options - - def rowCount(self, parent): - return len(self._params) - - def columnCount(self, parent): - return len(self._headers) +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._tab = self._opt_data + self._lst = self._data.get_params(self._tab) def data(self, index, role): if role != Qt.ItemDataRole.DisplayRole: @@ -75,24 +58,18 @@ class TableModel(QAbstractTableModel): cname = self._headers[column] if cname == "name": - value = self._params.get(row)[cname] - if value in tr.names: - value = tr.names[value] + value = self._lst.get(row)[cname] + if value in self._trad.get_dict("names"): + value = self._trad.get_dict("names")[value] return value elif cname == "value": - value = self._params.get(row)[cname] - if value in tr.yes_no: - value = tr.yes_no[value] + value = self._lst.get(row)[cname] + if value in self._trad.get_dict("yes_no"): + value = self._trad.get_dict("yes_no")[value] return value return QVariant() - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return tr.table_headers[self._headers[section]] - - return QVariant() - def setData(self, index, value, role=Qt.EditRole): if not index.isValid() or role != Qt.EditRole: return False @@ -102,12 +79,12 @@ class TableModel(QAbstractTableModel): try: if self._headers[column] == "value": - if value in tr.r_yes_no: - value = tr.r_yes_no[value].lower() + if value in self._trad.get_dict("r_yes_no"): + value = self._trad.get_dict("r_yes_no")[value].lower() self._undo.push( SetCommand( - self._params, row, + self._lst, row, "value", value ) ) @@ -123,7 +100,7 @@ class TableModel(QAbstractTableModel): # self._undo.push( # AddCommand( - # self._params, row + # self._lst, row # ) # ) @@ -135,7 +112,7 @@ class TableModel(QAbstractTableModel): # self._undo.push( # DelCommand( - # self._params, rows + # self._lst, rows # ) # ) @@ -147,7 +124,7 @@ class TableModel(QAbstractTableModel): # self._undo.push( # SortCommand( - # self._params, False + # self._lst, False # ) # ) @@ -164,7 +141,7 @@ class TableModel(QAbstractTableModel): # self._undo_stack.push( # MoveCommand( - # self._params, "up", row + # self._lst, "up", row # ) # ) @@ -172,7 +149,7 @@ class TableModel(QAbstractTableModel): # self.layoutChanged.emit() # def move_down(self, index, parent=QModelIndex()): - # if row > len(self._params): + # if row > len(self._lst): # return # target = row @@ -181,7 +158,7 @@ class TableModel(QAbstractTableModel): # self._undo_stack.push( # MoveCommand( - # self._params, "down", row + # self._lst, "down", row # ) # ) diff --git a/src/View/SolverParameters/Window.py b/src/View/SolverParameters/Window.py index a3b225a6..b655cd53 100644 --- a/src/View/SolverParameters/Window.py +++ b/src/View/SolverParameters/Window.py @@ -42,7 +42,7 @@ from PyQt5.QtWidgets import ( from View.SolverParameters.UndoCommand import * from View.SolverParameters.Table import TableModel -from View.SolverParameters import translate as tr +from View.SolverParameters.translate import ParamTranslate from Solver.Solvers import solver_long_name, solver_type_list @@ -55,16 +55,13 @@ class SolverParametersWindow(PamhyrWindow): _pamhyr_name = "Solver parameters" def __init__(self, study=None, config=None, parent=None): - # Init tanslate dictionary - tr.init() - name = self._pamhyr_name + " - " + study.name - super(SolverParametersWindow, self).__init__( title = name, study = study, config = config, options = ['copy'], + trad = ParamTranslate(), parent = parent ) @@ -73,8 +70,6 @@ class SolverParametersWindow(PamhyrWindow): self.setup_sc() self.setup_table() - self.ui.setWindowTitle(self._title) - def setup_sc(self): self._undo_stack = {} @@ -101,9 +96,13 @@ class SolverParametersWindow(PamhyrWindow): # Create table model self._table[st] = TableModel( + table_view = table, data = self._study.river, + opt_data = st, + table_headers = self._trad.get_dict("table_headers"), + editable_headers = ["value"], + trad = self._trad, undo = self._undo_stack[st], - tab = st, ) table.setModel(self._table[st]) diff --git a/src/View/SolverParameters/translate.py b/src/View/SolverParameters/translate.py index 2508629e..01c51a70 100644 --- a/src/View/SolverParameters/translate.py +++ b/src/View/SolverParameters/translate.py @@ -18,66 +18,60 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers = None -yes_no = None -r_yes_no = None -names = None +class ParamTranslate(PamhyrTranslate): + def __init__(self): + super(ParamTranslate, self).__init__() -def init(): - global table_headers - table_headers = { - "name": _translate("SolverParameters", "Name"), - "value": _translate("SolverParameters", "Value") - } + self._sub_dict["table_headers"] = { + "name": _translate("SolverParameters", "Name"), + "value": _translate("SolverParameters", "Value") + } - # Used to translate user parameter with value yes or no - global yes_no - yes_no = { - "yes": _translate("SolverParameters", "Yes"), - "no": _translate("SolverParameters", "No"), - "y": _translate("SolverParameters", "Y"), - "n": _translate("SolverParameters", "N"), - } + self._sub_dict["yes_no"] = { + "yes": _translate("SolverParameters", "Yes"), + "no": _translate("SolverParameters", "No"), + "y": _translate("SolverParameters", "Y"), + "n": _translate("SolverParameters", "N"), + } - # Reverse - global r_yes_no - r_yes_no = { - _translate("SolverParameters", "Yes"): "yes", - _translate("SolverParameters", "No"): "no", - _translate("SolverParameters", "Y"): "y", - _translate("SolverParameters", "N"): "n", - } + self._sub_dict["r_yes_no"] = { + _translate("SolverParameters", "Yes"): "yes", + _translate("SolverParameters", "No"): "no", + _translate("SolverParameters", "Y"): "y", + _translate("SolverParameters", "N"): "n", + } - global names - names = { - "all_init_time": _translate("SolverParameters", "Initial time (jj:hh:mm:ss)"), - "all_final_time": _translate("SolverParameters", "Final time (jj:hh:mm:ss)"), - "all_timestep": _translate("SolverParameters", "Timestep (second)"), - "all_command_line_arguments": _translate("SolverParameters", "Command line arguments"), - # Mage specific parameters - "mage_min_timestep": _translate("SolverParameters", "Minimum timestep (second)"), - "mage_timestep_tra": _translate("SolverParameters", "Time step of writing on .TRA"), - "mage_timestep_bin": _translate("SolverParameters", "Time step of writing on .BIN"), - "mage_implicitation": _translate("SolverParameters", "Implicitation parameter"), - "mage_continuity_discretization": _translate("SolverParameters", "Continuity discretization type (S/L)"), - "mage_qsj_discretization": _translate("SolverParameters", "QSJ discretization (A/B)"), - "mage_stop_criterion_iterations": _translate("SolverParameters", "Stop criterion iterations (G/A/R)"), - "mage_iteration_type": _translate("SolverParameters", "Iteration type"), - "mage_smooth_coef": _translate("SolverParameters", "Smoothing coefficient"), - "mage_cfl_max": _translate("SolverParameters", "Maximun accepted number of CFL"), - "mage_min_height": _translate("SolverParameters", "Minimum water height (meter)"), - "mage_max_niter": _translate("SolverParameters", "Maximun number of iterations (< 100)"), - "mage_timestep_reduction_factor": _translate("SolverParameters", "Timestep reduction factor"), - "mage_precision_reduction_factor_Z": _translate("SolverParameters", "Reduction precision factor of Z"), - "mage_precision_reduction_factor_Q": _translate("SolverParameters", "Reduction precision factor of Q"), - "mage_precision_reduction_factor_r": _translate("SolverParameters", "Reduction precision factor of residue"), - "mage_niter_max_precision": _translate("SolverParameters", "Number of iteration at maximum precision"), - "mage_niter_before_switch": _translate("SolverParameters", "Number of iteration before switch"), - "mage_max_froude": _translate("SolverParameters", "Maximum accepted Froude number"), - "mage_diffluence_node_height_balance": _translate("SolverParameters", "Diffluence node height balance"), - "mage_compute_reach_volume_balance": _translate("SolverParameters", "Compute reach volume balance (Y/N)"), - "mage_max_reach_volume_balance": _translate("SolverParameters", "Maximum reach volume balance"), - "mage_min_reach_volume_to_check": _translate("SolverParameters", "Minimum reach volume to check"), - } + self._sub_dict["names"] = { + "all_init_time": _translate("SolverParameters", "Initial time (jj:hh:mm:ss)"), + "all_final_time": _translate("SolverParameters", "Final time (jj:hh:mm:ss)"), + "all_timestep": _translate("SolverParameters", "Timestep (second)"), + "all_command_line_arguments": _translate("SolverParameters", "Command line arguments"), + # Mage specific parameters + "mage_min_timestep": _translate("SolverParameters", "Minimum timestep (second)"), + "mage_timestep_tra": _translate("SolverParameters", "Time step of writing on .TRA"), + "mage_timestep_bin": _translate("SolverParameters", "Time step of writing on .BIN"), + "mage_implicitation": _translate("SolverParameters", "Implicitation parameter"), + "mage_continuity_discretization": _translate("SolverParameters", "Continuity discretization type (S/L)"), + "mage_qsj_discretization": _translate("SolverParameters", "QSJ discretization (A/B)"), + "mage_stop_criterion_iterations": _translate("SolverParameters", "Stop criterion iterations (G/A/R)"), + "mage_iteration_type": _translate("SolverParameters", "Iteration type"), + "mage_smooth_coef": _translate("SolverParameters", "Smoothing coefficient"), + "mage_cfl_max": _translate("SolverParameters", "Maximun accepted number of CFL"), + "mage_min_height": _translate("SolverParameters", "Minimum water height (meter)"), + "mage_max_niter": _translate("SolverParameters", "Maximun number of iterations (< 100)"), + "mage_timestep_reduction_factor": _translate("SolverParameters", "Timestep reduction factor"), + "mage_precision_reduction_factor_Z": _translate("SolverParameters", "Reduction precision factor of Z"), + "mage_precision_reduction_factor_Q": _translate("SolverParameters", "Reduction precision factor of Q"), + "mage_precision_reduction_factor_r": _translate("SolverParameters", "Reduction precision factor of residue"), + "mage_niter_max_precision": _translate("SolverParameters", "Number of iteration at maximum precision"), + "mage_niter_before_switch": _translate("SolverParameters", "Number of iteration before switch"), + "mage_max_froude": _translate("SolverParameters", "Maximum accepted Froude number"), + "mage_diffluence_node_height_balance": _translate("SolverParameters", "Diffluence node height balance"), + "mage_compute_reach_volume_balance": _translate("SolverParameters", "Compute reach volume balance (Y/N)"), + "mage_max_reach_volume_balance": _translate("SolverParameters", "Maximum reach volume balance"), + "mage_min_reach_volume_to_check": _translate("SolverParameters", "Minimum reach volume to check"), + } From ba087f12a5d4dbc34463893a86b8e782a4bcf12d Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 10:40:18 +0200 Subject: [PATCH 38/44] refactoring: Apply table refacto on checklist. --- src/View/CheckList/Table.py | 28 +++------------------------- src/View/CheckList/Window.py | 10 ++++------ src/View/MainWindow.py | 4 ++-- src/View/Tools/PamhyrTable.py | 12 +++++++++++- 4 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/View/CheckList/Table.py b/src/View/CheckList/Table.py index 5810c4be..b355eb6c 100644 --- a/src/View/CheckList/Table.py +++ b/src/View/CheckList/Table.py @@ -35,25 +35,12 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel + _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, data=None): - super(QAbstractTableModel, self).__init__() - self._headers = ["name", "status"] - self._data = data - - def flags(self, index): - options = Qt.ItemIsEnabled | Qt.ItemIsSelectable - return options - - def rowCount(self, parent): - return len(self._data) - - def columnCount(self, parent): - return len(self._headers) - +class TableModel(PamhyrTableModel): def data(self, index, role): row = index.row() column = index.column() @@ -81,12 +68,3 @@ class TableModel(QAbstractTableModel): return self._data[row].summary return QVariant() - - def headerData(self, section, orientation, role): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return self._headers[section] - - return QVariant() - - def update(self): - self.layoutChanged.emit() diff --git a/src/View/CheckList/Window.py b/src/View/CheckList/Window.py index b02522bc..baf99983 100644 --- a/src/View/CheckList/Window.py +++ b/src/View/CheckList/Window.py @@ -40,6 +40,7 @@ from PyQt5.QtWidgets import ( from View.CheckList.Table import TableModel from View.CheckList.Worker import Worker +from View.CheckList.Translate import CheckListTranslate _translate = QCoreApplication.translate @@ -61,6 +62,7 @@ class CheckListWindow(PamhyrWindow): title = name, study = study, config = config, + trad = CheckListTranslate(), options = [], parent = parent ) @@ -78,16 +80,12 @@ class CheckListWindow(PamhyrWindow): def setup_table(self): table = self.find(QTableView, f"tableView") - self._table = TableModel( + table_view = table, + table_headers = self._trad.get_dict("table_headers"), data = self._checker_list, ) - table.setModel(self._table) - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) - def setup_progress_bar(self): self._progress = self.find(QProgressBar, f"progressBar") self._p = 0 # Progress current step diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 94acbe39..20f4f065 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -53,8 +53,8 @@ from View.Frictions.Window import FrictionsWindow from View.SedimentLayers.Window import SedimentLayersWindow from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow from View.SolverParameters.Window import SolverParametersWindow -# from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow -# from View.CheckList.Window import CheckListWindow +from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow +from View.CheckList.Window import CheckListWindow # from View.Results.Window import ResultsWindow from View.Debug.Window import ReplWindow diff --git a/src/View/Tools/PamhyrTable.py b/src/View/Tools/PamhyrTable.py index 65cd7687..d809dced 100644 --- a/src/View/Tools/PamhyrTable.py +++ b/src/View/Tools/PamhyrTable.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( QDialogButtonBox, QPushButton, QLineEdit, QFileDialog, QTableView, QAbstractItemView, QUndoStack, QShortcut, QAction, QItemDelegate, - QComboBox, QStyledItemDelegate, + QComboBox, QStyledItemDelegate, QHeaderView, ) logger = logging.getLogger() @@ -100,10 +100,17 @@ class PamhyrTableModel(QAbstractTableModel): self._setup_delegates() self._setup_lst() + self._table_view_configure() def _setup_lst(self): self._lst = self._data + def _table_view_configure(self): + self._table_view.setModel(self) + self._table_view.setSelectionBehavior(QAbstractItemView.SelectRows) + self._table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self._table_view.setAlternatingRowColors(True) + def flags(self, index): column = index.column() @@ -140,3 +147,6 @@ class PamhyrTableModel(QAbstractTableModel): def redo(self): self._undo.redo() self.layoutChanged.emit() + + def update(self): + self.layoutChanged.emit() From 2ff4f63d23ffc1c7ce4b4206d68111c878af6237 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 14:25:44 +0200 Subject: [PATCH 39/44] refactoring: refactor Results. --- doc/dev/documentation.org | 2 +- src/Model/Results/Results.py | 4 ++ src/Model/Results/River/River.py | 9 +++++ src/View/CheckList/Translate.py | 32 +++++++++++++++ src/View/MainWindow.py | 2 +- src/View/Results/PlotAC.py | 18 +++++---- src/View/Results/PlotH.py | 18 +++++---- src/View/Results/PlotKPC.py | 18 +++++---- src/View/Results/PlotSedProfile.py | 18 +++++---- src/View/Results/PlotSedReach.py | 18 +++++---- src/View/Results/PlotXY.py | 18 +++++---- src/View/Results/Table.py | 65 +++++++++--------------------- src/View/Results/Window.py | 40 +++++------------- src/View/Results/translate.py | 24 +++++------ 14 files changed, 149 insertions(+), 137 deletions(-) create mode 100644 src/View/CheckList/Translate.py diff --git a/doc/dev/documentation.org b/doc/dev/documentation.org index 8ca3ff05..e50fe58e 100644 --- a/doc/dev/documentation.org +++ b/doc/dev/documentation.org @@ -671,7 +671,7 @@ method to draw the plot from scratch, the second to update the plot if data has changed. #+begin_src python :python python3 :results output :noweb yes - from View.Plot.APlot import APlot + from View.Tools.PamhyrPlot import PamhyrPlot class MyPlot(APlot): def __init__(self, canvas=None, data=None, toolbar=None): diff --git a/src/Model/Results/Results.py b/src/Model/Results/Results.py index 67698f4e..3258b0e1 100644 --- a/src/Model/Results/Results.py +++ b/src/Model/Results/Results.py @@ -43,6 +43,10 @@ class Results(object): def river(self): return self._river + @property + def study(self): + return self._study + def set(self, key, value): self._meta_data[key] = value diff --git a/src/Model/Results/River/River.py b/src/Model/Results/River/River.py index 9f72f731..80cd8450 100644 --- a/src/Model/Results/River/River.py +++ b/src/Model/Results/River/River.py @@ -26,6 +26,9 @@ class Profile(object): self._profile = profile # Source profile in the study self._data = {} # Dict of dict {: {: , ...}, ...} + def __len__(self): + return len(self._data) + @property def name(self): return self._profile.name @@ -66,6 +69,9 @@ class Reach(object): ) ) + def __len__(self): + return len(self._profiles) + @property def name(self): return self._reach.name @@ -91,6 +97,9 @@ class River(object): # Dict with timestamps as key self._reachs = [] + def __len__(self): + return len(self._reachs) + @property def reachs(self): return self._reachs.copy() diff --git a/src/View/CheckList/Translate.py b/src/View/CheckList/Translate.py new file mode 100644 index 00000000..b3ce856d --- /dev/null +++ b/src/View/CheckList/Translate.py @@ -0,0 +1,32 @@ +# Translate.py -- Pamhyr +# Copyright (C) 2023 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + +class CheckListTranslate(PamhyrTranslate): + def __init__(self): + super(CheckListTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("CheckList", "Name"), + "status": _translate("CheckList", "Status"), + } diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 20f4f065..e2a8859c 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -55,7 +55,7 @@ from View.SedimentLayers.Reach.Window import ReachSedimentLayersWindow from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow -# from View.Results.Window import ResultsWindow +from View.Results.Window import ResultsWindow from View.Debug.Window import ReplWindow from Model.Study import Study diff --git a/src/View/Results/PlotAC.py b/src/View/Results/PlotAC.py index f0e4b0e2..ed3d99ad 100644 --- a/src/View/Results/PlotAC.py +++ b/src/View/Results/PlotAC.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,16 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotAC(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotAC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotAC, self).__init__( - canvas=canvas, - data=results, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._current_timestamp = max(results.get("timestamps")) diff --git a/src/View/Results/PlotH.py b/src/View/Results/PlotH.py index 557c7cec..99a59f88 100644 --- a/src/View/Results/PlotH.py +++ b/src/View/Results/PlotH.py @@ -22,7 +22,7 @@ from functools import reduce from datetime import datetime from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -32,14 +32,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotH(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotH(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotH, self).__init__( - canvas=canvas, - data=results, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._mode = "time" diff --git a/src/View/Results/PlotKPC.py b/src/View/Results/PlotKPC.py index 68f6fb4e..61de649d 100644 --- a/src/View/Results/PlotKPC.py +++ b/src/View/Results/PlotKPC.py @@ -17,7 +17,7 @@ # -*- coding: utf-8 -*- from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -25,14 +25,16 @@ from PyQt5.QtCore import ( _translate = QCoreApplication.translate -class PlotKPC(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotKPC(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotKPC, self).__init__( - canvas=canvas, - data=results, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._current_timestamp = max(results.get("timestamps")) diff --git a/src/View/Results/PlotSedProfile.py b/src/View/Results/PlotSedProfile.py index 62f4e290..3163931b 100644 --- a/src/View/Results/PlotSedProfile.py +++ b/src/View/Results/PlotSedProfile.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,14 +15,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotSedProfile(APlot): - def __init__(self, canvas=None, study=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotSedProfile(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotSedProfile, self).__init__( - canvas=canvas, - data=study, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._results = results diff --git a/src/View/Results/PlotSedReach.py b/src/View/Results/PlotSedReach.py index 657b70e8..c63c80ec 100644 --- a/src/View/Results/PlotSedReach.py +++ b/src/View/Results/PlotSedReach.py @@ -5,7 +5,7 @@ import logging from functools import reduce from tools import timer -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -15,14 +15,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotSedReach(APlot): - def __init__(self, canvas=None, study=None, results=None, - reach_id=0, profile_id=0, - toolbar=None): +class PlotSedReach(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + parent=None): super(PlotSedReach, self).__init__( - canvas=canvas, - data=study, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self._results = results diff --git a/src/View/Results/PlotXY.py b/src/View/Results/PlotXY.py index 3e7a88f3..42716776 100644 --- a/src/View/Results/PlotXY.py +++ b/src/View/Results/PlotXY.py @@ -21,7 +21,7 @@ import logging from functools import reduce from tools import timer, trace -from View.Plot.APlot import APlot +from View.Tools.PamhyrPlot import PamhyrPlot from PyQt5.QtCore import ( QCoreApplication @@ -31,14 +31,16 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -class PlotXY(APlot): - def __init__(self, canvas=None, results=None, - reach_id=0, profile_id=0, - toolbar=None, display_current=True): +class PlotXY(PamhyrPlot): + def __init__(self, canvas=None, trad=None, toolbar=None, + results=None, reach_id=0, profile_id=0, + display_current=True, parent=None): super(PlotXY, self).__init__( - canvas=canvas, - data=results, - toolbar=toolbar + canvas = canvas, + trad = trad, + data = results, + toolbar = toolbar, + parent = parent ) self.display_current = display_current diff --git a/src/View/Results/Table.py b/src/View/Results/Table.py index 8e167459..f4d7b81a 100644 --- a/src/View/Results/Table.py +++ b/src/View/Results/Table.py @@ -34,6 +34,7 @@ from PyQt5.QtWidgets import ( QComboBox, ) +from View.Tools.PamhyrTable import PamhyrTableModel from View.Results.translate import * logger = logging.getLogger() @@ -41,33 +42,16 @@ logger = logging.getLogger() _translate = QCoreApplication.translate -class TableModel(QAbstractTableModel): - def __init__(self, results = None, study = None, mode = None, undo=None): - super(QAbstractTableModel, self).__init__() +class TableModel(PamhyrTableModel): + def _setup_lst(self): + _river = self._data.river + if self._opt_data == "reach": + self._lst = _river.reachs + else: + self._lst = _river.reach(0).profiles - self._results = results - self._study = study - self._mode = mode - self._undo_stack = undo - - self._table_headers = table_headers_reach - if mode != "reach": - self._table_headers = table_headers_profile - - self._headers = list(self._table_headers.keys()) - - self._selected = 0 - self._timestamp = 0 - - def rowCount(self, parent=QModelIndex()): - if self._mode == "reach": - return len(self._results.river.reachs) - - current_reach = self._results.river.reach(self._selected) - return len(current_reach.profiles) - - def columnCount(self, parent=QModelIndex()): - return len(self._headers) + def __init__(self, **kwargs): + super(TableModel, self).__init__(**kwargs) def data(self, index, role=Qt.DisplayRole): if role != Qt.ItemDataRole.DisplayRole: @@ -76,32 +60,23 @@ class TableModel(QAbstractTableModel): row = index.row() column = index.column() - if self._mode == "reach": + if self._opt_data == "reach": if self._headers[column] == "name": - v = self._results.river.reach(row).name + v = self._lst[row].name return str(v) else: - current_reach = self._results.river.reach(self._selected) if self._headers[column] == "name": - v = current_reach.profile(row).name + v = self._lst[row].name return str(v) elif self._headers[column] == "kp": - v = current_reach.profile(row).kp + v = self._lst[row].kp return f"{v:.4f}" return QVariant() - def headerData(self, section, orientation, role=Qt.DisplayRole): - if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal: - return self._table_headers[self._headers[section]] - - return QVariant() - - def index(self, row, column, parent=QModelIndex()): - if not self.hasIndex(row, column, parent): - return QModelIndex() - - return self.createIndex(row, column, QModelIndex()) - - def flags(self, index): - return Qt.ItemIsEnabled | Qt.ItemIsSelectable + def update(self, reach): + _river = self._data.river + if self._opt_data == "reach": + self._lst = _river.reachs + else: + self._lst = _river.reach(reach).profiles diff --git a/src/View/Results/Window.py b/src/View/Results/Window.py index 4009c836..61e9f22b 100644 --- a/src/View/Results/Window.py +++ b/src/View/Results/Window.py @@ -41,8 +41,8 @@ from PyQt5.QtWidgets import ( QSlider, QLabel, ) -from View.Plot.MplCanvas import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar from View.Results.PlotXY import PlotXY from View.Results.PlotAC import PlotAC @@ -52,10 +52,7 @@ from View.Results.PlotSedReach import PlotSedReach from View.Results.PlotSedProfile import PlotSedProfile from View.Results.Table import TableModel -from View.Results.translate import ( - table_headers_reach, table_headers_profile, - retranslate, -) +from View.Results.translate import ResultsTranslate from View.Stricklers.Window import StricklersWindow _translate = QCoreApplication.translate @@ -85,42 +82,27 @@ class ResultsWindow(PamhyrWindow): title = name, study = study, config = config, + trad = ResultsTranslate(), parent=parent ) self.setup_table() - self.setup_graph() + self.setup_plot() self.setup_slider() self.setup_statusbar() self.setup_connections() - self.ui.setWindowTitle(self._title) - - def setup_title(self, title): - self._title = ( - title + " - " - + self._study.name + " - " - + self._solver.name + " - " - + self._results.date - ) - def setup_table(self): - retranslate() self._table = {} - for t in ["reach", "profile"]: table = self.find(QTableView, f"tableView_{t}") self._table[t] = TableModel( - results = self._results, - study = self._study, - mode = t, + table_view = table, + table_headers = self._trad.get_dict(f"table_headers_{t}"), + data = self._results, undo = self._undo_stack, + opt_data = t ) - table.setModel(self._table[t]) - - table.setSelectionBehavior(QAbstractItemView.SelectRows) - table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - table.setAlternatingRowColors(True) def setup_slider(self): self._slider_profile = self.find(QSlider, f"verticalSlider_profile") @@ -132,7 +114,7 @@ class ResultsWindow(PamhyrWindow): self._slider_time.setMaximum(len(self._timestamps) - 1) self._slider_time.setValue(len(self._timestamps) - 1) - def setup_graph(self): + def setup_plot(self): self.canvas = MplCanvas(width=5, height=4, dpi=100) self.canvas.setObjectName("canvas") self.toolbar = PamhyrPlotToolbar( @@ -219,7 +201,6 @@ class ResultsWindow(PamhyrWindow): self.plot_sed_reach = PlotSedReach( canvas = self.canvas_5, results = self._results, - study = self._study, reach_id = 0, profile_id = 0, toolbar = self.toolbar_5 @@ -239,7 +220,6 @@ class ResultsWindow(PamhyrWindow): self.plot_sed_profile = PlotSedProfile( canvas = self.canvas_6, results = self._results, - study = self._study, reach_id = 0, profile_id = 0, toolbar = self.toolbar_6 diff --git a/src/View/Results/translate.py b/src/View/Results/translate.py index 37351b66..14f55ba3 100644 --- a/src/View/Results/translate.py +++ b/src/View/Results/translate.py @@ -18,19 +18,19 @@ from PyQt5.QtCore import QCoreApplication +from View.Tools.PamhyrTranslate import PamhyrTranslate + _translate = QCoreApplication.translate -table_headers_reach = { - "name": _translate("Results", "Reach name"), -} +class ResultsTranslate(PamhyrTranslate): + def __init__(self): + super(ResultsTranslate, self).__init__() -table_headers_profile = { - "name": _translate("Results", "Name"), - "kp": _translate("Results", "KP (m)"), -} + self._sub_dict["table_headers_reach"] = { + "name": _translate("Results", "Reach name"), + } -def retranslate(): - table_headers_reach["name"] = _translate("Results", "Reach name") - - table_headers_profile["name"] = _translate("Results", "Name") - table_headers_profile["kp"] = _translate("Results", "KP (m)") + self._sub_dict["table_headers_profile"] = { + "name": _translate("Results", "Name"), + "kp": _translate("Results", "KP (m)"), + } From b1c658d7116614c173144fbe6feb54c4ec7e6c56 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 14:34:15 +0200 Subject: [PATCH 40/44] refactoring: Delete deprecated unused files. --- .../Geometry/Profile/mainwindow_ui_profile.py | 260 ------------- src/View/Geometry/mainwindow_ui_reach.py | 357 ------------------ 2 files changed, 617 deletions(-) delete mode 100644 src/View/Geometry/Profile/mainwindow_ui_profile.py delete mode 100644 src/View/Geometry/mainwindow_ui_reach.py diff --git a/src/View/Geometry/Profile/mainwindow_ui_profile.py b/src/View/Geometry/Profile/mainwindow_ui_profile.py deleted file mode 100644 index a6844e55..00000000 --- a/src/View/Geometry/Profile/mainwindow_ui_profile.py +++ /dev/null @@ -1,260 +0,0 @@ -# mainwindow_ui_profile.py -- Pamhyr -# Copyright (C) 2023 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. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# -*- coding: utf-8 -*- - -import os.path - -from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import Qt, QSize -from PyQt5.QtWidgets import ( - QTableView, QAbstractItemView, QHeaderView, - QToolBar, -) - -from View.Plot import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar - -file_path = os.path.abspath(os.path.dirname(__file__)) -icon_path = f"{os.path.dirname(os.path.dirname(file_path))}/../View/ui/ressources/" - -class Ui_MainWindow(object): - def setupUi(self, MainWindowProfile): - MainWindowProfile.setObjectName("MainWindowProfile") - MainWindowProfile.resize(1089, 699) - self.centralwidget = QtWidgets.QWidget(MainWindowProfile) - self.centralwidget.setObjectName("centralwidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.centralwidget) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.splitter = QtWidgets.QSplitter(self.centralwidget) - self.splitter.setOrientation(Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.widget = QtWidgets.QWidget(self.splitter) - self.widget.setObjectName("widget") - self.verticalLayout_left = QtWidgets.QVBoxLayout(self.widget) - self.verticalLayout_left.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_left.setObjectName("verticalLayout_left") - - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - - self.toolBar = QToolBar(MainWindowProfile) - self.toolBar.setOrientation(Qt.Vertical) - self.toolBar.setEnabled(True) - MainWindowProfile.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) - - btn_size = QSize(40, 28) - - self.btn_add = QtWidgets.QAction(self.widget) - self.btn_add.setObjectName("btn_add") - icon_btn_add = QtGui.QIcon() - icon_btn_add.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-add.png")) - self.btn_add.setIcon(icon_btn_add) - self.toolBar.addAction(self.btn_add) - - self.btn_delete = QtWidgets.QAction(self.widget) - self.btn_delete.setObjectName("btn_delete") - icon_btn_delete = QtGui.QIcon() - icon_btn_delete.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-remove.png")) - self.btn_delete.setIcon(icon_btn_delete) - self.toolBar.addAction(self.btn_delete) - - self.btn_sort_asc_x = QtWidgets.QAction(self.widget) - self.btn_sort_asc_x.setObjectName("btn_sort_asc_x") - icon_btn_sort_asc_x = QtGui.QIcon() - icon_btn_sort_asc_x.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-ascending.png")) - self.btn_sort_asc_x.setIcon(icon_btn_sort_asc_x) - self.toolBar.addAction(self.btn_sort_asc_x) - - self.btn_sort_desc_x = QtWidgets.QAction(self.widget) - self.btn_sort_desc_x.setObjectName("btn_sort_desc_x") - icon_btn_sort_desc_x = QtGui.QIcon() - icon_btn_sort_desc_x.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-descending.png")) - self.btn_sort_desc_x.setIcon(icon_btn_sort_desc_x) - self.toolBar.addAction(self.btn_sort_desc_x) - - self.btn_sort_asc_y = QtWidgets.QAction(self.widget) - self.btn_sort_asc_y.setObjectName("btn_sort_asc_y") - icon_btn_sort_asc_y = QtGui.QIcon() - icon_btn_sort_asc_y.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-ascending.png")) - self.btn_sort_asc_y.setIcon(icon_btn_sort_asc_y) - self.toolBar.addAction(self.btn_sort_asc_y) - - self.btn_sort_desc_y = QtWidgets.QAction(self.widget) - self.btn_sort_desc_y.setObjectName("btn_sort_desc_y") - icon_btn_sort_desc_y = QtGui.QIcon() - icon_btn_sort_desc_y.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-descending.png")) - self.btn_sort_desc_y.setIcon(icon_btn_sort_desc_y) - self.toolBar.addAction(self.btn_sort_desc_y) - - self.btn_move_up = QtWidgets.QAction(self.widget) - self.btn_move_up.setObjectName("btn_move_up") - icon_btn_move_up = QtGui.QIcon() - icon_btn_move_up.addPixmap( - QtGui.QPixmap(f"{icon_path}up.png")) - self.btn_move_up.setIcon(icon_btn_move_up) - self.toolBar.addAction(self.btn_move_up) - - self.btn_move_down = QtWidgets.QAction(self.widget) - self.btn_move_down.setObjectName("btn_move_down") - icon_btn_move_down = QtGui.QIcon() - icon_btn_move_down.addPixmap( - QtGui.QPixmap(f"{icon_path}down.png")) - self.btn_move_down.setIcon(icon_btn_move_down) - self.toolBar.addAction(self.btn_move_down) - - self.btn_export = QtWidgets.QAction(self.widget) - self.btn_export.setObjectName("btn_export") - icon_btn_export = QtGui.QIcon() - icon_btn_export.addPixmap( - QtGui.QPixmap(f"{icon_path}export.png")) - self.btn_export.setIcon(icon_btn_export) - self.toolBar.addAction(self.btn_export) - - self.btn_copy = QtWidgets.QAction(self.widget) - self.btn_copy.setObjectName("btn_copy") - icon_btn_copy = QtGui.QIcon() - icon_btn_copy.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-copy.png")) - self.btn_copy.setIcon(icon_btn_copy) - self.toolBar.addAction(self.btn_copy) - - self.btn_paste = QtWidgets.QAction(self.widget) - self.btn_paste.setObjectName("btn_paste") - icon_btn_paste = QtGui.QIcon() - icon_btn_paste.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-paste.png")) - self.btn_paste.setIcon(icon_btn_paste) - self.toolBar.addAction(self.btn_paste) - - self.btn_check = QtWidgets.QAction(self.widget) - self.btn_check.setObjectName("btn_check") - icon_btn_check = QtGui.QIcon() - icon_btn_check.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-apply.png")) - self.btn_check.setIcon(icon_btn_check) - self.toolBar.addAction(self.btn_check) - - self.btn_go_back = QtWidgets.QAction(self.widget) - self.btn_go_back.setObjectName("btn_go_back") - icon_btn_go_back = QtGui.QIcon() - icon_btn_go_back.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-undo.png")) - self.btn_go_back.setIcon(icon_btn_go_back) - self.toolBar.addAction(self.btn_go_back) - - self.btn_reset = QtWidgets.QAction(self.widget) - self.btn_reset.setObjectName("btn_reset") - icon_btn_reset = QtGui.QIcon() - icon_btn_reset.addPixmap( - QtGui.QPixmap(f"{icon_path}cancel.png")) - self.btn_reset.setIcon(icon_btn_reset) - self.toolBar.addAction(self.btn_reset) - - self.verticalLayout_left.addLayout(self.horizontalLayout) - - self.tableView = QTableView(self.widget) - self.tableView.setObjectName("tableView") - self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) - self.tableView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents) - self.tableView.resizeColumnsToContents() - self.tableView.resizeRowsToContents() - self.tableView.setAlternatingRowColors(True) - self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.verticalLayout_left.addWidget(self.tableView) - - self.widget1 = QtWidgets.QWidget(self.splitter) - self.widget1.setObjectName("widget1") - - self.verticalLayout_right = QtWidgets.QVBoxLayout(self.widget1) - self.verticalLayout_right.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_right.setObjectName("verticalLayout_right") - - self.label_graphic_help = QtWidgets.QLabel(self.widget1) - self.label_graphic_help.setMaximumSize(QtCore.QSize(16777215, 25)) - self.label_graphic_help.setBaseSize(QtCore.QSize(0, 30)) - self.label_graphic_help.setObjectName("label_graphic_help") - self.verticalLayout_right.addWidget(self.label_graphic_help) - - self.canvas = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas.setObjectName("canvas") - - self.my_toolbar = PamhyrPlotToolbar( - self.canvas, self.widget1 - ) - - self.verticalLayout_right.addWidget(self.my_toolbar) - self.verticalLayout_right.addWidget(self.canvas) - self.horizontalLayout_2.addWidget(self.splitter) - - MainWindowProfile.setCentralWidget(self.centralwidget) - - self.menubar = QtWidgets.QMenuBar(MainWindowProfile) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1089, 20)) - self.menubar.setObjectName("menubar") - MainWindowProfile.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindowProfile) - self.statusbar.setObjectName("statusbar") - MainWindowProfile.setStatusBar(self.statusbar) - - self.retranslateUi(MainWindowProfile) - QtCore.QMetaObject.connectSlotsByName(MainWindowProfile) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindowProfile", "MainWindowProfile")) - self.btn_add.setToolTip(_translate("MainWindowProfile", "Insérer un point")) - self.btn_delete.setToolTip( - _translate("MainWindowProfile", "Supprimer le/les point(s) sélectionnés")) - self.btn_sort_asc_x.setToolTip( - _translate("MainWindowProfile", "Trier les points par ordre croissant de X")) - self.btn_sort_desc_x.setToolTip( - _translate("MainWindowProfile", "Trier les points par ordre décroissant de X")) - self.btn_sort_asc_y.setToolTip(_translate("MainWindowProfile", - "Trier les points par ordre croissant de Y" - )) - self.btn_sort_desc_y.setToolTip(_translate("MainWindowProfile", - "Trier les points par ordre décroissant de Y")) - self.btn_move_up.setToolTip(_translate("MainWindowProfile", - "Décaler le point sélectionné vers le haut")) - self.btn_move_down.setToolTip(_translate("MainWindowProfile", - "Décaler le point sélectionné vers le bas")) - self.btn_export.setToolTip(_translate("MainWindowProfile", - "Exporter (dans un fichier) les points du profil au format tabulé")) - self.btn_copy.setToolTip( - _translate("MainWindowProfile", "Copier la sélection au format tabulé")) - self.btn_paste.setToolTip(_translate("MainWindowProfile", - "Coller la sélection depuis le presse-papier au format tabulé")) - self.btn_check.setToolTip(_translate("MainWindowProfile", - "Vérifier la validité de la saisie et garder ou pas les modifications " - "apportées")) - self.btn_go_back.setToolTip(_translate("MainWindowProfile", - "Annuler toutes les modifications depuis la dernière validation")) - self.btn_reset.setToolTip(_translate("MainWindowProfile", - "Annuler toutes les modifications et revenir à l\'état initial")) - self.label_graphic_help.setToolTip(_translate("MainWindowProfile", - "Ligne d'eau : \n \tZ : Cote (m) \n \tA : Aire mouillée (" - "m\u00B2) \n \tp : " - "Périmètre mouillé (m) \n \tL : Largeur au miroir (m)")) - self.label_graphic_help.setText( - _translate("MainWindowProfile", "\'Maj + Clic\' : Ligne d\'eau & \'Ctrl + Clic\' : " - "Sélectionner des points")) diff --git a/src/View/Geometry/mainwindow_ui_reach.py b/src/View/Geometry/mainwindow_ui_reach.py deleted file mode 100644 index ff0797ab..00000000 --- a/src/View/Geometry/mainwindow_ui_reach.py +++ /dev/null @@ -1,357 +0,0 @@ -# mainwindow_ui_reach.py -- Pamhyr -# Copyright (C) 2023 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. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# -*- coding: utf-8 -*- - -import sys -import os.path - -from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import QSize, Qt -from PyQt5.QtWidgets import ( - QAbstractItemView, QHeaderView, QToolBar, -) - -from View.Plot import MplCanvas -from View.Plot.PamhyrToolbar import PamhyrPlotToolbar - - -file_path = os.path.abspath(os.path.dirname(__file__)) -icon_path = f"{os.path.dirname(os.path.dirname(file_path))}/View/ui/ressources/" - -class Ui_MainWindow(object): - def setupUi(self, MainWindow_reach): - MainWindow_reach.setObjectName("MainWindow_reach") - MainWindow_reach.resize(1400, 800) - self.centralwidget = QtWidgets.QWidget(MainWindow_reach) - self.centralwidget.setObjectName("centralwidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.centralwidget) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.splitter_3 = QtWidgets.QSplitter(self.centralwidget) - self.splitter_3.setOrientation(Qt.Horizontal) - self.splitter_3.setObjectName("splitter_3") - self.widget = QtWidgets.QWidget(self.splitter_3) - self.widget.setObjectName("widget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.widget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - - # self.label_section_order = QtWidgets.QLabel(self.widget) - # self.label_section_order.setLayoutDirection(Qt.LeftToRight) - # self.label_section_order.setObjectName("label_section_order") - - # self.verticalLayout.addWidget(self.label_section_order) - - self.toolBar = QToolBar(MainWindow_reach) - self.toolBar.setEnabled(True) - MainWindow_reach.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) - - btn_size = QSize(40, 28) - - self.btn_open = QtWidgets.QAction(self.widget) - self.btn_open.setObjectName("btn_open") - icon_btn_open = QtGui.QIcon() - icon_btn_open.addPixmap( - QtGui.QPixmap(f"{icon_path}open.png")) - self.btn_open.setIcon(icon_btn_open) - self.toolBar.addAction(self.btn_open) - - self.btn_add = QtWidgets.QAction(self.widget) - self.btn_add.setObjectName("btn_add") - icon_btn_add = QtGui.QIcon() - icon_btn_add.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk_add.png")) - self.btn_add.setIcon(icon_btn_add) - self.toolBar.addAction(self.btn_add) - - self.btn_delete = QtWidgets.QAction(self.widget) - self.btn_delete.setObjectName("btn_delete") - icon_btn_delete = QtGui.QIcon() - icon_btn_delete.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-remove.png")) - self.btn_delete.setIcon(icon_btn_delete) - self.toolBar.addAction(self.btn_delete) - - self.btn_edit = QtWidgets.QAction(self.widget) - self.btn_edit.setObjectName("btn_edit") - icon_btn_edit = QtGui.QIcon() - icon_btn_edit.addPixmap( - QtGui.QPixmap(f"{icon_path}gnome-stock-edit.png")) - self.btn_edit.setIcon(icon_btn_edit) - self.toolBar.addAction(self.btn_edit) - - # self.btn_copy_selected_profile = QtWidgets.QAction(self.widget) - # self.btn_copy_selected_profile.setObjectName("btn_copy_selected_profile") - # icon_btn_copy_selected_profile = QtGui.QIcon() - # icon_btn_copy_selected_profile.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-copy.png")) - # self.btn_copy_selected_profile.setIcon(icon_btn_copy_selected_profile) - # self.toolBar.addAction(self.btn_copy_selected_profile) - - # self.btn_paste_selected_profile = QtWidgets.QAction(self.widget) - # self.btn_paste_selected_profile.setObjectName("btn_paste_selected_profile") - # icon_btn_paste_selected_profile = QtGui.QIcon() - # icon_btn_paste_selected_profile.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-paste.png")) - # self.btn_paste_selected_profile.setIcon(icon_btn_paste_selected_profile) - # self.toolBar.addAction(self.btn_paste_selected_profile) - - # self.btn_duplicate_selected_profile = QtWidgets.QAction(self.widget) - # self.btn_duplicate_selected_profile.setObjectName("btn_duplicate_selected_profile") - # icon_btn_duplicate_selected_profile = QtGui.QIcon() - # icon_btn_duplicate_selected_profile.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-dnd-multiple.png")) - # self.btn_duplicate_selected_profile.setIcon(icon_btn_duplicate_selected_profile) - # self.toolBar.addAction(self.btn_duplicate_selected_profile) - - self.btn_sort_asc = QtWidgets.QAction(self.widget) - self.btn_sort_asc.setObjectName("btn_sort_asc") - icon_btn_sort_asc = QtGui.QIcon() - icon_btn_sort_asc.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-ascending.png")) - self.btn_sort_asc.setIcon(icon_btn_sort_asc) - self.toolBar.addAction(self.btn_sort_asc) - - self.btn_sort_desc = QtWidgets.QAction(self.widget) - self.btn_sort_desc.setObjectName("btn_sort_desc") - icon_btn_sort_desc = QtGui.QIcon() - icon_btn_sort_desc.addPixmap( - QtGui.QPixmap(f"{icon_path}gtk-sort-descending.png")) - self.btn_sort_desc.setIcon(icon_btn_sort_desc) - self.toolBar.addAction(self.btn_sort_desc) - - self.btn_move_up = QtWidgets.QAction(self.widget) - self.btn_move_up.setObjectName("btn_move_up") - icon_btn_move_up = QtGui.QIcon() - icon_btn_move_up.addPixmap( - QtGui.QPixmap(f"{icon_path}up.png")) - self.btn_move_up.setIcon(icon_btn_move_up) - self.toolBar.addAction(self.btn_move_up) - - self.btn_move_down = QtWidgets.QAction(self.widget) - self.btn_move_down.setObjectName("btn_move_down") - icon_btn_move_down = QtGui.QIcon() - icon_btn_move_down.addPixmap( - QtGui.QPixmap(f"{icon_path}down.png")) - self.btn_move_down.setIcon(icon_btn_move_down) - self.toolBar.addAction(self.btn_move_down) - - # self.btn_end_editing = QtWidgets.QAction(self.widget) - # self.btn_end_editing.setObjectName("btn_end_editing") - # icon_btn_save = QtGui.QIcon() - # icon_btn_save.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-apply.png")) - # self.btn_end_editing.setIcon(icon_btn_save) - # self.toolBar.addAction(self.btn_end_editing) - - self.verticalLayout.addLayout(self.horizontalLayout) - self.tableView = QtWidgets.QTableView(self.widget) - self.tableView.setObjectName("tableView") - self.verticalLayout.addWidget(self.tableView) - self.tableView.setSelectionBehavior(QAbstractItemView.SelectRows) - self.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - self.tableView.setAlternatingRowColors(True) - self.tableView.verticalHeader().hide() - self.tableView.resizeColumnsToContents() - self.tableView.resizeRowsToContents() - - self.widget1 = QtWidgets.QWidget(self.splitter_3) - self.widget1.setObjectName("widget1") - self.gridLayout = QtWidgets.QGridLayout(self.widget1) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.splitter_2 = QtWidgets.QSplitter(self.widget1) - self.splitter_2.setOrientation(Qt.Vertical) - self.splitter_2.setObjectName("splitter_2") - self.splitter = QtWidgets.QSplitter(self.splitter_2) - self.splitter.setOrientation(Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.verticalLayoutWidget_3 = QtWidgets.QWidget(self.splitter) - self.verticalLayoutWidget_3.setObjectName("verticalLayoutWidget_3") - self.verticalLayout_canvas_1 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget_3) - self.verticalLayout_canvas_1.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_canvas_1.setObjectName("verticalLayout_canvas_1") - - self.canvas_1 = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas_1.setObjectName("canvas_1") - - self.toolbar_1 = PamhyrPlotToolbar( - self.canvas_1, self.centralwidget, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] - ) - - self.verticalLayout_canvas_1.addWidget(self.toolbar_1) - self.verticalLayout_canvas_1.addWidget(self.canvas_1) - - self.verticalLayoutWidget_2 = QtWidgets.QWidget(self.splitter) - self.verticalLayoutWidget_2.setObjectName("verticalLayoutWidget_2") - self.verticalLayout_canvas_2 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget_2) - self.verticalLayout_canvas_2.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_canvas_2.setObjectName("verticalLayout_canvas_2") - - self.canvas_2 = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas_2.setObjectName("canvas_2") - - self.toolbar_2 = PamhyrPlotToolbar( - self.canvas_2, self.centralwidget, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] - ) - - self.verticalLayout_canvas_2.addWidget(self.toolbar_2) - self.verticalLayout_canvas_2.addWidget(self.canvas_2) - self.verticalLayoutWidget = QtWidgets.QWidget(self.splitter_2) - self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") - self.verticalLayout_canvas_3 = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) - self.verticalLayout_canvas_3.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_canvas_3.setObjectName("verticalLayout_canvas_3") - - self.canvas_3 = MplCanvas.MplCanvas(width=5, height=4, dpi=100) - self.canvas_3.setObjectName("canvas_3") - - self.toolbar_3 = PamhyrPlotToolbar( - self.canvas_3, self.centralwidget, - items = ["home", "zoom", "save", "iso", "back/forward", "move"] - ) - - self.verticalLayout_canvas_3.addWidget(self.toolbar_3) - self.verticalLayout_canvas_3.addWidget(self.canvas_3) - - self.gridLayout.addWidget(self.splitter_2, 0, 0, 3, 3) - self.verticalSlider = QtWidgets.QSlider(self.widget1) - self.verticalSlider.setMinimumSize(QtCore.QSize(30, 0)) - self.verticalSlider.setOrientation(Qt.Vertical) - self.verticalSlider.setInvertedAppearance(True) - self.verticalSlider.setInvertedControls(False) - self.verticalSlider.setObjectName("verticalSlider") - self.gridLayout.addWidget(self.verticalSlider, 0, 3, 1, 1) - - self.btn_slider_up = QtWidgets.QPushButton(self.widget1) - self.btn_slider_up.setMaximumSize(btn_size) - self.btn_slider_up.setText("") - self.btn_slider_up.setObjectName("btn_slider_up") - icon_btn_slider_up = QtGui.QIcon() - icon_btn_slider_up.addPixmap( - QtGui.QPixmap(f"{icon_path}go-up2.png")) - self.btn_slider_up.setIcon(icon_btn_slider_up) - #self.btn_slider_up.setIconSize(btn_size) - self.gridLayout.addWidget(self.btn_slider_up, 1, 3, 1, 1) - - self.btn_slider_down = QtWidgets.QPushButton(self.widget1) - self.btn_slider_down.setMaximumSize(btn_size) - self.btn_slider_down.setText("") - self.btn_slider_down.setObjectName("btn_slider_down") - icon_btn_slider_down = QtGui.QIcon() - icon_btn_slider_down.addPixmap( - QtGui.QPixmap(f"{icon_path}go-down1.png")) - self.btn_slider_down.setIcon(icon_btn_slider_down) - #self.btn_slider_down.setIconSize(btn_size) - self.gridLayout.addWidget(self.btn_slider_down, 2, 3, 1, 1) - - self.vertical_slider_label = QtWidgets.QLabel(self.widget1) - self.vertical_slider_label.setObjectName("vertical_slider_label") - self.gridLayout.addWidget(self.vertical_slider_label, 3, 2, 2, 1) - self.vertical_slider_label.setAlignment(Qt.AlignRight) - - # self.btn_slider_go_back = QtWidgets.QPushButton(self.widget1) - # self.btn_slider_go_back.setMaximumSize(btn_size) - # self.btn_slider_go_back.setText("") - # self.btn_slider_go_back.setObjectName("btn_slider_go_back") - # icon_btn_slider_go_back = QtGui.QIcon() - # icon_btn_slider_go_back.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-go-back.png")) - # self.btn_slider_go_back.setIcon(icon_btn_slider_go_back) - # #self.btn_slider_go_back.setIconSize(btn_size) - # self.gridLayout.addWidget(self.btn_slider_go_back, 5, 0, 1, 1) - # self.btn_slider_go_back.setEnabled(False) - - # self.btn_slider_go_forward = QtWidgets.QPushButton(self.widget1) - # self.btn_slider_go_forward.setMaximumSize(btn_size) - # self.btn_slider_go_forward.setText("") - # self.btn_slider_go_forward.setObjectName("btn_slider_go_forward") - # icon_btn_slider_go_forward = QtGui.QIcon() - # icon_btn_slider_go_forward.addPixmap( - # QtGui.QPixmap(f"{icon_path}gtk-go-forward.png")) - # self.btn_slider_go_forward.setIcon(icon_btn_slider_go_forward) - # #self.btn_slider_go_forward.setIconSize(btn_size) - # self.gridLayout.addWidget(self.btn_slider_go_forward, 5, 1, 1, 1) - # self.btn_slider_go_forward.setEnabled(False) - - # self.horizontalSlider = QtWidgets.QSlider(self.widget1) - # self.horizontalSlider.setMinimumSize(QtCore.QSize(0, 30)) - # self.horizontalSlider.setOrientation(Qt.Horizontal) - # self.horizontalSlider.setObjectName("horizontalSlider") - # self.gridLayout.addWidget(self.horizontalSlider, 5, 2, 1, 1) - # self.horizontalSlider.setEnabled(False) - - self.horizontalLayout_2.addWidget(self.splitter_3) - MainWindow_reach.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow_reach) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1123, 20)) - self.menubar.setObjectName("menubar") - MainWindow_reach.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow_reach) - self.statusbar.setObjectName("statusbar") - MainWindow_reach.setStatusBar(self.statusbar) - - self.retranslateUi(MainWindow_reach) - QtCore.QMetaObject.connectSlotsByName(MainWindow_reach) - - def retranslateUi(self, MainWindow_reach): - _translate = QtCore.QCoreApplication.translate - self.name_reach = "" - self.mainwindow_title = _translate("MainWindow_reach", "Geometry reach edit") - MainWindow_reach.setWindowTitle(self.mainwindow_title + f"{self.name_reach}") - # self.label_section_order.setText(f"{self.text_section_order}") - self.vertical_slider_value = "" - self.vertical_slider_label.setText(_translate("MainWindow_reach", "Kp = ") + f"{self.vertical_slider_value}") - self.btn_open.setToolTip(_translate("MainWindow_reach", " Import geometry")) - self.btn_add.setToolTip(_translate("MainWindow_reach", " New profile")) - self.btn_delete.setToolTip(_translate("MainWindow_reach", " Delete selected profile")) - self.btn_edit.setToolTip(_translate("MainWindow_reach", " Editer selected profile")) - - self.btn_sort_asc.setToolTip(_translate("MainWindow_reach", " Trier les profils par ordre croissant des Pk")) - self.btn_sort_desc.setToolTip(_translate("MainWindow_reach", " Trier les profils par ordre décroissant des Pk")) - self.btn_move_up.setToolTip(_translate("MainWindow_reach", " Changer l'ordre des profils (en décalant le " - "profil sélectionné vers le haut)")) - self.btn_move_down.setToolTip(_translate("MainWindow_reach", " Changer l'ordre des profils (en décalant le " - "profil sélectionné vers le bas)")) - # self.btn_end_editing.setToolTip(_translate("MainWindow_reach", " Terminer l'édition")) - self.tableView_header = [_translate("MainWindow_reach", "Name"), - _translate("MainWindow_reach", "Kp (m)"), - _translate("MainWindow_reach", "Type")] - - self.toolbar_1._actions["isometric_view"].setShortcut(_translate("MainWindow_reach", "Alt+Z")) - self.toolbar_1._actions["isometric_view"].setToolTip(_translate("MainWindow_reach", "Vue isométrique (Alt+Z)")) - - self.toolbar_2._actions["isometric_view"].setShortcut(_translate("MainWindow_reach", "Alt+E")) - self.toolbar_2._actions["isometric_view"].setToolTip(_translate("MainWindow_reach", "Vue isométrique (Alt+E)")) - - self.toolbar_3._actions["isometric_view"].setShortcut(_translate("MainWindow_reach", "Alt+R")) - self.toolbar_3._actions["isometric_view"].setToolTip(_translate("MainWindow_reach", "Vue isométrique (Alt+R)")) - - self.toolbar_1._actions["non_isometric_view"].setShortcut("Alt+S") - self.toolbar_1._actions["non_isometric_view"].setToolTip( - _translate("MainWindow_reach", "Vue globale automatique (Alt+S)")) - - self.toolbar_2._actions["non_isometric_view"].setShortcut("Alt+D") - self.toolbar_2._actions["non_isometric_view"].setToolTip( - _translate("MainWindow_reach", "Vue globale automatique (Alt+D)")) - - self.toolbar_3._actions["non_isometric_view"].setShortcut("Alt+F") - self.toolbar_3._actions["non_isometric_view"].setToolTip( - _translate("MainWindow_reach", "Vue globale automatique (Alt+F)")) From 1a47bbe19d7fa7d8c2ac41d095a52566863cdd49 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 14:54:17 +0200 Subject: [PATCH 41/44] Results: Allow to open last results from mainwindow. --- src/View/MainWindow.py | 14 +++++++++++++- src/View/RunSolver/Window.py | 7 ++++++- src/View/ui/MainWindow.ui | 13 ++++++------- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index e2a8859c..23c26b51 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -111,6 +111,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self._study = None # Results + self._last_solver = None self._last_results = None # UI @@ -150,6 +151,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): Returns: Nothing """ + logger.debug(f"Set {action} to {enable}") self.findChild(QAction, action).setEnabled(enable) def setup_sc(self): @@ -180,6 +182,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_run_solver": self.run_solver, "action_menu_sediment_layers": self.open_sediment_layers, "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, + "action_menu_results_last": self.open_last_results, ## Help "action_menu_about": self.open_about, # ToolBar action @@ -325,9 +328,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): for action in model_action: self.enable_actions(action, not no_model) - def set_results(self, results): + def set_results(self, solver, results): + self._last_solver = solver self._last_results = results + self.enable_actions("action_menu_results_last", True) + ############ # FEATURES # ############ @@ -703,6 +709,12 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): else: res.activateWindow() + def open_last_results(self): + if self._last_solver is None or self._last_results is None: + return + + self.open_solver_results(self._last_solver, self._last_results) + ######### # DEBUG # ######### diff --git a/src/View/RunSolver/Window.py b/src/View/RunSolver/Window.py index 72a65d45..62484308 100644 --- a/src/View/RunSolver/Window.py +++ b/src/View/RunSolver/Window.py @@ -204,6 +204,11 @@ class SolverLogWindow(PamhyrWindow): if self._solver.log_file() != "": self.find(QAction, "action_log_file").setEnabled(True) + # Get results + if self._results is None: + self._results = self._solver.results(self._study, self._workdir, qlog = self._output) + self._parent.set_results(self._solver, self._results) + while self._output.qsize() != 0: s = self._output.get() if type(s) is str and "[ERROR]" in s: @@ -256,7 +261,7 @@ class SolverLogWindow(PamhyrWindow): if self._results is None: self._results = self._solver.results(self._study, self._workdir, qlog = self._output) - self._parent.set_results(self._results) + self._parent.set_results(self._solver, self._results) self._parent.open_solver_results(self._solver, self._results) def log_file(self): diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index ea77d698..7578d600 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -141,12 +141,11 @@ - + - &Plots + &Results - - + @@ -178,7 +177,7 @@ - + @@ -597,12 +596,12 @@ Close - + false - Hydrograph + Visualize last results From 559d7dc00680106da6f99df96eba7bf7f52b8879 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 15:54:27 +0200 Subject: [PATCH 42/44] doc: Keep doc html and add doc window in Pamhyr2. --- .gitlab-ci.yml | 3 ++ doc/users/images/.gitkeep | 0 src/View/Doc/Window.py | 70 +++++++++++++++++++++++++++++++++++++++ src/View/MainWindow.py | 17 ++++++++++ src/View/ui/MainWindow.ui | 31 +++++++++++++++-- src/View/ui/WebView.ui | 50 ++++++++++++++++++++++++++++ 6 files changed, 169 insertions(+), 2 deletions(-) create mode 100644 doc/users/images/.gitkeep create mode 100644 src/View/Doc/Window.py create mode 100644 src/View/ui/WebView.ui diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7f2aecdf..7b58f4f4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -98,6 +98,8 @@ build-users-doc: artifacts: paths: - doc/users/documentation.pdf + - doc/users/documentation.html + - doc/users/images/ build-developers-doc: stage: build @@ -121,6 +123,7 @@ build-developers-doc: - doc/dev/documentation.pdf - doc/dev/documentation.html - doc/dev/html.tar + - doc/dev/images/ build-linux: stage: build diff --git a/doc/users/images/.gitkeep b/doc/users/images/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py new file mode 100644 index 00000000..73416322 --- /dev/null +++ b/src/View/Doc/Window.py @@ -0,0 +1,70 @@ +# Window.py -- Pamhyr +# Copyright (C) 2023 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# -*- coding: utf-8 -*- + +import os +import logging + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5.QtCore import QCoreApplication + +_translate = QCoreApplication.translate +logger = logging.getLogger() + + +from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtCore import QUrl +from PyQt5.QtWebKitWidgets import QWebView +from PyQt5.QtWebKit import QWebSettings + +class DocWindow(PamhyrWindow): + _pamhyr_ui = "WebView" + _pamhyr_name = "Doc" + + def _path_file(self, filename): + return os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "..", "..", "doc", filename + ) + ) + + def __init__(self, filename=None, + study=None, config=None, + parent=None): + super(DocWindow, self).__init__( + title = self._pamhyr_name, + study = study, + config = config, + options = [], + parent = parent + + ) + + self.setup_setting() + self.setup_url(filename) + + def setup_url(self, filename): + webView = self.find(QWebView, "webView") + webView.setUrl(QUrl(f"file://{self._path_file(filename)}")) + + def setup_setting(self): + webView = self.find(QWebView, "webView") + settings = webView.settings() + settings.setAttribute(QWebSettings.PluginsEnabled, True) + settings.setAttribute(QWebSettings.JavascriptEnabled, False) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 23c26b51..f304c144 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -56,6 +56,7 @@ from View.SolverParameters.Window import SolverParametersWindow from View.RunSolver.Window import SelectSolverWindow, SolverLogWindow from View.CheckList.Window import CheckListWindow from View.Results.Window import ResultsWindow +from View.Doc.Window import DocWindow from View.Debug.Window import ReplWindow from Model.Study import Study @@ -183,6 +184,8 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_sediment_layers": self.open_sediment_layers, "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, "action_menu_results_last": self.open_last_results, + "action_menu_Pamhyr": self.open_doc_user, + "action_menu_Pamhyr_dev": self.open_doc_dev, ## Help "action_menu_about": self.open_about, # ToolBar action @@ -715,6 +718,20 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): self.open_solver_results(self._last_solver, self._last_results) + def open_doc(self, filename): + doc = DocWindow( + filename = filename, + parent = self + ) + doc.show() + + + def open_doc_user(self): + self.open_doc("Pamhyr2-users.html") + + def open_doc_dev(self): + self.open_doc("Pamhyr2-dev.html") + ######### # DEBUG # ######### diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 7578d600..9fd3b229 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -160,8 +160,15 @@ &Help - - + + + Help + + + + + + @@ -895,6 +902,26 @@ Edit reach sediment layers + + + Doc + + + + + Pamhyr2 users + + + + + Pamhyr2 developer + + + + + Mage + + diff --git a/src/View/ui/WebView.ui b/src/View/ui/WebView.ui new file mode 100644 index 00000000..d29a0c89 --- /dev/null +++ b/src/View/ui/WebView.ui @@ -0,0 +1,50 @@ + + + MainWindow + + + + 0 + 0 + 640 + 480 + + + + MainWindow + + + + + + + + about:blank + + + + + + + + + + 0 + 0 + 640 + 22 + + + + + + + + QWebView + QWidget +
QtWebKitWidgets/QWebView
+
+
+ + +
From a77c342e699f4b8ddd8e48236274f006441167a7 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Fri, 6 Oct 2023 16:21:23 +0200 Subject: [PATCH 43/44] doc: Fix window display for version PyQt5 > 5.5 --- requirements.txt | 3 ++- src/View/Doc/Window.py | 26 ++++++++++++++------------ src/View/ui/WebView.ui | 15 +-------------- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/requirements.txt b/requirements.txt index 729dbbe4..01d9a29a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ PyQt5==5.15.4 PyQt5-Qt5==5.15.2 -PyQt5-sip==12.8.1 +PyQt5-sip==12.12.2 +PyQtWebEngine==5.15.6 pyqtgraph>=0.12.1 matplotlib>=3.4.1 numpy>=1.24.2 diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py index 73416322..fec3b3aa 100644 --- a/src/View/Doc/Window.py +++ b/src/View/Doc/Window.py @@ -27,10 +27,9 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -from PyQt5.QtWidgets import QApplication, QWidget +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout from PyQt5.QtCore import QUrl -from PyQt5.QtWebKitWidgets import QWebView -from PyQt5.QtWebKit import QWebSettings +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings class DocWindow(PamhyrWindow): _pamhyr_ui = "WebView" @@ -56,15 +55,18 @@ class DocWindow(PamhyrWindow): ) - self.setup_setting() + self.setup_web_engine() self.setup_url(filename) - def setup_url(self, filename): - webView = self.find(QWebView, "webView") - webView.setUrl(QUrl(f"file://{self._path_file(filename)}")) + def setup_web_engine(self): + vl = self.find(QVBoxLayout, "verticalLayout") + self._web_view = QWebEngineView() - def setup_setting(self): - webView = self.find(QWebView, "webView") - settings = webView.settings() - settings.setAttribute(QWebSettings.PluginsEnabled, True) - settings.setAttribute(QWebSettings.JavascriptEnabled, False) + settings = self._web_view.settings() + settings.setAttribute(QWebEngineSettings.PluginsEnabled, True) + settings.setAttribute(QWebEngineSettings.JavascriptEnabled, False) + + vl.addWidget(self._web_view) + + def setup_url(self, filename): + self._web_view.setUrl(QUrl(f"file://{self._path_file(filename)}")) diff --git a/src/View/ui/WebView.ui b/src/View/ui/WebView.ui index d29a0c89..0576f1a3 100644 --- a/src/View/ui/WebView.ui +++ b/src/View/ui/WebView.ui @@ -16,13 +16,7 @@ - - - - about:blank - - - + @@ -38,13 +32,6 @@ - - - QWebView - QWidget -
QtWebKitWidgets/QWebView
-
-
From f4d580a000d492fae4ec8d05d62ee71139239950 Mon Sep 17 00:00:00 2001 From: Pierre-Antoine Rouby Date: Mon, 9 Oct 2023 11:11:23 +0200 Subject: [PATCH 44/44] doc: Doc in format html or pdf, back/forward button and firefox button. --- packages/linux.sh | 41 ++++++++++++++++++++++---------- src/View/Doc/Window.py | 31 +++++++++++++++++++++--- src/View/MainWindow.py | 14 ++++++----- src/View/Tools/ASubWindow.py | 21 +++++++++++++--- src/View/ui/MainWindow.ui | 37 +++++++++++++++++++++++------ src/View/ui/WebView.ui | 46 +++++++++++++++++++++++++++++++++--- 6 files changed, 155 insertions(+), 35 deletions(-) diff --git a/packages/linux.sh b/packages/linux.sh index 215047e8..9a5787a7 100755 --- a/packages/linux.sh +++ b/packages/linux.sh @@ -43,18 +43,21 @@ cd $OLD_PWD echo " *** COPY DATA" -mkdir -p dist/pamhyr/View/ui -mkdir -p dist/pamhyr/View/ui/Widgets -cp -r ../src/View/ui/ressources/ dist/pamhyr/View/ui/ -cp -r ../src/View/ui/Widgets/*.ui dist/pamhyr/View/ui/Widgets/ -cp -r ../src/View/ui/*.ui dist/pamhyr/View/ui/ +mkdir -p dist/pamhyr/_internal/View/ui +mkdir -p dist/pamhyr/_internal/View/ui/Widgets +cp -r ../src/View/ui/ressources/ dist/pamhyr/_internal/View/ui/ +cp -r ../src/View/ui/Widgets/*.ui dist/pamhyr/_internal/View/ui/Widgets/ +cp -r ../src/View/ui/*.ui dist/pamhyr/_internal/View/ui/ mkdir -p dist/pamhyr/lang cp -r ../src/lang/*.qm dist/pamhyr/lang/ -cp ../VERSION dist/pamhyr/ -cp ../AUTHORS dist/pamhyr/ -cp ../LICENSE dist/pamhyr/ +mkdir dist/pamhyr/_internal/ +#cp ../VERSION dist/pamhyr/_internal/ +VERSION=$(cat ../VERSION) +echo "local-build-$VERSION" > dist/pamhyr/_internal/VERSION +cp ../AUTHORS dist/pamhyr/_internal/ +cp ../LICENSE dist/pamhyr/_internal/ mkdir -p dist/pamhyr/mage/ cp ../mage/mage dist/pamhyr/mage/ @@ -65,6 +68,18 @@ mkdir -p dist/pamhyr/tests_cases/ mkdir -p dist/pamhyr/tests_cases/Saar cp ../tests_cases/Saar/Saar.pamhyr dist/pamhyr/tests_cases/Saar/ +mkdir -p dist/pamhyr/doc/ +cp ../doc/dev/documentation.pdf dist/pamhyr/doc/Pamhyr2-dev.pdf +cp ../doc/dev/documentation.html dist/pamhyr/doc/Pamhyr2-dev.html + +cp ../doc/users/documentation.pdf dist/pamhyr/doc/Pamhyr2-users.pdf +cp ../doc/users/documentation.html dist/pamhyr/doc/Pamhyr2-users.html + +mkdir -p dist/pamhyr/doc/images +cp ../doc/users/images/* dist/pamhyr/doc/images/ +cp ../doc/dev/images/* dist/pamhyr/doc/images/ +cp ../doc/images/* dist/pamhyr/doc/images/ + echo " *** MAKE SRC PACKAGE" OLD_PWD=$PWD @@ -76,11 +91,11 @@ mv ../pamhyr-src.tar.gz ./ echo " *** MAKE BIN PACKAGE" -OLD_PWD=$PWD -cd dist/ -tar --xz -cf pamhyr-gnulinux-amd64.tar.xz pamhyr --checkpoint=.100 -cd $OLD_PWD +# OLD_PWD=$PWD +# cd dist/ +# tar --xz -cf pamhyr-gnulinux-amd64.tar.xz pamhyr --checkpoint=.100 +# cd $OLD_PWD -mv dist/pamhyr-gnulinux-amd64.tar.xz ./ +# mv dist/pamhyr-gnulinux-amd64.tar.xz ./ echo " *** DONE" diff --git a/src/View/Doc/Window.py b/src/View/Doc/Window.py index fec3b3aa..3c39dca0 100644 --- a/src/View/Doc/Window.py +++ b/src/View/Doc/Window.py @@ -18,6 +18,7 @@ import os import logging +import subprocess from View.Tools.PamhyrWindow import PamhyrWindow @@ -27,7 +28,7 @@ _translate = QCoreApplication.translate logger = logging.getLogger() -from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout +from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QAction from PyQt5.QtCore import QUrl from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings @@ -55,8 +56,18 @@ class DocWindow(PamhyrWindow): ) + self._path = self._path_file(filename) + self.setup_web_engine() - self.setup_url(filename) + self.setup_url(self._path) + self.setup_connection() + + def setup_connection(self): + self.findChild(QAction, "action_back").triggered.connect(self.back) + self.findChild(QAction, "action_forward").triggered.connect(self.forward) + + self.findChild(QAction, "action_firefox").triggered.connect(self.open_in_firefox) + def setup_web_engine(self): vl = self.find(QVBoxLayout, "verticalLayout") @@ -64,9 +75,23 @@ class DocWindow(PamhyrWindow): settings = self._web_view.settings() settings.setAttribute(QWebEngineSettings.PluginsEnabled, True) - settings.setAttribute(QWebEngineSettings.JavascriptEnabled, False) + settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) + settings.setAttribute(QWebEngineSettings.PdfViewerEnabled, True) vl.addWidget(self._web_view) def setup_url(self, filename): + logger.info(f"Open documentation : {filename}") self._web_view.setUrl(QUrl(f"file://{self._path_file(filename)}")) + + def back(self): + self._web_view.back() + + def forward(self): + self._web_view.forward() + + def open_in_firefox(self): + _ = subprocess.Popen( + f"firefox {self._path}", + shell = True + ) diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index f304c144..f8067ae6 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -184,9 +184,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_sediment_layers": self.open_sediment_layers, "action_menu_edit_reach_sediment_layers": self.open_reach_sediment_layers, "action_menu_results_last": self.open_last_results, - "action_menu_Pamhyr": self.open_doc_user, - "action_menu_Pamhyr_dev": self.open_doc_dev, ## Help + "action_menu_pamhyr_users_pdf": lambda: self.open_doc_user(ext="pdf"), + "action_menu_pamhyr_developers_pdf": lambda: self.open_doc_dev(ext="pdf"), + "action_menu_pamhyr_users_html": lambda: self.open_doc_user(ext="html"), + "action_menu_pamhyr_developers_html": lambda: self.open_doc_dev(ext="html"), "action_menu_about": self.open_about, # ToolBar action "action_toolBar_quit": self.close, @@ -726,11 +728,11 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): doc.show() - def open_doc_user(self): - self.open_doc("Pamhyr2-users.html") + def open_doc_user(self, ext="pdf"): + self.open_doc(f"Pamhyr2-users.{ext}") - def open_doc_dev(self): - self.open_doc("Pamhyr2-dev.html") + def open_doc_dev(self, ext="pdf"): + self.open_doc(f"Pamhyr2-dev.{ext}") ######### # DEBUG # diff --git a/src/View/Tools/ASubWindow.py b/src/View/Tools/ASubWindow.py index 9ac63c2d..b6adcc82 100644 --- a/src/View/Tools/ASubWindow.py +++ b/src/View/Tools/ASubWindow.py @@ -467,7 +467,12 @@ class ASubMainWindow(QMainWindow, ASubWindowFeatures, WindowToolKit): super(ASubMainWindow, self).__init__(parent=parent) if ui is not None: self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "..", "ui", f"{ui}.ui"), + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", f"{ui}.ui" + ) + ), self ) @@ -499,7 +504,12 @@ class ASubWindow(QDialog, ASubWindowFeatures, WindowToolKit): def __init__(self, name="", ui="dummy", parent=None, **kwargs): super(ASubWindow, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "..", "ui", f"{ui}.ui"), + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", f"{ui}.ui" + ) + ), self ) self.name = name @@ -530,7 +540,12 @@ class AWidget(QWidget, ASubWindowFeatures): def __init__(self, ui="", parent=None): super(AWidget, self).__init__(parent=parent) self.ui = loadUi( - os.path.join(os.path.dirname(__file__), "..", "ui", "Widgets", f"{ui}.ui"), + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + "..", "ui", "Widgets", f"{ui}.ui" + ) + ), self ) self.parent = parent diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index 9fd3b229..951dadc9 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -164,8 +164,16 @@ Help - - + + + Pamhyr2 + + + + + + + @@ -907,11 +915,6 @@ Doc - - - Pamhyr2 users - - Pamhyr2 developer @@ -922,6 +925,26 @@ Mage + + + Users (pdf) + + + + + Users (html) + + + + + Developers (pdf) + + + + + Developers (html) + + diff --git a/src/View/ui/WebView.ui b/src/View/ui/WebView.ui index 0576f1a3..7d0a41bf 100644 --- a/src/View/ui/WebView.ui +++ b/src/View/ui/WebView.ui @@ -6,8 +6,8 @@ 0 0 - 640 - 480 + 800 + 450 @@ -25,12 +25,52 @@ 0 0 - 640 + 800 22 + + + toolBar + + + TopToolBarArea + + + false + + + + + + + + Firefox + + + Open document in Firefox + + + + + + ressources/gtk-go-back.pngressources/gtk-go-back.png + + + back + + + + + + ressources/gtk-go-forward.pngressources/gtk-go-forward.png + + + forward + +