From 92eac86c68fcab590efc23639974b7451f710769 Mon Sep 17 00:00:00 2001
From: Pierre-Antoine Rouby <pierre-antoine.rouby@inrae.fr>
Date: Tue, 2 Apr 2024 14:51:17 +0200
Subject: [PATCH] AddFiles, REPLine: Add undo commands.

---
 src/Model/AdditionalFile/AddFile.py     | 24 ++++++++
 src/Model/REPLine/REPLine.py            | 24 ++++++++
 src/View/AdditionalFiles/Edit/Window.py | 20 +++++--
 src/View/AdditionalFiles/List.py        | 18 ++++--
 src/View/AdditionalFiles/UndoCommand.py | 80 +++++++++++++++++++++++++
 src/View/AdditionalFiles/Window.py      | 17 +++++-
 src/View/REPLines/Edit/Window.py        | 23 +++++--
 src/View/REPLines/List.py               | 18 ++++--
 src/View/REPLines/UndoCommand.py        | 80 +++++++++++++++++++++++++
 src/View/REPLines/Window.py             | 15 ++++-
 src/View/Tools/PamhyrList.py            |  4 +-
 11 files changed, 299 insertions(+), 24 deletions(-)
 create mode 100644 src/View/AdditionalFiles/UndoCommand.py
 create mode 100644 src/View/REPLines/UndoCommand.py

diff --git a/src/Model/AdditionalFile/AddFile.py b/src/Model/AdditionalFile/AddFile.py
index 64099205..c6888132 100644
--- a/src/Model/AdditionalFile/AddFile.py
+++ b/src/Model/AdditionalFile/AddFile.py
@@ -47,6 +47,30 @@ class AddFile(SQLSubModel):
 
         AddFile._id_cnt = max(id, AddFile._id_cnt+1)
 
+    def __getitem__(self, key):
+        value = None
+
+        if key == "enabled":
+            value = self._enabled
+        elif key == "name":
+            value = self._name
+        elif key == "path":
+            value = self._path
+        elif key == "text":
+            value = self._text
+
+        return value
+
+    def __setitem__(self, key, value):
+        if key == "enabled":
+            self._enabled = value
+        elif key == "name":
+            self._name = value
+        elif key == "path":
+            self._path = value
+        elif key == "text":
+            self._text = value
+
     @property
     def enabled(self):
         return self._enabled
diff --git a/src/Model/REPLine/REPLine.py b/src/Model/REPLine/REPLine.py
index aea23d8c..17c084b1 100644
--- a/src/Model/REPLine/REPLine.py
+++ b/src/Model/REPLine/REPLine.py
@@ -47,6 +47,30 @@ class REPLine(SQLSubModel):
 
         REPLine._id_cnt = max(id, REPLine._id_cnt+1)
 
+    def __getitem__(self, key):
+        value = None
+
+        if key == "enabled":
+            value = self._enabled
+        elif key == "name":
+            value = self._name
+        elif key == "line":
+            value = self._line
+        elif key == "solvers":
+            value = self._solvers
+
+        return value
+
+    def __setitem__(self, key, value):
+        if key == "enabled":
+            self._enabled = value
+        elif key == "name":
+            self._name = value
+        elif key == "line":
+            self._line = value
+        elif key == "solvers":
+            self._solvers = value
+
     @property
     def enabled(self):
         return self._enabled
diff --git a/src/View/AdditionalFiles/Edit/Window.py b/src/View/AdditionalFiles/Edit/Window.py
index 6db27942..b6c82862 100644
--- a/src/View/AdditionalFiles/Edit/Window.py
+++ b/src/View/AdditionalFiles/Edit/Window.py
@@ -27,6 +27,9 @@ from PyQt5.QtWidgets import (
 )
 
 from View.AdditionalFiles.Translate import AddFileTranslate
+from View.AdditionalFiles.UndoCommand import (
+    SetCommand
+)
 
 logger = logging.getLogger()
 
