From e9b00aa148e5caf1dd2b057e8cc9fd9d1fed4dbc Mon Sep 17 00:00:00 2001
From: Pierre-Antoine Rouby <pierre-antoine.rouby@inrae.fr>
Date: Fri, 21 Jul 2023 16:11:13 +0200
Subject: [PATCH] SL: Add Edit window and fix DB save and load.

---
 src/Model/River.py                          |   1 +
 src/Model/SedimentLayer/SedimentLayer.py    |  45 +++++-
 src/View/SedimentLayers/Edit/Table.py       | 153 ++++++++++++++++++++
 src/View/SedimentLayers/Edit/UndoCommand.py |  97 +++++++++++++
 src/View/SedimentLayers/Edit/Window.py      | 150 +++++++++++++++++++
 src/View/SedimentLayers/Edit/translate.py   |  11 ++
 src/View/SedimentLayers/Window.py           |   4 +-
 src/View/ui/EditSedimentLayers.ui           |   8 +-
 8 files changed, 456 insertions(+), 13 deletions(-)

diff --git a/src/Model/River.py b/src/Model/River.py
index 2ff98bd8..59b47957 100644
--- a/src/Model/River.py
+++ b/src/Model/River.py
@@ -282,6 +282,7 @@ class River(Graph, SQLSubModel):
         objs.append(self._boundary_condition)
         objs.append(self._initial_conditions)
         objs.append(self._lateral_contribution)
+        objs.append(self._sediment_layers)
         objs.append(self._stricklers)
 
         for solver in self._parameters:
diff --git a/src/Model/SedimentLayer/SedimentLayer.py b/src/Model/SedimentLayer/SedimentLayer.py
index 65684bd8..7f8f1fa4 100644
--- a/src/Model/SedimentLayer/SedimentLayer.py
+++ b/src/Model/SedimentLayer/SedimentLayer.py
@@ -16,7 +16,6 @@ class Layer(SQLSubModel):
         super(Layer, self).__init__()
 
         self._status = status
-        self._sl = sl
 
         self._name = name
         self._type = type
@@ -27,6 +26,31 @@ class Layer(SQLSubModel):
         else:
             self.id = id
 
+        Layer._id_cnt = max(id, Layer._id_cnt+1)
+
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, name):
+        self._name = name
+
+    @property
+    def type(self):
+        return self._type
+
+    @type.setter
+    def type(self, type):
+        self._type = type
+
+    @property
+    def height(self):
+        return self._height
+
+    @height.setter
+    def height(self, height):
+        self._height = height
 
     @classmethod
     def _sql_create(cls, execute):
@@ -56,7 +80,7 @@ class Layer(SQLSubModel):
         table = execute(
             "SELECT id, ind, name, type, height " +
             "FROM sedimentary_layer_layer " +
-            f"WHERE sl = {sl.id}"
+            f"WHERE sl = {sl}"
         )
 
         for _ in table:
@@ -77,13 +101,14 @@ class Layer(SQLSubModel):
 
     def _sql_save(self, execute, data = None):
         ind = data["ind"]
+        sl = data["sl"]
 
         sql = (
             "INSERT INTO " +
             "sedimentary_layer_layer(id, ind, name, type, height, sl) "+
             "VALUES (" +
             f"{self.id}, {ind}, '{self._sql_format(self._name)}', " +
-            f"'{self._sql_format(self._type)}', '{self._height}', {self._sl.id}" +
+            f"'{self._sql_format(self._type)}', '{self._height}', {sl}" +
             ")"
         )
         execute(sql)
@@ -110,6 +135,9 @@ class SedimentLayer(SQLSubModel):
         else:
             self.id = id
 
+    def __len__(self):
+        return len(self._layers)
+
     @property
     def name(self):
         return self._name
@@ -147,7 +175,7 @@ class SedimentLayer(SQLSubModel):
         new = []
 
         table = execute(
-            "SELECT id, name, comment" +
+            "SELECT id, name, comment " +
             "FROM sedimentary_layer "
         )
 
@@ -171,11 +199,14 @@ class SedimentLayer(SQLSubModel):
             data = {}
 
         sql = (
-            "INSERT INTO sedimentary_layer (id, name) "+
-            f"VALUES ({self.id}, '{self._sql_format(self._name)}')"
+            "INSERT INTO sedimentary_layer (id, name, comment) " +
+            f"VALUES ({self.id}, '{self._sql_format(self._name)}', " +
+            f"'{self._sql_format(self._comment)}')"
         )
         execute(sql)
 
+        data["sl"] = self
+
         ind = 0
         for l in self._layers:
             data["ind"] = ind
@@ -196,7 +227,7 @@ class SedimentLayer(SQLSubModel):
         self._status.modified()
 
     def new(self, index):
-        n = Layer(status = self._status)
+        n = Layer(sl=self, status = self._status)
         self.insert(index, n)
         self._status.modified()
         return n
