From d49091a46941261c27c7763ff23983ae795b977c Mon Sep 17 00:00:00 2001
From: Pierre-Antoine Rouby <pierre-antoine.rouby@inrae.fr>
Date: Wed, 3 May 2023 15:23:03 +0200
Subject: [PATCH] BC: Edit: Add table display.

---
 .../BoundaryCondition/BoundaryCondition.py    |  49 ++++-
 .../BoundaryConditionTypes.py                 |   1 +
 src/View/BoundaryCondition/BCUndoCommand.py   |   4 +-
 .../BoundaryConditionWindow.py                |   1 +
 src/View/BoundaryCondition/Edit/Table.py      | 173 ++++++++++++++++++
 .../BoundaryCondition/Edit/UndoCommand.py     | 147 +++++++++++++++
 src/View/BoundaryCondition/Edit/Window.py     |  94 ++++++++++
 7 files changed, 463 insertions(+), 6 deletions(-)
 create mode 100644 src/View/BoundaryCondition/Edit/Table.py
 create mode 100644 src/View/BoundaryCondition/Edit/UndoCommand.py

diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py
index cca0ebe9..6978ca98 100644
--- a/src/Model/BoundaryCondition/BoundaryCondition.py
+++ b/src/Model/BoundaryCondition/BoundaryCondition.py
@@ -15,6 +15,9 @@ class BoundaryCondition(object):
         self._header = []
         self._types = [int, float]
 
+    def __len__(self):
+        return len(self._data)
+
     @property
     def name(self):
         return self._name
@@ -46,9 +49,16 @@ class BoundaryCondition(object):
     def data(self):
         return self._data.copy()
 
+    def get_type_column(self, column):
+        if 0 <= column < 2:
+            return self._types[column]
+        return None
+
+    @property
     def _default_0(self):
         return self._types[0](0)
 
+    @property
     def _default_1(self):
         return self._types[1](0.0)
 
@@ -56,13 +66,14 @@ class BoundaryCondition(object):
         return self._data is not None
 
     def add(self, index:int):
-        value = (self.default_0, self_default_1)
+        value = (self._default_0, self._default_1)
         self._data.insert(index, value)
+        return value
 
     def insert(self, index:int, value):
         self._data.insert(index, value)
 
-    def delete(self, indexes):
+    def delete_i(self, indexes):
         self._data = list(
             map(
                 lambda e: e[1],
@@ -73,12 +84,30 @@ class BoundaryCondition(object):
             )
         )
 
-    def sort(self, _reverse):
-        self._data.sort(reverse=_reverse)
+    def delete(self, els):
+        self._data = list(
+            filter(
+                lambda e: e not in els,
+                self.data
+            )
+        )
+
+    def sort(self, _reverse=False, key=None):
+        if key is None:
+            self._data.sort(reverse=_reverse)
+        else:
+            self._data.sort(reverse=_reverse, key=key)
 
     def get_i(self, index):
         return self.data[index]
 
+    def get_range(self, _range):
+        l = []
+        for r in _range:
+            l.append(r)
+        return l
+
+
     def _set_i_c_v(self, index, column, value):
         v = list(self._data[index])
         v[column] = self._types[column](value)
@@ -103,3 +132,15 @@ class BoundaryCondition(object):
                         new._set_i_c_v(ind, j, v[i])
 
         return new
+
+    def move_up(self, index):
+        if index < len(self):
+            next = index - 1
+            d = self._data
+            d[index], d[next] = d[next], d[index]
+
+    def move_down(self, index):
+        if index >= 0:
+            prev = index + 1
+            d = self._data
+            d[index], d[prev] = d[prev], d[index]
diff --git a/src/Model/BoundaryCondition/BoundaryConditionTypes.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py
index 209e9ac7..327bc3a5 100644
--- a/src/Model/BoundaryCondition/BoundaryConditionTypes.py
+++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py
@@ -41,5 +41,6 @@ class ZOverDebit(BoundaryCondition):
         self._header = ["z", "debit"]
         self._types = [float, float]
 
+    @property
     def _default_0(self):
         return 0.0