@@ -36,7 +39,7 @@ class EditAddFileWindow(PamhyrWindow):
     _pamhyr_name = "Edit additional file"
 
     def __init__(self, study=None, config=None, add_file=None,
-                 trad=None, parent=None):
+                 trad=None, undo=None, parent=None):
 
         name = trad[self._pamhyr_name] + " - " + study.name
         super(EditAddFileWindow, self).__init__(
@@ -50,6 +53,8 @@ class EditAddFileWindow(PamhyrWindow):
         self._add_file = add_file
         self._hash_data.append(self._add_file)
 
+        self._undo = undo
+
         self.setup_values()
         self.setup_connection()
 
@@ -71,10 +76,15 @@ class EditAddFileWindow(PamhyrWindow):
         path = self.get_line_edit_text("lineEdit_path")
         text = self.get_plaintext_edit_text("plainTextEdit")
 
-        self._add_file.enabled = is_enabled
-        self._add_file.name = name
-        self._add_file.path = path
-        self._add_file.text = text
+        self._undo.push(
+            SetCommand(
+                self._add_file,
+                enabled=is_enabled,
+                name=name,
+                path=path,
+                text=text,
+            )
+        )
 
         self._propagate_update(key=Modules.ADDITIONAL_FILES)
         self.close()
diff --git a/src/View/AdditionalFiles/List.py b/src/View/AdditionalFiles/List.py
index 374ce54c..51755733 100644
--- a/src/View/AdditionalFiles/List.py
+++ b/src/View/AdditionalFiles/List.py
@@ -30,6 +30,9 @@ from PyQt5.QtGui import (
 )
 
 from View.Tools.PamhyrList import PamhyrListModel
+from View.AdditionalFiles.UndoCommand import (
+    AddCommand, DelCommand
+)
 
 logger = logging.getLogger()
 
@@ -62,10 +65,17 @@ class ListModel(PamhyrListModel):
         return QVariant()
 
     def add(self, row):
-        self._data.new(row)
+        self._undo.push(
+            AddCommand(
+                self._data, row
+            )
+        )
         self.update()
 
-    def delete(self, rows):
-        logger.info(f"add_files: delete {rows}")
-        self._data.delete_i(rows)
+    def delete(self, row):
+        self._undo.push(
+            DelCommand(
+                self._data, row
+            )
+        )
         self.update()
diff --git a/src/View/AdditionalFiles/UndoCommand.py b/src/View/AdditionalFiles/UndoCommand.py
new file mode 100644
index 00000000..ba417ff6
--- /dev/null
+++ b/src/View/AdditionalFiles/UndoCommand.py
@@ -0,0 +1,80 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2024  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 tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+
+class SetCommand(QUndoCommand):
+    def __init__(self, add_file, **kwargs):
+        QUndoCommand.__init__(self)
+
+        self._add_file = add_file
+        self._new = kwargs
+        self._old = None
+
+    def undo(self):
+        f = self._add_file
+
+        for key in self._old:
+            f[key] = self._old[key]
+
+    def redo(self):
+        f = self._add_file
+
+        if self._old is None:
+            self._old = {}
+            for key in self._new:
+                self._old[key] = f[key]
+
+        for key in self._new:
+            f[key] = self._new[key]
+
+class AddCommand(QUndoCommand):
+    def __init__(self, files, row):
+        QUndoCommand.__init__(self)
+
+        self._files = files
+        self._row = row
+        self._new = None
+
+    def undo(self):
+        self._files.delete([self._new])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._files.new(self._row)
+        else:
+            self._files.insert(self._row, self._new)
+
+class DelCommand(QUndoCommand):
+    def __init__(self, files, row):
+        QUndoCommand.__init__(self)
+
+        self._files = files
+        self._row = row
+        self._old = self._files.get(row)
+
+    def undo(self):
+        self._files.insert(self._row, self._old)
+
+    def redo(self):
+        self._files.delete_i([self._row])
diff --git a/src/View/AdditionalFiles/Window.py b/src/View/AdditionalFiles/Window.py
index 6cd84203..c6c22522 100644
--- a/src/View/AdditionalFiles/Window.py
+++ b/src/View/AdditionalFiles/Window.py
@@ -28,7 +28,6 @@ from View.AdditionalFiles.List import ListModel
 from View.AdditionalFiles.Translate import AddFileTranslate
 from View.AdditionalFiles.Edit.Window import EditAddFileWindow
 
-
 class AddFileListWindow(PamhyrWindow):
     _pamhyr_ui = "AdditionalFileList"
     _pamhyr_name = "Additional files"
@@ -55,6 +54,8 @@ class AddFileListWindow(PamhyrWindow):
         self._list = ListModel(
             list_view=lst,
             data=self._study.river.additional_files,
+            undo=self._undo_stack,
+            trad=self._trad,
         )
 
     def setup_connections(self):
@@ -80,7 +81,10 @@ class AddFileListWindow(PamhyrWindow):
 
     def delete(self):
         rows = self.selected_rows()
-        self._list.delete(rows)
+        if len(rows) == 0:
+            return
+
+        self._list.delete(rows[0])
 
     def edit(self):
         rows = self.selected_rows()
@@ -99,6 +103,15 @@ class AddFileListWindow(PamhyrWindow):
                 config=self._config,
                 add_file=add_file,
                 trad=self._trad,
+                undo=self._undo_stack,
                 parent=self,
             )
             win.show()