diff --git a/src/View/SedimentLayers/Edit/Table.py b/src/View/SedimentLayers/Edit/Table.py
index e69de29b..6bd59387 100644
--- a/src/View/SedimentLayers/Edit/Table.py
+++ b/src/View/SedimentLayers/Edit/Table.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+
+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.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)
+
+    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._sl.get(row).name
+        elif self._headers[column] == "type":
+            return self._sl.get(row).type
+        elif self._headers[column] == "height":
+            return self._sl.get(row).height
+
+        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
+
+        row = index.row()
+        column = index.column()
+
+        if self._headers[column] == "name":
+            self._undo.push(
+                SetNameCommand(
+                    self._sl, row, value
+                )
+            )
+        if self._headers[column] == "comment":
+            self._undo.push(
+                SetCommentCommand(
+                    self._sl, row, value
+                )
+            )
+
+        self.dataChanged.emit(index, index)
+        return True
+
+    def add(self, row, parent=QModelIndex()):
+        self.beginInsertRows(parent, row, row - 1)
+
+        self._undo.push(
+            AddCommand(
+                self._sl, row
+            )
+        )
+
+        self.endInsertRows()
+        self.layoutChanged.emit()
+
+    def delete(self, rows, parent=QModelIndex()):
+        self.beginRemoveRows(parent, rows[0], rows[-1])
+
+        self._undo.push(
+            DelCommand(
+                self._sl, rows
+            )
+        )
+
+        self.endRemoveRows()
+        self.layoutChanged.emit()
+
+    def move_up(self, row, parent=QModelIndex()):
+        if row <= 0:
+            return
+
+        target = row + 2
+
+        self.beginMoveRows(parent, row - 1, row - 1, parent, target)
+
+        self._undo_stack.push(
+            MoveCommand(
+                self._sl, "up", row
+            )
+        )
+
+        self.endMoveRows()
+        self.layoutChanged.emit()
+
+    def move_down(self, index, parent=QModelIndex()):
+        if row > len(self._sl):
+            return
+
+        target = row
+
+        self.beginMoveRows(parent, row + 1, row + 1, parent, target)
+
+        self._undo_stack.push(
+            MoveCommand(
+                self._sl, "down", row
+            )
+        )
+
+        self.endMoveRows()
+        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/SedimentLayers/Edit/UndoCommand.py b/src/View/SedimentLayers/Edit/UndoCommand.py
index e69de29b..9e630a6e 100644
--- a/src/View/SedimentLayers/Edit/UndoCommand.py
+++ b/src/View/SedimentLayers/Edit/UndoCommand.py
@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+from Model.SedimentLayer.SedimentLayer import SedimentLayer
+from Model.SedimentLayer.SedimentLayerList import SedimentLayerList
+
+class SetNameCommand(QUndoCommand):
+    def __init__(self, sediment_layers, index, new_value):
+        QUndoCommand.__init__(self)
+
+        self._sediment_layers = sediment_layers
+        self._index = index
+        self._old = self._sediment_layers.get(self._index).name
+        self._new = new_value
+
+    def undo(self):
+        self._sediment_layers.get(self._index).name = self._old
+
+    def redo(self):
+        self._sediment_layers.get(self._index).name = self._new
+
+class SetHeightCommand(QUndoCommand):
+    def __init__(self, sediment_layers, index, new_value):
+        QUndoCommand.__init__(self)
+
+        self._sediment_layers = sediment_layers
+        self._index = index
+        self._old = self._sediment_layers.get(self._index).height
+        self._new = new_value
+
+    def undo(self):
+        self._sediment_layers.get(self._index).height = self._old
+
+    def redo(self):
+        self._sediment_layers.get(self._index).height = self._new
+
+class AddCommand(QUndoCommand):
+    def __init__(self, sediment_layers, index):
+        QUndoCommand.__init__(self)
+
+        self._sediment_layers = sediment_layers
+        self._index = index
+        self._new = None
+
+    def undo(self):
+        self._sediment_layers.delete_i([self._index])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._sediment_layers.new(self._index)
+        else:
+            self._sediment_layers.insert(self._index, self._new)
+
+class DelCommand(QUndoCommand):
+    def __init__(self, sediment_layers, rows):
+        QUndoCommand.__init__(self)
+
+        self._sediment_layers = sediment_layers
+        self._rows = rows
+
+        self._sl_pos = []
+        for row in rows:
+            self._sl_pos.append((row, self._sediment_layers.get(row)))
+        self._sl_pos.sort()
+
+    def undo(self):
+        for row, el in self._sl_pos:
+            self._sediment_layers.insert(row, el)
+
+    def redo(self):
+        self._sediment_layers.delete_i(self._rows)
+
+class MoveCommand(QUndoCommand):
+    def __init__(self, sediment_layers, up, i):
+        QUndoCommand.__init__(self)
+
+        self._sediment_layers = sediment_layers
+        self._up = up == "up"
+        self._i = i
+
+    def undo(self):
+        if self._up:
+            self._sediment_layers.move_up(self._i)
+        else:
+            self._sediment_layers.move_down(self._i)
+
+    def redo(self):
+        if self._up:
+            self._sediment_layers.move_up(self._i)
+        else:
+            self._sediment_layers.move_down(self._i)
diff --git a/src/View/SedimentLayers/Edit/Window.py b/src/View/SedimentLayers/Edit/Window.py
index e69de29b..44100d6f 100644
--- a/src/View/SedimentLayers/Edit/Window.py
+++ b/src/View/SedimentLayers/Edit/Window.py
@@ -0,0 +1,150 @@
+# -*- coding: utf-8 -*-
+
+import logging
+
+from tools import trace, timer
+
+from View.ASubWindow import ASubMainWindow
+from View.ListedSubWindow import ListedSubWindow
+
+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.Plot.MplCanvas import MplCanvas
+
+from View.SedimentLayers.Edit.UndoCommand import *
+from View.SedimentLayers.Edit.Table import *
+from View.SedimentLayers.Edit.translate import *
+
+_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
+        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
+        )
+
+    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):
+        self._table = {}
+
+        table = self.find(QTableView, f"tableView")
+        self._table = TableModel(
+            study = self._study,
+            sl = self._sl,
+            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)
+        self.canvas.setObjectName("canvas")
+        self.plot_layout = self.find(QVBoxLayout, "verticalLayout")
+        self.plot_layout.addWidget(self.canvas)
+
+        # self.plot = PlotKPC(
+        #     canvas = self.canvas,
+        #     data = self._reach.reach,
+        #     toolbar = None,
+        #     display_current = False
+        # )
+        # 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_move_up").triggered.connect(self.delete)
+        self.find(QAction, "action_move_down").triggered.connect(self.delete)
+
+        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")
+        return list(
+            # Delete duplicate
+            set(
+                map(
+                    lambda i: i.row(),
+                    table.selectedIndexes()
+                )
+            )
+        )
+
+    def add(self):
+        rows = self.index_selected_rows()
+        if len(self._sl) == 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()
diff --git a/src/View/SedimentLayers/Edit/translate.py b/src/View/SedimentLayers/Edit/translate.py
index e69de29b..3b61d18c 100644
--- a/src/View/SedimentLayers/Edit/translate.py
+++ b/src/View/SedimentLayers/Edit/translate.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+
+from PyQt5.QtCore import QCoreApplication
+
+_translate = QCoreApplication.translate
+
+table_headers = {
+    "name": _translate("SedimentLayers", "Name"),
+    "type": _translate("SedimentLayers", "Type"),
+    "height": _translate("Sedimentlayers", "Height"),
+}
diff --git a/src/View/SedimentLayers/Window.py b/src/View/SedimentLayers/Window.py
index 22b61318..b2e8f1fa 100644
--- a/src/View/SedimentLayers/Window.py
+++ b/src/View/SedimentLayers/Window.py
@@ -30,7 +30,7 @@ from View.SedimentLayers.Table import *
 from View.Plot.MplCanvas import MplCanvas
 from View.SedimentLayers.translate import *
 