diff --git a/src/View/BoundaryCondition/BCUndoCommand.py b/src/View/BoundaryCondition/BCUndoCommand.py
index dfddf82c..3cd3ec9f 100644
--- a/src/View/BoundaryCondition/BCUndoCommand.py
+++ b/src/View/BoundaryCondition/BCUndoCommand.py
@@ -174,5 +174,5 @@ class DuplicateCommand(QUndoCommand):
         self._lst.delete(self._bc)
 
     def redo(self):
-        for profile in self._profiles:
-            self._lst.insert(self._rows[0], profile)
+        for bc in self._bcs:
+            self._lst.insert(self._rows[0], bc)
diff --git a/src/View/BoundaryCondition/BoundaryConditionWindow.py b/src/View/BoundaryCondition/BoundaryConditionWindow.py
index 3c763118..07fdeb6f 100644
--- a/src/View/BoundaryCondition/BoundaryConditionWindow.py
+++ b/src/View/BoundaryCondition/BoundaryConditionWindow.py
@@ -319,6 +319,7 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow):
     def index_selected_rows(self):
         table = self.find(QTableView, "tableView")
         return list(
+            # Delete duplicate
             set(
                 map(
                     lambda i: i.row(),
diff --git a/src/View/BoundaryCondition/Edit/Table.py b/src/View/BoundaryCondition/Edit/Table.py
new file mode 100644
index 00000000..9235bee8
--- /dev/null
+++ b/src/View/BoundaryCondition/Edit/Table.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+
+from tools import trace, timer
+
+from View.ASubWindow import ASubMainWindow
+from View.ListedSubWindow import ListedSubWindow
+
+from PyQt5.QtCore import (
+    Qt, QVariant, QAbstractTableModel,
+    QCoreApplication, QModelIndex, pyqtSlot,
+    QRect,
+)
+
+from PyQt5.QtWidgets import (
+    QTableView, QAbstractItemView,
+)
+
+from View.BoundaryCondition.Edit.UndoCommand import (
+    SetDataCommand, AddCommand, DelCommand,
+    SortCommand, MoveCommand, PasteCommand,
+    DuplicateCommand,
+)
+
+from Model.BoundaryCondition.BoundaryConditionTypes import (
+    NotDefined, PonctualContribution,
+    TimeOverZ, TimeOverDebit, ZOverDebit
+)
+
+_translate = QCoreApplication.translate
+
+table_headers = {
+    "time": _translate("BoundaryCondition", "Time"),
+    "debit": _translate("BoundaryCondition", "Debit"),
+    "z": _translate("BoundaryCondition", "Z (m)")
+}
+
+
+class TableModel(QAbstractTableModel):
+    def __init__(self, data=None, undo=None):
+        super(QAbstractTableModel, self).__init__()
+        self._headers = data.header
+        self._data = data
+        self._undo = undo
+
+    def flags(self, index):
+        options = Qt.ItemIsEnabled | Qt.ItemIsSelectable
+        options |= Qt.ItemIsEditable
+
+        return options
+
+    def rowCount(self, parent):
+        return len(self._data)
+
+    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()
+
+        value = QVariant()
+
+        if 0 <= column < 2:
+            v = self._data.get_i(row)[column]
+            if self._data.get_type_column(column) == float:
+                value = f"{v:.4f}"
+            else:
+                # TODO: Time format
+                value = f"{v}"
+
+        return value
+
+    def headerData(self, section, orientation, role):
+        if role == Qt.ItemDataRole.DisplayRole and orientation == Qt.Orientation.Horizontal:
+            return 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
+
+        row = index.row()
+        column = index.column()
+
+        self._undo.push(
+            SetDataCommand(
+                self._data, row, column, 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._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, False
+            )
+        )
+
+        self.layoutAboutToBeChanged.emit()
+        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._data, "up", row
+            )
+        )
+
+        self.endMoveRows()
+        self.layoutChanged.emit()
+
+    def move_down(self, index, parent=QModelIndex()):
+        if row > len(self._data):
+            return
+
+        target = row
+
+        self.beginMoveRows(parent, row + 1, row + 1, parent, target)
+
+        self._undo_stack.push(
+            MoveCommand(
+                self._data, "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/BoundaryCondition/Edit/UndoCommand.py b/src/View/BoundaryCondition/Edit/UndoCommand.py
new file mode 100644
index 00000000..7f3bbef5
--- /dev/null
+++ b/src/View/BoundaryCondition/Edit/UndoCommand.py
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+
+from copy import deepcopy
+from tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition
+
+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._bc = []
+        for row in rows:
+            self._bc.append((row, self._data.get_i(row)))
+        self._bc.sort()
+
+    def undo(self):
+        for row, el in self._bc:
+            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.name
+        )
+        if self._indexes is None:
+            self._indexes = list(
+                map(
+                    lambda p: self._old.index(p),
+                    self._data.data
+                )
+            )
+            self._old = None
+
+
+class MoveCommand(QUndoCommand):
+    def __init__(self, data, up, i):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._up = up == "up"
+        self._i = i
+
+    def undo(self):
+        if self._up:
+            self._data.move_up(self._i)
+        else:
+            self._data.move_down(self._i)
+
+    def redo(self):
+        if self._up:
+            self._data.move_up(self._i)
+        else:
+            self._data.move_down(self._i)
+
+
+class PasteCommand(QUndoCommand):
+    def __init__(self, data, row, bc):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._row = row
+        self._bc = deepcopy(bc)
+        self._bc.reverse()
+
+    def undo(self):
+        self._data.delete(self._bc)
+
+    def redo(self):
+        for bc in self._bc:
+            self._data.insert(self._row, bc)
+
+
+class DuplicateCommand(QUndoCommand):
+    def __init__(self, data, rows, bc):
+        QUndoCommand.__init__(self)
+
+        self._data = data
+        self._rows = rows
+        self._bc = deepcopy(bc)
+        self._bc.reverse()
+
+    def undo(self):
+        self._data.delete(self._bc)
+
+    def redo(self):
+        for bc in self._bcs:
+            self._data.insert(self._rows[0], bc)
diff --git a/src/View/BoundaryCondition/Edit/Window.py b/src/View/BoundaryCondition/Edit/Window.py
index 895ac1b6..7de37c5e 100644
--- a/src/View/BoundaryCondition/Edit/Window.py
+++ b/src/View/BoundaryCondition/Edit/Window.py
@@ -3,6 +3,10 @@
 from View.ASubWindow import ASubMainWindow
 from View.ListedSubWindow import ListedSubWindow
 
+from PyQt5.QtGui import (
+    QKeySequence,
+)
+
 from PyQt5.QtCore import (
     Qt, QVariant, QAbstractTableModel, QCoreApplication,
 )
@@ -10,9 +14,11 @@ from PyQt5.QtCore import (
 from PyQt5.QtWidgets import (
     QDialogButtonBox, QPushButton, QLineEdit,
     QFileDialog, QTableView, QAbstractItemView,
+    QUndoStack, QShortcut, QAction, QItemDelegate,
 )
 
 from View.BoundaryCondition.translate import long_types
+from View.BoundaryCondition.Edit.Table import TableModel
 
 _translate = QCoreApplication.translate
 
@@ -26,6 +32,9 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow):
         self._title = title
 
         self.setup_window()
+        self.setup_sc()
+        self.setup_table()
+        self.setup_connections()
 
     def setup_window(self):
         if self._data is not None:
@@ -39,3 +48,88 @@ class EditBoundaryConditionWindow(ASubMainWindow, ListedSubWindow):
             self.ui.setWindowTitle(title)
         else:
             self.ui.setWindowTitle(_translate("BoundaryCondition", 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):
+        table = self.find(QTableView, "tableView")
+        self._table = TableModel(
+            data = self._data,
+            undo = self._undo_stack
+        )
+        table.setModel(self._table)
+        table.setSelectionBehavior(QAbstractItemView.SelectRows)
+        table.setAlternatingRowColors(True)
+
+    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)
+
+    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])
+
+    def delete(self):
+        rows = self.index_selected_rows()
+        if len(rows) == 0:
+            return
+
+        self._table.delete(rows)
+
+    def sort(self):
+        self._table.sort(False)
+
+    def move_up(self):
+        row = self.index_selected_row()
+        self._table.move_up(row)
+
+    def move_down(self):
+        row = self.index_selected_row()
+        self._table.move_down(row)
+
+
+    def copy(self):
+        print("TODO")
+
+    def paste(self):
+        print("TODO")
+
+    def undo(self):
+        self._table.undo()
+
+    def redo(self):
+        self._table.redo()
-- 
GitLab