diff --git a/src/Model/Reservoir/Reservoir.py b/src/Model/Reservoir/Reservoir.py new file mode 100644 index 0000000000000000000000000000000000000000..97a6311318e23e56d52c931f5272ad037ad376d2 --- /dev/null +++ b/src/Model/Reservoir/Reservoir.py @@ -0,0 +1,268 @@ +# Reservoir.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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from Model.Tools.PamhyrDB import SQLSubModel + +logger = logging.getLogger() + + +class Reservoir(SQLSubModel): + _sub_classes = [] + _id_cnt = 0 + + def __init__(self, id: int = -1, name: str = "", + status=None): + super(Reservoir, self).__init__() + + self._status = status + + if id == -1: + self.id = Reservoir._id_cnt + else: + self.id = id + + self._name = name + self._node = None + self._data = [] + + Reservoir._id_cnt = max(Reservoir._id_cnt + 1, self.id) + + @classmethod + def _db_create(cls, execute): + execute(""" + CREATE TABLE reservoir( + id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + node INTEGER, + FOREIGN KEY(node) REFERENCES river_node(id) + ) + """) + + execute(""" + CREATE TABLE reservoir_data( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + ind INTEGER NOT NULL, + elevation REAL NOT NULL, + surface REAL NOT NULL, + reservoir INTEGER, + FOREIGN KEY(reservoir) REFERENCES reservoir(id) + ) + """) + + return cls._create_submodel(execute) + + @classmethod + def _db_update(cls, execute, version): + major, minor, release = version.strip().split(".") + if major == minor == "0": + if int(release) < 5: + cls._db_create(execute) + + return True + + @classmethod + def _db_load(cls, execute, data=None): + new = [] + + table = execute( + "SELECT id, name, node " + + "FROM reservoir " + ) + + for row in table: + id = row[0] + name = row[1] + node_id = row[2] + new_reservoir = cls(id, name, status=data["status"]) + + new_reservoir._node = None + if node_id != -1: + new_reservoir._node = next(filter(lambda n: n.id == node_id, data["nodes"])) + + new_data = [] + table = execute( + "SELECT elevation, surface " + + "FROM reservoir_data " + + f"WHERE reservoir = {id} " + + "ORDER BY ind ASC" + ) + for t in table: + new_data.append((t[0], t[1])) + + new_reservoir._data = new_data + + new.append(new_reservoir) + + return new + + def _db_save(self, execute, data=None): + + execute(f"DELETE FROM reservoir WHERE id = {self.id}") + execute(f"DELETE FROM reservoir_data WHERE reservoir = {self.id}") + + node_id = -1 + if self._node is not None: + node_id = self._node.id + + sql = ( + "INSERT INTO " + + "reservoir(id, name, node) " + + "VALUES (" + + f"{self.id}, '{self._db_format(self._name)}', " + + f"{node_id}" + + ")" + ) + execute(sql) + + ind = 0 + for d in self._data: + sql = ( + "INSERT INTO " + + "reservoir_data(ind, elevation, surface, reservoir) " + + f"VALUES ({ind}, '{d[0]}', {d[1]}, {self.id})" + ) + execute(sql) + ind += 1 + + return True + + def __len__(self): + return len(self._data) + + @property + def name(self): + return self._name + + @name.setter + def name(self, name): + self._name = name + self._status.modified() + + @property + def node(self): + return self._node + + @node.setter + def node(self, node): + self._node = node + self._status.modified() + + def has_node(self): + return self._node is not None + + @property + def data(self): + return self._data.copy() + + @property + def _default_elevation(self): + return 0.0 + + @property + def _default_surface(self): + return 0.0 + + def is_define(self): + return len(self._data) != 0 + + def new_from_data(self, data): + + try: + new_0 = float(data[0]) + new_1 = float(data[1]) + except Exception as e: + logger.error(e) + new_0 = None + new_1 = None + + return (new_0, new_1) + + def add(self, index: int): + value = (self._default_elevation, self._default_surface) + self._data.insert(index, value) + self._status.modified() + return value + + def insert(self, index: int, value): + self._data.insert(index, value) + self._status.modified() + + def delete_i(self, indexes): + self._data = list( + map( + lambda e: e[1], + filter( + lambda e: e[0] not in indexes, + enumerate(self.data) + ) + ) + ) + self._status.modified() + + def delete(self, els): + self._data = list( + filter( + lambda e: e not in els, + self.data + ) + ) + self._status.modified() + + def sort(self, _reverse=False, key=None): + if key is None: + self._data.sort(reverse=_reverse) + else: + self._data.sort(reverse=_reverse, key=key) + self._status.modified() + + def get_i(self, index): + return self.data[index] + + def get_range(self, _range): + lst = [] + for r in _range: + lst.append(r) + return lst + + def _set_i_c_v(self, index, column, value): + v = list(self._data[index]) + v[column] = value + self._data[index] = tuple(v) + self._status.modified() + + def set_i_elevation(self, index: int, value): + self._set_i_c_v(index, 0, value) + + def set_i_surface(self, index: int, value): + self._set_i_c_v(index, 1, value) + + def move_up(self, index): + if index < len(self): + next = index - 1 + d = self._data + d[index], d[next] = d[next], d[index] + self._status.modified() + + def move_down(self, index): + if index >= 0: + prev = index + 1 + d = self._data + d[index], d[prev] = d[prev], d[index] + self._status.modified() diff --git a/src/Model/Reservoir/ReservoirList.py b/src/Model/Reservoir/ReservoirList.py new file mode 100644 index 0000000000000000000000000000000000000000..60d2b98ef5989433b5e4cc1cd8066eff2e8e3c0b --- /dev/null +++ b/src/Model/Reservoir/ReservoirList.py @@ -0,0 +1,87 @@ +# ReservoirList.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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from copy import copy +from tools import trace, timer + +from Model.Tools.PamhyrList import PamhyrModelList +from Model.Reservoir.Reservoir import Reservoir + + +class ReservoirList(PamhyrModelList): + _sub_classes = [ + Reservoir, + ] + + @classmethod + def _db_load(cls, execute, data=None): + new = cls(status=data['status']) + + new._lst = Reservoir._db_load( + execute, data + ) + + return new + + def _db_save(self, execute, data=None): + execute("DELETE FROM reservoir") + execute("DELETE FROM reservoir_data") + + if data is None: + data = {} + + for reservoir in self._lst: + reservoir._db_save(execute, data=data) + + return True + + def new(self, index): + r = Reservoir(status=self._status) + self._lst.insert(index, r) + self._status.modified() + return r + + def __copy__(self): + new = ReservoirList() + + new._lst = self._lst.copy() + + return new + + def __deepcopy__(self): + new = ReservoirList() + + new._lst = self._lst.deepcopy() + + return new + + def copy(self): + return copy(self) + + def get_assoc_to_node(self, node): + assoc = list( + filter( + lambda i: i.node is node, + self._lst + ) + ) + + if len(assoc) > 0: + return assoc[0] + + return None diff --git a/src/Model/River.py b/src/Model/River.py index 5047b43578bb2c2e136a06902f6bf24fd4324c25..84836625f83f2d7e69031a744d07b3a6aa5803e6 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -36,6 +36,7 @@ from Model.Stricklers.StricklersList import StricklersList from Model.Friction.FrictionList import FrictionList from Model.SolverParameters.SolverParametersList import SolverParametersList from Model.SedimentLayer.SedimentLayerList import SedimentLayerList +from Model.Reservoir.ReservoirList import ReservoirList from Solver.Solvers import solver_type_list @@ -217,6 +218,7 @@ class River(Graph, SQLSubModel): StricklersList, SolverParametersList, SedimentLayerList, + ReservoirList, ] def __init__(self, status=None): @@ -234,6 +236,7 @@ class River(Graph, SQLSubModel): self._stricklers = StricklersList(status=self._status) self._parameters = {} self._sediment_layers = SedimentLayerList(status=self._status) + self._reservoir = ReservoirList(status=self._status) @classmethod def _db_create(cls, execute): @@ -294,6 +297,12 @@ class River(Graph, SQLSubModel): data ) + # Reservoir + new._reservoir = ReservoirList._db_load( + execute, + data + ) + # Parameters new._parameters = SolverParametersList._db_load( execute, @@ -309,6 +318,7 @@ class River(Graph, SQLSubModel): objs.append(self._lateral_contribution) objs.append(self._sediment_layers) objs.append(self._stricklers) + objs.append(self._reservoir) for solver in self._parameters: objs.append(self._parameters[solver]) @@ -349,6 +359,10 @@ class River(Graph, SQLSubModel): return ret[0] + @property + def reservoir(self): + return self._reservoir + @property def parameters(self): return self._parameters diff --git a/src/Model/Study.py b/src/Model/Study.py index e9ccdb41b2527e1c5ba6b6e6929e4d5f3b2031be..b163088b8d93312297d38c7ae469f800233269df 100644 --- a/src/Model/Study.py +++ b/src/Model/Study.py @@ -41,7 +41,7 @@ class Study(SQLModel): def __init__(self, filename=None, init_new=True): # Metadata - self._version = "0.0.4" + self._version = "0.0.5" self.creation_date = datetime.now() self.last_modification_date = datetime.now() self.last_save_date = datetime.now() diff --git a/src/Solver/Mage.py b/src/Solver/Mage.py index 3a785e4e022743ee23753b21598bbf2389b3cc03..a62a66650a9111f915a51f02748c49f5b774eea1 100644 --- a/src/Solver/Mage.py +++ b/src/Solver/Mage.py @@ -412,6 +412,36 @@ class Mage(CommandLineSolver): files.append(f"{name}.INI") return files + @timer + def _export_CAS(self, study, repertory, qlog, name="0"): + files = [] + + reservoirs = study.river.reservoir.lst + if len(reservoirs) == 0: + return files + + if qlog is not None: + qlog.put("Export CAS file") + + with mage_file_open(os.path.join(repertory, f"{name}.CAS"), "w+") as f: + files.append(f"{name}.CAS") + + for reservoir in reservoirs: + reservoir.sort() + node = reservoir.node + name = f"{node.id:3}".replace(" ", "x") + f.write(f"* {node.name} ({name}) Reservoir\n") + f.write(f"${name}\n") + f.write(f"*{'Elev(m)':>9}|{'Area(ha)':>10}\n") + + for d in reservoir.data: + v0 = d[0] + v1 = d[1] + + f.write(f"{v0:>10.3f}{v1:>10.3f}\n") + + return files + @timer def _export_REP(self, study, repertory, files, qlog, name="0"): if qlog is not None: @@ -653,6 +683,7 @@ class Mage8(Mage): self._export_bound_cond(study, repertory, qlog, name=name) files = files + self._export_RUG(study, repertory, qlog, name=name) files = files + self._export_INI(study, repertory, qlog, name=name) + files = files + self._export_CAS(study, repertory, qlog, name=name) self._export_REP(study, repertory, files, qlog, name=name) return True diff --git a/src/View/MainWindow.py b/src/View/MainWindow.py index 57ad99960079bf920343ff80ced700d74ca89247..04ed5f94267aad9aad4e2419b6aea1fab59b47f1 100644 --- a/src/View/MainWindow.py +++ b/src/View/MainWindow.py @@ -47,6 +47,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.Reservoir.Window import ReservoirWindow from View.LateralContribution.Window import LateralContributionWindow from View.InitialConditions.Window import InitialConditionsWindow from View.Stricklers.Window import StricklersWindow @@ -101,7 +102,7 @@ define_model_action = [ "action_menu_boundary_conditions", "action_menu_initial_conditions", "action_menu_edit_friction", "action_menu_edit_lateral_contribution", "action_menu_run_solver", "action_menu_sediment_layers", - "action_menu_edit_reach_sediment_layers" + "action_menu_edit_reach_sediment_layers", "action_menu_edit_reservoirs" ] action = ( @@ -191,6 +192,7 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): "action_menu_edit_network": self.open_network, "action_menu_edit_geometry": self.open_geometry, "action_menu_boundary_conditions": self.open_boundary_cond, + "action_menu_edit_reservoirs": self.open_reservoir, "action_menu_initial_conditions": self.open_initial_conditions, "action_menu_edit_friction": self.open_frictions, "action_menu_edit_lateral_contribution": self.open_lateral_contrib, @@ -616,6 +618,16 @@ class ApplicationWindow(QMainWindow, ListedSubWindow, WindowToolKit): bound = BoundaryConditionWindow(study=self._study, parent=self) bound.show() + def open_reservoir(self): + if self.sub_window_exists( + ReservoirWindow, + data=[self._study, None] + ): + return + + reservoir = ReservoirWindow(study=self._study, parent=self) + reservoir.show() + def open_lateral_contrib(self): if self.sub_window_exists( LateralContributionWindow, diff --git a/src/View/Reservoir/Edit/Plot.py b/src/View/Reservoir/Edit/Plot.py new file mode 100644 index 0000000000000000000000000000000000000000..8f8f49b858228c34d3b143ec17bea797afa9f146 --- /dev/null +++ b/src/View/Reservoir/Edit/Plot.py @@ -0,0 +1,101 @@ +# Plot.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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from datetime import datetime + +from tools import timer, trace +from View.Tools.PamhyrPlot import PamhyrPlot + +from PyQt5.QtCore import ( + QCoreApplication +) + +from View.BoundaryCondition.Edit.translate import BCETranslate + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class Plot(PamhyrPlot): + def __init__(self, mode="time", data=None, + trad=None, canvas=None, toolbar=None, + parent=None): + super(Plot, self).__init__( + canvas=canvas, + trad=trad, + data=data, + toolbar=toolbar, + parent=parent + ) + + self._table_headers = self._trad.get_dict("table_headers") + self._mode = mode + + @timer + def draw(self): + self.canvas.axes.cla() + self.canvas.axes.grid(color='grey', linestyle='--', linewidth=0.5) + + if len(self.data) == 0: + self._init = False + return + + # Plot data + x = list(map(lambda v: v[0], self.data.data)) + y = list(map(lambda v: v[1], self.data.data)) + self._line, = self.canvas.axes.plot( + x, y, + color='r', lw=1., + markersize=5, marker='+', + picker=30, + ) + + # Plot label + #header = self.data.header + self.canvas.axes.set_xlabel( + self._table_headers["z"], color='black', fontsize=10 + ) + self.canvas.axes.set_ylabel( + self._table_headers["Area"], color='black', fontsize=10 + ) + + self.canvas.axes.autoscale_view(True, True, True) + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() + # self.toolbar.update() + + self._init = True + + @timer + def update(self, ind=None): + if not self._init: + self.draw() + return + + x = list(map(lambda v: v[0], self.data.data)) + y = list(map(lambda v: v[1], self.data.data)) + + self._line.set_data(x, y) + + self.canvas.axes.relim() + self.canvas.axes.autoscale() + self.canvas.figure.tight_layout() + self.canvas.figure.canvas.draw_idle() diff --git a/src/View/Reservoir/Edit/Table.py b/src/View/Reservoir/Edit/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..21e0f91bc95bbdcaf2103685df6a22354851df64 --- /dev/null +++ b/src/View/Reservoir/Edit/Table.py @@ -0,0 +1,141 @@ +# Table.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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from datetime import date, time, datetime, timedelta + +from tools import trace, timer + +from View.Tools.PamhyrTable import PamhyrTableModel + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, QTime, QDateTime, +) + +from PyQt5.QtWidgets import ( + QTableView, QAbstractItemView, QSpinBox, QItemDelegate, +) + +from View.Reservoir.Edit.UndoCommand import ( + SetDataCommand, AddCommand, DelCommand, + SortCommand, PasteCommand, +) + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class TableModel(PamhyrTableModel): + def data(self, index, role): + if role == Qt.TextAlignmentRole: + return Qt.AlignHCenter | Qt.AlignVCenter + + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + value = QVariant() + + if 0 <= column < 2: + v = self._data.get_i(row)[column] + value = f"{v:.4f}" + + return value + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + + try: + self._undo.push( + SetDataCommand( + self._data, row, column, float(value) + ) + ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._data, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._data, rows + ) + ) + + self.endRemoveRows() + + def sort(self, _reverse, parent=QModelIndex()): + self.layoutAboutToBeChanged.emit() + + self._undo.push( + SortCommand( + self._data, _reverse + ) + ) + + self.layoutAboutToBeChanged.emit() + self.layoutChanged.emit() + + def paste(self, row, data): + if len(data) == 0: + return + + self.layoutAboutToBeChanged.emit() + + self._undo.push( + PasteCommand( + self._data, row, + list( + map( + lambda d: self._data.new_from_data(d), + data + ) + ) + ) + ) + + self.layoutAboutToBeChanged.emit() + self.layoutChanged.emit() diff --git a/src/View/Reservoir/Edit/Translate.py b/src/View/Reservoir/Edit/Translate.py new file mode 100644 index 0000000000000000000000000000000000000000..f7660885faa2fe83bbce0ad9550efb8397152b21 --- /dev/null +++ b/src/View/Reservoir/Edit/Translate.py @@ -0,0 +1,35 @@ +# 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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +from View.Reservoir.Translate import ReservoirTranslate + +_translate = QCoreApplication.translate + + +class EditReservoirTranslate(ReservoirTranslate): + def __init__(self): + super(EditReservoirTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "z": _translate("Reservoir", "Elevation (m)"), + "Area": _translate("Reservoir", "Area (hectare)"), + } diff --git a/src/View/Reservoir/Edit/UndoCommand.py b/src/View/Reservoir/Edit/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..22b7a459657530f8edca01a44d972b746a75b4bc --- /dev/null +++ b/src/View/Reservoir/Edit/UndoCommand.py @@ -0,0 +1,135 @@ +# UndoCommand.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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.Reservoir.Reservoir import Reservoir + +logger = logging.getLogger() + + +class SetDataCommand(QUndoCommand): + def __init__(self, data, index, column, new_value): + QUndoCommand.__init__(self) + + self._data = data + self._index = index + self._column = column + self._old = self._data.get_i(self._index)[self._column] + self._new = new_value + + def undo(self): + self._data._set_i_c_v(self._index, self._column, self._old) + + def redo(self): + self._data._set_i_c_v(self._index, self._column, self._new) + + +class AddCommand(QUndoCommand): + def __init__(self, data, index): + QUndoCommand.__init__(self) + + self._data = data + self._index = index + self._new = None + + def undo(self): + self._data.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._data.add(self._index) + else: + self._data.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, data, rows): + QUndoCommand.__init__(self) + + self._data = data + self._rows = rows + + self._point = [] + for row in rows: + self._point.append((row, self._data.get_i(row))) + self._point.sort() + + def undo(self): + for row, el in self._point: + self._data.insert(row, el) + + def redo(self): + self._data.delete_i(self._rows) + + +class SortCommand(QUndoCommand): + def __init__(self, data, _reverse): + QUndoCommand.__init__(self) + + self._data = data + self._reverse = _reverse + + self._old = self._data.data + self._indexes = None + + def undo(self): + ll = self._data.data + self._data.sort( + key=lambda x: self._indexes[ll.index(x)] + ) + + def redo(self): + self._data.sort( + _reverse=self._reverse, + key=lambda x: x[0] + ) + if self._indexes is None: + self._indexes = list( + map( + lambda p: self._old.index(p), + self._data.data + ) + ) + self._old = None + + +class PasteCommand(QUndoCommand): + def __init__(self, data, row, hs): + QUndoCommand.__init__(self) + + self._data = data + self._row = row + self._h = hs + self._h.reverse() + + def undo(self): + self._data.delete_i( + range(self._row, self._row + len(self._h)) + ) + + def redo(self): + for h in self._h: + self._data.insert(self._row, h) diff --git a/src/View/Reservoir/Edit/Window.py b/src/View/Reservoir/Edit/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..34724bea63ab44e4e37aec5557fd02a32a2bd9f5 --- /dev/null +++ b/src/View/Reservoir/Edit/Window.py @@ -0,0 +1,217 @@ +# 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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from tools import timer, trace + +from View.Tools.PamhyrWindow import PamhyrWindow +from View.Tools.PamhyrWidget import PamhyrWidget + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5 import QtCore +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, QCoreApplication, + pyqtSlot, pyqtSignal, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QHeaderView, QDoubleSpinBox, QVBoxLayout, +) + +from View.Tools.Plot.PamhyrCanvas import MplCanvas +from View.Tools.Plot.PamhyrToolbar import PamhyrPlotToolbar + +from View.Reservoir.Edit.Translate import EditReservoirTranslate +from View.Reservoir.Edit.Table import TableModel +from View.Reservoir.Edit.Plot import Plot + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class EditReservoirWindow(PamhyrWindow): + _pamhyr_ui = "Reservoir" + _pamhyr_name = "Edit Reservoir" + + def __init__(self, data=None, study=None, config=None, parent=None): + self._data = data + trad = EditReservoirTranslate() + + 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("Reservoir", "Not associated")) + name = ( + _translate("Edit Reservoir", self._pamhyr_name) + + f" - {study.name} " + + f" - {self._data.name} ({self._data.id}) " + + f"({node_name})" + ) + + super(EditReservoirWindow, self).__init__( + title=name, + study=study, + config=config, + trad=trad, + parent=parent + ) + + self._hash_data.append(data) + + self.setup_table() + self.setup_plot() + self.setup_connections() + + def setup_table(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, + table_headers=table_headers, + editable_headers=table_headers, + #editable_headers=self._data.header, + delegates={ + #"time": self._delegate_time, + }, + data=self._data, + undo=self._undo_stack, + opt_data=self._study.time_system + ) + + table.setModel(self._table) + table.setSelectionBehavior(QAbstractItemView.SelectRows) + table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + table.setAlternatingRowColors(True) + + def setup_plot(self): + self.canvas = MplCanvas(width=5, height=4, dpi=100) + self.canvas.setObjectName("canvas") + self.toolbar = PamhyrPlotToolbar( + self.canvas, self + ) + self.verticalLayout.addWidget(self.toolbar) + self.verticalLayout.addWidget(self.canvas) + + self.plot = Plot( + canvas=self.canvas, + data=self._data, + mode=self._study.time_system, + trad=self._trad, + toolbar=self.toolbar, + ) + self.plot.draw() + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self.find(QAction, "action_sort").triggered.connect(self.sort) + + self._table.dataChanged.connect(self.update) + + def update(self): + self.plot.update() + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + return table.selectionModel()\ + .selectedRows()[0]\ + .row() + + def index_selected_rows(self): + table = self.find(QTableView, "tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._data) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + self.plot.update() + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + self.plot.update() + + def sort(self): + self._table.sort(False) + self.plot.update() + + def _copy(self): + rows = self.index_selected_rows() + + table = [] + #table.append(self._data.header) + table.append(self._trad.get_dict("table_headers")) + + data = self._data.data + for row in rows: + table.append(list(data[row])) + + self.copyTableIntoClipboard(table) + + def _paste(self): + header, data = self.parseClipboardTable() + + logger.debug(f"paste: h:{header}, d:{data}") + + if len(data) == 0: + return + + row = 0 + rows = self.index_selected_rows() + if len(rows) != 0: + row = rows[0] + + self._table.paste(row, data) + self.plot.update() + + def _undo(self): + self._table.undo() + self.plot.update() + self.widget_update() + + def _redo(self): + self._table.redo() + self.plot.update() + self.widget_update() diff --git a/src/View/Reservoir/Table.py b/src/View/Reservoir/Table.py new file mode 100644 index 0000000000000000000000000000000000000000..66cdae82109f2aa212ea68b249ad310fb6e984be --- /dev/null +++ b/src/View/Reservoir/Table.py @@ -0,0 +1,170 @@ +# Table.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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging +import traceback + +from tools import trace, timer + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, +) + +from View.Tools.PamhyrTable import PamhyrTableModel + +from View.Reservoir.UndoCommand import ( + SetNameCommand, SetNodeCommand, + AddCommand, DelCommand, PasteCommand, +) + +logger = logging.getLogger() + +_translate = QCoreApplication.translate + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, trad=None, parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._trad = trad + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + self.editor.addItems( + [_translate("Reservoir", "Not associated")] + + self._data.nodes_names() + ) + + self.editor.setCurrentText(index.data(Qt.DisplayRole)) + return self.editor + + def setEditorData(self, editor, index): + value = index.data(Qt.DisplayRole) + self.editor.currentTextChanged.connect(self.currentItemChanged) + + def setModelData(self, editor, model, index): + text = str(editor.currentText()) + model.setData(index, text) + editor.close() + editor.deleteLater() + + def updateEditorGeometry(self, editor, option, index): + r = QRect(option.rect) + if self.editor.windowFlags() & Qt.Popup: + if editor.parent() is not None: + r.setTopLeft(self.editor.parent().mapToGlobal(r.topLeft())) + editor.setGeometry(r) + + @pyqtSlot() + def currentItemChanged(self): + self.commitData.emit(self.sender()) + + +class TableModel(PamhyrTableModel): + def _setup_lst(self): + self._lst = self._data.reservoir + + def rowCount(self, parent): + return len(self._lst) + + def data(self, index, role): + if role != Qt.ItemDataRole.DisplayRole: + return QVariant() + + row = index.row() + column = index.column() + + if self._headers[column] == "name": + return self._lst.get(row).name + elif self._headers[column] == "node": + n = self._lst.get(row).node + if n is None: + return _translate("Reservoir", "Not associated") + return n.name + + return QVariant() + + def setData(self, index, value, role=Qt.EditRole): + if not index.isValid() or role != Qt.EditRole: + return False + + row = index.row() + column = index.column() + + try: + if self._headers[column] == "name": + self._undo.push( + SetNameCommand( + self._lst, row, value + ) + ) + elif self._headers[column] == "node": + self._undo.push( + SetNodeCommand( + self._lst, row, self._data.node(value) + ) + ) + except Exception as e: + logger.info(e) + logger.debug(traceback.format_exc()) + + self.dataChanged.emit(index, index) + return True + + def add(self, row, parent=QModelIndex()): + self.beginInsertRows(parent, row, row - 1) + + self._undo.push( + AddCommand( + self._lst, row + ) + ) + + self.endInsertRows() + self.layoutChanged.emit() + + def delete(self, rows, parent=QModelIndex()): + self.beginRemoveRows(parent, rows[0], rows[-1]) + + self._undo.push( + DelCommand( + self._lst, rows + ) + ) + + self.endRemoveRows() + self.layoutChanged.emit() + + def undo(self): + self._undo.undo() + self.layoutChanged.emit() + + def redo(self): + self._undo.redo() + self.layoutChanged.emit() diff --git a/src/View/Reservoir/Translate.py b/src/View/Reservoir/Translate.py new file mode 100644 index 0000000000000000000000000000000000000000..556c9381e0c12575b3b360914e460fb2701b0694 --- /dev/null +++ b/src/View/Reservoir/Translate.py @@ -0,0 +1,33 @@ +# 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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from PyQt5.QtCore import QCoreApplication + +from View.Tools.PamhyrTranslate import PamhyrTranslate + +_translate = QCoreApplication.translate + + +class ReservoirTranslate(PamhyrTranslate): + def __init__(self): + super(ReservoirTranslate, self).__init__() + + self._sub_dict["table_headers"] = { + "name": _translate("Reservoir", "Name"), + "node": _translate("Reservoir", "Node") + } diff --git a/src/View/Reservoir/UndoCommand.py b/src/View/Reservoir/UndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..078c31d316484ced6a57b611a666f0402272eac8 --- /dev/null +++ b/src/View/Reservoir/UndoCommand.py @@ -0,0 +1,127 @@ +# UndoCommand.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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +from copy import deepcopy +from tools import trace, timer + +from PyQt5.QtWidgets import ( + QMessageBox, QUndoCommand, QUndoStack, +) + +from Model.Reservoir.Reservoir import Reservoir +from Model.Reservoir.ReservoirList import ReservoirList + + +class SetNameCommand(QUndoCommand): + def __init__(self, reservoir_lst, index, new_value): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + self._index = index + self._old = self._reservoir_lst.get(self._index).name + self._new = str(new_value) + + def undo(self): + self._reservoir_lst.get(self._index).name = self._old + + def redo(self): + self._reservoir_lst.get(self._index).name = self._new + + +class SetNodeCommand(QUndoCommand): + def __init__(self, reservoir_lst, index, node): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + self._index = index + self._old = self._reservoir_lst.get(self._index).node + self._new = node + self._prev_assoc_to_node = self._reservoir_lst.get_assoc_to_node(node) + + def _previous_assoc_node(self, node): + if self._prev_assoc_to_node is not None: + self._prev_assoc_to_node.node = node + + def undo(self): + self._reservoir_lst.get(self._index).node = self._old + self._previous_assoc_node(self._new) + + def redo(self): + self._reservoir_lst.get(self._index).node = self._new + self._previous_assoc_node(None) + + +class AddCommand(QUndoCommand): + def __init__(self, reservoir_lst, index): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + + self._index = index + self._new = None + + def undo(self): + self._reservoir_lst.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._reservoir_lst.new(self._index) + else: + self._reservoir_lst.insert(self._index, self._new) + + +class DelCommand(QUndoCommand): + def __init__(self, reservoir_lst, rows): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + + self._rows = rows + + self._reservoir = [] + for row in rows: + self._reservoir.append((row, self._reservoir_lst.get(row))) + self._reservoir.sort() + + def undo(self): + for row, el in self._reservoir: + self._reservoir_lst.insert(row, el) + + def redo(self): + self._reservoir_lst.delete_i(self._rows) + + +class PasteCommand(QUndoCommand): + def __init__(self, reservoir_lst, row, reservoir): + QUndoCommand.__init__(self) + + self._reservoir_lst = reservoir_lst + + self._row = row + self._reservoir = deepcopy(reservoir) + self._reservoir.reverse() + + def undo(self): + self._reservoir_lst.delete_i( + self._tab, + range(self._row, self._row + len(self._reservoir)) + ) + + def redo(self): + for r in self._reservoir: + self._reservoir_lst.insert(self._row, r) diff --git a/src/View/Reservoir/Window.py b/src/View/Reservoir/Window.py new file mode 100644 index 0000000000000000000000000000000000000000..147c05d157563d79859b80936123c280b63ddc1e --- /dev/null +++ b/src/View/Reservoir/Window.py @@ -0,0 +1,173 @@ +# 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 <https://www.gnu.org/licenses/>. + +# -*- coding: utf-8 -*- + +import logging + +from tools import trace, timer + +from View.Tools.PamhyrWindow import PamhyrWindow + +from PyQt5.QtGui import ( + QKeySequence, +) + +from PyQt5.QtCore import ( + Qt, QVariant, QAbstractTableModel, + QCoreApplication, QModelIndex, pyqtSlot, + QRect, +) + +from PyQt5.QtWidgets import ( + QDialogButtonBox, QPushButton, QLineEdit, + QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, QVBoxLayout, QHeaderView, QTabWidget, +) + +from View.Reservoir.Table import ( + TableModel, ComboBoxDelegate +) + +from View.Network.GraphWidget import GraphWidget +from View.Reservoir.Translate import ReservoirTranslate +from View.Reservoir.Edit.Window import EditReservoirWindow + +_translate = QCoreApplication.translate + +logger = logging.getLogger() + + +class ReservoirWindow(PamhyrWindow): + _pamhyr_ui = "ReservoirList" + _pamhyr_name = "Reservoir" + + def __init__(self, study=None, config=None, parent=None): + name = self._pamhyr_name + " - " + study.name + + super(ReservoirWindow, self).__init__( + title=name, + study=study, + config=config, + trad=ReservoirTranslate(), + parent=parent + ) + + self._reservoir_lst = self._study.river.reservoir + + self.setup_table() + self.setup_graph() + self.setup_connections() + + def setup_table(self): + self._table = None + + self._delegate_node = ComboBoxDelegate( + trad=self._trad, + data=self._study.river, + parent=self + ) + + table = self.find(QTableView, f"tableView") + self._table = TableModel( + table_view=table, + table_headers=self._trad.get_dict("table_headers"), + editable_headers=["name", "node"], + delegates={ + "node": self._delegate_node, + }, + trad=self._trad, + data=self._study.river, + undo=self._undo_stack, + ) + + def setup_graph(self): + self.graph_widget = GraphWidget( + self._study.river, + min_size=None, size=(200, 200), + only_display=True, + parent=self + ) + self.graph_layout = self.find(QVBoxLayout, "verticalLayout") + self.graph_layout.addWidget(self.graph_widget) + + def setup_connections(self): + self.find(QAction, "action_add").triggered.connect(self.add) + self.find(QAction, "action_delete").triggered.connect(self.delete) + self.find(QAction, "action_edit").triggered.connect(self.edit) + + def index_selected_row(self): + table = self.find(QTableView, "tableView") + return table.selectionModel()\ + .selectedRows()[0]\ + .row() + + def index_selected_rows(self): + table = self.find(QTableView, "tableView") + return list( + # Delete duplicate + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + rows = self.index_selected_rows() + if len(self._reservoir_lst) == 0 or len(rows) == 0: + self._table.add(0) + else: + self._table.add(rows[0]) + + def delete(self): + rows = self.index_selected_rows() + if len(rows) == 0: + return + + self._table.delete(rows) + + def _copy(self): + logger.info("TODO: copy") + + def _paste(self): + logger.info("TODO: paste") + + def _undo(self): + self._table.undo() + + def _redo(self): + self._table.redo() + + def edit(self): + rows = self.index_selected_rows() + for row in rows: + data = self._reservoir_lst.get(row) + + if self.sub_window_exists( + EditReservoirWindow, + data=[self._study, None, data] + ): + continue + + win = EditReservoirWindow( + data=data, + study=self._study, + parent=self + ) + win.show() diff --git a/src/View/ui/MainWindow.ui b/src/View/ui/MainWindow.ui index f33e6dcfb8892aa3c87f5fc6f0dc2ada6e31d474..9476708ceb809a9bf54dd129f6bc2330648a93ef 100644 --- a/src/View/ui/MainWindow.ui +++ b/src/View/ui/MainWindow.ui @@ -131,6 +131,7 @@ <addaction name="action_menu_initial_conditions"/> <addaction name="action_menu_edit_friction"/> <addaction name="action_menu_edit_lateral_contribution"/> + <addaction name="action_menu_edit_reservoirs"/> </widget> <widget class="QMenu" name="menu_results"> <property name="title"> @@ -492,10 +493,7 @@ <string>Boundary conditions and one-time contributions</string> </property> <property name="font"> - <font> - <weight>50</weight> - <bold>false</bold> - </font> + <font/> </property> </action> <action name="action_menu_initial_conditions"> @@ -600,11 +598,7 @@ <string>Visualize the last results</string> </property> <property name="font"> - <font> - <family>Ubuntu</family> - <weight>50</weight> - <bold>false</bold> - </font> + <font/> </property> </action> <action name="action_plot_limnigram"> @@ -932,6 +926,14 @@ <string>Developers (html)</string> </property> </action> + <action name="action_menu_edit_reservoirs"> + <property name="text"> + <string>Reservoirs</string> + </property> + <property name="toolTip"> + <string>Edit reservoirs</string> + </property> + </action> </widget> <resources/> <connections> diff --git a/src/View/ui/Reservoir.ui b/src/View/ui/Reservoir.ui new file mode 100644 index 0000000000000000000000000000000000000000..d2dc390256c8dc70b8df08f12c872223930c3fb3 --- /dev/null +++ b/src/View/ui/Reservoir.ui @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>600</height> + </rect> + </property> + <property name="windowTitle"> + <string>MainWindow</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QTableView" name="tableView"> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + </widget> + <widget class="QWidget" name="verticalLayoutWidget"> + <layout class="QVBoxLayout" name="verticalLayout"/> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>800</width> + <height>22</height> + </rect> + </property> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <widget class="QToolBar" name="toolBar"> + <property name="windowTitle"> + <string>toolBar</string> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="action_add"/> + <addaction name="action_delete"/> + <addaction name="action_sort"/> + </widget> + <action name="action_add"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset> + </property> + <property name="text"> + <string>Add</string> + </property> + <property name="toolTip"> + <string>Add a new point</string> + </property> + </action> + <action name="action_delete"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset> + </property> + <property name="text"> + <string>Delete</string> + </property> + <property name="toolTip"> + <string>Delete points</string> + </property> + </action> + <action name="action_edit"> + <property name="icon"> + <iconset> + <normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset> + </property> + <property name="text"> + <string>Edit</string> + </property> + <property name="toolTip"> + <string>Edit elevation/surface law</string> + </property> + </action> + <action name="action_sort"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-sort-ascending.png</normaloff>ressources/gtk-sort-ascending.png</iconset> + </property> + <property name="text"> + <string>Sort</string> + </property> + <property name="toolTip"> + <string>Sort points by elevation</string> + </property> + </action> + </widget> + <resources/> + <connections/> +</ui> diff --git a/src/View/ui/ReservoirList.ui b/src/View/ui/ReservoirList.ui new file mode 100644 index 0000000000000000000000000000000000000000..db3b822d84f932c11e38bcc2b63864d58eeeaeb3 --- /dev/null +++ b/src/View/ui/ReservoirList.ui @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>849</width> + <height>600</height> + </rect> + </property> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + <property name="windowTitle"> + <string>MainWindow</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QGridLayout" name="gridLayout"> + <item row="0" column="0"> + <widget class="QSplitter" name="splitter"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <widget class="QWidget" name="verticalLayoutWidget"> + <layout class="QVBoxLayout" name="_1"> + <item> + <widget class="QTableView" name="tableView"> + <property name="minimumSize"> + <size> + <width>300</width> + <height>0</height> + </size> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QWidget" name="verticalLayoutWidget_2"> + <layout class="QVBoxLayout" name="verticalLayout"/> + </widget> + </widget> + </item> + </layout> + </widget> + <widget class="QMenuBar" name="menubar"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>849</width> + <height>22</height> + </rect> + </property> + </widget> + <widget class="QStatusBar" name="statusbar"/> + <widget class="QToolBar" name="toolBar"> + <property name="windowTitle"> + <string>toolBar</string> + </property> + <attribute name="toolBarArea"> + <enum>TopToolBarArea</enum> + </attribute> + <attribute name="toolBarBreak"> + <bool>false</bool> + </attribute> + <addaction name="action_add"/> + <addaction name="action_delete"/> + <addaction name="action_edit"/> + </widget> + <action name="action_add"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset> + </property> + <property name="text"> + <string>Add</string> + </property> + <property name="toolTip"> + <string>Add a new reservoir</string> + </property> + </action> + <action name="action_delete"> + <property name="icon"> + <iconset> + <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset> + </property> + <property name="text"> + <string>Delete</string> + </property> + <property name="toolTip"> + <string>Delete reservoirs</string> + </property> + </action> + <action name="action_edit"> + <property name="icon"> + <iconset> + <normaloff>ressources/edit.png</normaloff>ressources/edit.png</iconset> + </property> + <property name="text"> + <string>Edit</string> + </property> + <property name="toolTip"> + <string>Edit reservoir law</string> + </property> + </action> + </widget> + <resources/> + <connections/> +</ui>