+
+    def _undo(self):
+        self._undo_stack.undo()
+        self.update()
+
+    def _redo(self):
+        self._undo_stack.redo()
+        self.update()
diff --git a/src/View/REPLines/Edit/Window.py b/src/View/REPLines/Edit/Window.py
index 120a0a29..473ab63d 100644
--- a/src/View/REPLines/Edit/Window.py
+++ b/src/View/REPLines/Edit/Window.py
@@ -27,6 +27,9 @@ from PyQt5.QtWidgets import (
 )
 
 from View.REPLines.Translate import REPLineTranslate
+from View.REPLines.UndoCommand import (
+    SetCommand
+)
 
 logger = logging.getLogger()
 
@@ -36,7 +39,7 @@ class EditREPLineWindow(PamhyrDialog):
     _pamhyr_name = "Edit Mage REP lines"
 
     def __init__(self, study=None, config=None, rep_line=None,
-                 trad=None, parent=None):
+                 trad=None, undo=None, parent=None):
 
         name = trad[self._pamhyr_name] + " - " + study.name
         super(EditREPLineWindow, self).__init__(
@@ -50,6 +53,8 @@ class EditREPLineWindow(PamhyrDialog):
         self._rep_line = rep_line
         self._hash_data.append(self._rep_line)
 
+        self._undo = undo
+
         self.setup_values()
 
     def setup_values(self):
@@ -61,11 +66,17 @@ class EditREPLineWindow(PamhyrDialog):
         is_enabled = self.get_check_box("checkBox_enabled")
         name = self.get_line_edit_text("lineEdit_name")
         line = self.get_line_edit_text("lineEdit_line")
-
-        self._rep_line.enabled = is_enabled
-        self._rep_line.name = name
-        self._rep_line.line = line
-        self._rep_line.solvers = set()
+        solvers = set()
+
+        self._undo.push(
+            SetCommand(
+                self._rep_line,
+                enabled=is_enabled,
+                name=name,
+                line=line,
+                solvers=solvers,
+            )
+        )
 
         self._propagate_update(key=Modules.ADDITIONAL_FILES)
         self.close()
diff --git a/src/View/REPLines/List.py b/src/View/REPLines/List.py
index a591a4b1..b1e1752c 100644
--- a/src/View/REPLines/List.py
+++ b/src/View/REPLines/List.py
@@ -30,6 +30,9 @@ from PyQt5.QtGui import (
 )
 
 from View.Tools.PamhyrList import PamhyrListModel
+from View.REPLines.UndoCommand import (
+    AddCommand, DelCommand
+)
 
 logger = logging.getLogger()
 
@@ -62,10 +65,17 @@ class ListModel(PamhyrListModel):
         return QVariant()
 
     def add(self, row):
-        self._data.new(row)
+        self._undo.push(
+            AddCommand(
+                self._data, row
+            )
+        )
         self.update()
 
-    def delete(self, rows):
-        logger.info(f"add_files: delete {rows}")
-        self._data.delete_i(rows)
+    def delete(self, row):
+        self._undo.push(
+            DelCommand(
+                self._data, row
+            )
+        )
         self.update()
diff --git a/src/View/REPLines/UndoCommand.py b/src/View/REPLines/UndoCommand.py
new file mode 100644
index 00000000..7618c207
--- /dev/null
+++ b/src/View/REPLines/UndoCommand.py
@@ -0,0 +1,80 @@
+# UndoCommand.py -- Pamhyr
+# Copyright (C) 2024  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 tools import trace, timer
+
+from PyQt5.QtWidgets import (
+    QMessageBox, QUndoCommand, QUndoStack,
+)
+
+
+class SetCommand(QUndoCommand):
+    def __init__(self, rep_line, **kwargs):
+        QUndoCommand.__init__(self)
+
+        self._rep_line = rep_line
+        self._new = kwargs
+        self._old = None
+
+    def undo(self):
+        f = self._rep_line
+
+        for key in self._old:
+            f[key] = self._old[key]
+
+    def redo(self):
+        f = self._rep_line
+
+        if self._old is None:
+            self._old = {}
+            for key in self._new:
+                self._old[key] = f[key]
+
+        for key in self._new:
+            f[key] = self._new[key]
+
+class AddCommand(QUndoCommand):
+    def __init__(self, lines, row):
+        QUndoCommand.__init__(self)
+
+        self._lines = lines
+        self._row = row
+        self._new = None
+
+    def undo(self):
+        self._lines.delete([self._new])
+
+    def redo(self):
+        if self._new is None:
+            self._new = self._lines.new(self._row)
+        else:
+            self._lines.insert(self._row, self._new)
+
+class DelCommand(QUndoCommand):
+    def __init__(self, lines, row):
+        QUndoCommand.__init__(self)
+
+        self._lines = lines
+        self._row = row
+        self._old = self._lines.get(row)
+
+    def undo(self):
+        self._lines.insert(self._row, self._old)
+
+    def redo(self):
+        self._lines.delete_i([self._row])
diff --git a/src/View/REPLines/Window.py b/src/View/REPLines/Window.py
index 92cb015b..a7bc4ce0 100644
--- a/src/View/REPLines/Window.py
+++ b/src/View/REPLines/Window.py
@@ -55,6 +55,7 @@ class REPLineListWindow(PamhyrWindow):
         self._list = ListModel(
             list_view=lst,
             data=self._study.river.rep_lines,
+            undo=self._undo_stack,
         )
 
     def setup_connections(self):
@@ -80,7 +81,10 @@ class REPLineListWindow(PamhyrWindow):
 
     def delete(self):
         rows = self.selected_rows()
-        self._list.delete(rows)
+        if len(rows) < 0:
+            return
+
+        self._list.delete(rows[0])
 
     def edit(self):
         rows = self.selected_rows()
@@ -99,6 +103,15 @@ class REPLineListWindow(PamhyrWindow):
                 config=self._config,
                 rep_line=rep_line,
                 trad=self._trad,
+                undo=self._undo_stack,
                 parent=self,
             )
             win.show()
+
+    def _undo(self):
+        self._undo_stack.undo()
+        self.update()
+
+    def _redo(self):
+        self._undo_stack.redo()
+        self.update()
diff --git a/src/View/Tools/PamhyrList.py b/src/View/Tools/PamhyrList.py
index 7acc7ee0..709672b0 100644
--- a/src/View/Tools/PamhyrList.py
+++ b/src/View/Tools/PamhyrList.py
@@ -73,11 +73,11 @@ class PamhyrListModel(QAbstractListModel):
 
     def undo(self):
         self._undo.undo()
-        self.layoutChanged.emit()
+        self.update()
 
     def redo(self):
         self._undo.redo()
-        self.layoutChanged.emit()
+        self.update()
 
     def update(self):
         self.layoutChanged.emit()
-- 
GitLab