-# from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow
+from View.SedimentLayers.Edit.Window import EditSedimentLayersWindow
 
 _translate = QCoreApplication.translate
 
@@ -150,7 +150,7 @@ class SedimentLayersWindow(ASubMainWindow, ListedSubWindow):
         for row in rows:
             slw = EditSedimentLayersWindow(
                 study = self._study,
-                sl = self.__sediment_layers[row],
+                sl = self._sediment_layers.get(row),
                 parent = self
             )
             slw.show()
diff --git a/src/View/ui/EditSedimentLayers.ui b/src/View/ui/EditSedimentLayers.ui
index 7e236641..6fe4c11b 100644
--- a/src/View/ui/EditSedimentLayers.ui
+++ b/src/View/ui/EditSedimentLayers.ui
@@ -49,12 +49,12 @@
    <attribute name="toolBarBreak">
     <bool>false</bool>
    </attribute>
-   <addaction name="action_add_sediment_layer"/>
-   <addaction name="action_delete_sediment_layer"/>
+   <addaction name="action_add"/>
+   <addaction name="action_del"/>
    <addaction name="action_move_up"/>
    <addaction name="action_move_down"/>
   </widget>
-  <action name="action_add_sediment_layer">
+  <action name="action_add">
    <property name="icon">
     <iconset>
      <normaloff>ressources/gtk-add.png</normaloff>ressources/gtk-add.png</iconset>
@@ -69,7 +69,7 @@
     <string>Ctrl+N</string>
    </property>
   </action>
-  <action name="action_delete_sediment_layer">
+  <action name="action_del">
    <property name="icon">
     <iconset>
      <normaloff>ressources/gtk-remove.png</normaloff>ressources/gtk-remove.png</iconset>
-- 
GitLab