diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index b3ebd29a52df5cbb0ed65ebb889c859d4fb4e53b..cf844ff696ad36414d783e424d8c7ccfacb29036 100644 --- a/src/Model/BoundaryCondition/BoundaryCondition.py +++ b/src/Model/BoundaryCondition/BoundaryCondition.py @@ -1,30 +1,7 @@ # -*- coding: utf-8 -*- -from copy import copy - from Model.Except import NotImplementedMethodeError -class BoundaryConditionList(object): - def __init__(self): - self._data = [] - - def __len__(self): - return len(self._data) - - def __getitem__(self, index): - return self._data[index] - - def __setitem__(self, index, value): - self._data[index] = value - - def __copy__(self): - new = BoundaryConditionList() - new._data = self._data.copy() - return new - - def copy(self): - return copy(self) - class BoundaryCondition(object): def __init__(self, name:str = ""): super(BoundaryCondition, self).__init__() @@ -67,6 +44,12 @@ class BoundaryCondition(object): def data(self): return self._data.copy() + def _default_0(self): + return self._types[0](0) + + def _default_1(self): + return self._types[1](0.0) + def is_define(self): return self._data is not None @@ -117,9 +100,3 @@ class BoundaryCondition(object): new._set_i_c_v(ind, j, v[i]) return new - - def _default_0(self): - return self._types[0](0) - - def _default_1(self): - return self._types[1](0.0) diff --git a/src/Model/BoundaryCondition/BoundaryConditionList.py b/src/Model/BoundaryCondition/BoundaryConditionList.py new file mode 100644 index 0000000000000000000000000000000000000000..d5c2d9add71b5067addaacff8e6cec358661bc8c --- /dev/null +++ b/src/Model/BoundaryCondition/BoundaryConditionList.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +from copy import copy +from tools import trace, timer + +from Model.Except import NotImplementedMethodeError + +from Model.BoundaryCondition.BoundaryConditionTypes import ( + NotDefined, + PonctualContribution, + TimeOverZ, TimeOverDebit, ZOverDebit +) + +class BoundaryConditionList(list): + def __init__(self): + super(BoundaryConditionList, self).__init__() + + def new(self, index): + n = NotDefined() + self.insert(index, n) + return n + + def delete(self, bcs): + for bc in bcs: + self.remove(bc) + + def delete_i(self, indexes): + bcs = map( + lambda x: x[1], + filter( + lambda x: x[0] in indexes, + enumerate(self) + ) + ) + + for bc in bcs: + self.remove(bc) + + def move_up(self, index): + if index < len(self): + next = index - 1 + + self[index], self[next] = self[next], self[index] + + def move_down(self, index): + if index >= 0: + prev = index + 1 + + self[index], self[prev] = self[prev], self[index] + + def __copy__(self): + new = BoundaryConditionList() + + for bc in self: + new.append(bc) + + return new + + def __deepcopy__(self): + new = BoundaryConditionList() + + for bc in self: + new.append(deepcopy(bc)) + + return new + + def copy(self): + return copy(self) diff --git a/src/Model/BoundaryCondition/Types.py b/src/Model/BoundaryCondition/BoundaryConditionTypes.py similarity index 82% rename from src/Model/BoundaryCondition/Types.py rename to src/Model/BoundaryCondition/BoundaryConditionTypes.py index 78df831eeec4772e146a706e4d002c7e575ecf3b..b7515d432f7067f1a2e4de92f549233492ac7409 100644 --- a/src/Model/BoundaryCondition/Types.py +++ b/src/Model/BoundaryCondition/BoundaryConditionTypes.py @@ -4,12 +4,14 @@ from Model.Except import NotImplementedMethodeError from Model.BoundaryCondition.BoundaryCondition import BoundaryCondition -BC_types = [ - "PC", - "TZ", - "TD", - "ZD" -] + +class NotDefined(BoundaryCondition): + def __init__(self, name:str = ""): + super(NotDefined, self).__init__(name=name) + + self._type = "ND" + self._header = ["", ""] + self._types = [str, str] class PonctualContribution(BoundaryCondition): def __init__(self, name:str = ""): diff --git a/src/Model/River.py b/src/Model/River.py index 51f9b2ef8c88ce0570f4a2ce3d549aa4dcbf0b29..dad5e824573c413c4fd33eb2ae8a8310a71ffab9 100644 --- a/src/Model/River.py +++ b/src/Model/River.py @@ -8,8 +8,10 @@ from Model.Geometry.Profile import Profile from Model.Geometry.Reach import Reach from Model.BoundaryCondition.BoundaryCondition import ( - BoundaryCondition, - BoundaryConditionList, + BoundaryCondition +) +from Model.BoundaryCondition.BoundaryConditionList import ( + BoundaryConditionList ) diff --git a/src/View/BoundaryCondition/BCUndoCommand.py b/src/View/BoundaryCondition/BCUndoCommand.py new file mode 100644 index 0000000000000000000000000000000000000000..9bcefc9c493e49a83a18e260a4c77e4506ecd076 --- /dev/null +++ b/src/View/BoundaryCondition/BCUndoCommand.py @@ -0,0 +1,175 @@ +# -*- 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 +from Model.BoundaryCondition.BoundaryConditionList import BoundaryConditionList + +class SetNameCommand(QUndoCommand): + def __init__(self, lst, index, new_value): + QUndoCommand.__init__(self) + + self._lst = lst + self._index = index + self._old = self._lst[self._index].name + self._new = new_value + + def undo(self): + self._lst[self._index].name = self._old + + def redo(self): + self._lst[self._index].name = self._new + +class SetNodeCommand(QUndoCommand): + def __init__(self, lst, index, node): + QUndoCommand.__init__(self) + + self._lst = lst + self._index = index + self._old = self.lst[index].node + self._new = node + + def undo(self): + self._lst[self._index].node = self._old + + def redo(self): + self._lst[self._index].node = self._new + +class SetTypeCommand(QUndoCommand): + def __init__(self, lst, index, _type): + QUndoCommand.__init__(self) + + self._lst = lst + self._index = index + self._type = _type + self._old = self.lst[index] + self._new = self.lst[index].convert(self._type) + + def undo(self): + self._lst[self._index] = self._old + + def redo(self): + self._lst[self._index] = self._new + +class AddCommand(QUndoCommand): + def __init__(self, lst, index): + QUndoCommand.__init__(self) + + self._lst = lst + self._index = index + self._new = None + + def undo(self): + self._lst.delete_i([self._index]) + + def redo(self): + if self._new is None: + self._new = self._lst.new(self._index) + else: + self._lst.insert(self._index, self._new) + +class DelCommand(QUndoCommand): + def __init__(self, lst, rows): + QUndoCommand.__init__(self) + + self._lst = lst + self._rows = rows + + self._bc = [] + for row in rows: + self._bc.append((row, self._lst[row])) + self._bc.sort() + + def undo(self): + for row, el in self._bc: + self._lst.insert(row, el) + + def redo(self): + self._lst.delete_i(self._rows) + +class SortCommand(QUndoCommand): + def __init__(self, lst, _reverse): + QUndoCommand.__init__(self) + + self._lst = lst + self._reverse = _reverse + + self.old = self._lst.copy() + self._indexes = None + + def undo(self): + ll = self._lst.copy() + self._lst.sort( + key=lambda x: self._indexes[ll.index(x)] + ) + + def redo(self): + self._lst.sort(self._reverse) + if self._indexes is None: + self._indexes = list( + map( + lambda p: self._old.index(p), + self._lst + ) + ) + self._old = None + + +class MoveCommand(QUndoCommand): + def __init__(self, lst, up, i): + QUndoCommand.__init__(self) + + self._lst = lst + self._up = up == "up" + self._i = i + + def undo(self): + if self._up: + self._lst.move_up(self._i) + else: + self._lst.move_down(self._i) + + def redo(self): + if self._up: + self._lst.move_up(self._i) + else: + self._lst.move_down(self._i) + + +class PasteCommand(QUndoCommand): + def __init__(self, lst, row, bc): + QUndoCommand.__init__(self) + + self._lst = lst + self._row = row + self._bc = deepcopy(bc) + self._bc.reverse() + + def undo(self): + self._lst.delete(self._bc) + + def redo(self): + for bc in self._bc: + self._lst.insert(self._row, bc) + + +class DuplicateCommand(QUndoCommand): + def __init__(self, lst, rows, bc): + QUndoCommand.__init__(self) + + self._lst = lst + self._rows = rows + self._bc = deepcopy(bc) + self._bc.reverse() + + def undo(self): + self._lst.delete(self._bc) + + def redo(self): + for profile in self._profiles: + self._lst.insert(self._rows[0], profile) diff --git a/src/View/BoundaryCondition/BoundaryConditionWindow.py b/src/View/BoundaryCondition/BoundaryConditionWindow.py index 70f8b346660c0cc21446d019b76eb76a80ce6c85..ab23890125986b92fe326116bdcde0c4aa5fd474 100644 --- a/src/View/BoundaryCondition/BoundaryConditionWindow.py +++ b/src/View/BoundaryCondition/BoundaryConditionWindow.py @@ -1,23 +1,41 @@ # -*- coding: utf-8 -*- +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, + QCoreApplication, QModelIndex, pyqtSlot, ) from PyQt5.QtWidgets import ( QDialogButtonBox, QPushButton, QLineEdit, QFileDialog, QTableView, QAbstractItemView, + QUndoStack, QShortcut, QAction, QItemDelegate, + QComboBox, ) +from View.BoundaryCondition.BCUndoCommand import ( + SetNameCommand, SetNodeCommand, SetTypeCommand, + AddCommand, DelCommand, SortCommand, + MoveCommand, PasteCommand, DuplicateCommand, +) +from Model.BoundaryCondition.BoundaryConditionTypes import ( + NotDefined, PonctualContribution, + TimeOverZ, TimeOverDebit, ZOverDebit +) from View.BoundaryCondition.EditBoundaryConditionWindow import EditBoundaryConditionWindow _translate = QCoreApplication.translate long_types = { + "ND": _translate("BoundaryCondition", "Not defined"), "PC": _translate("BoundaryCondition", "Ponctual contribution"), "TZ": _translate("BoundaryCondition", "Time over Z"), "TD": _translate("BoundaryCondition", "Time over Debit"), @@ -30,11 +48,63 @@ table_headers = { "node": _translate("BoundaryCondition", "Node") } +BC_types = { + "ND": NotDefined, + "PC": PonctualContribution, + "TZ": TimeOverZ, + "TD": TimeOverDebit, + "ZD": ZOverDebit +} + + +class ComboBoxDelegate(QItemDelegate): + def __init__(self, data=None, mode="type", parent=None): + super(ComboBoxDelegate, self).__init__(parent) + + self._data = data + self._mode = mode + + def createEditor(self, parent, option, index): + self.editor = QComboBox(parent) + + if self._mode == "type": + self.editor.addItems(BC_types.keys()) + else: + self.editor.addItems( + ["Not associate"] + + 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 and 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(QAbstractTableModel): - def __init__(self, data=None): + def __init__(self, data=None, undo=None): super(QAbstractTableModel, self).__init__() self._headers = list(table_headers.keys()) self._data = data + self._undo = undo self._lst = self._data.boundary_condition def flags(self, index): @@ -57,12 +127,12 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - return self.lst[row].name + return self._lst[row].name elif self._headers[column] == "type": - t = self.lst[row].bctype + t = self._lst[row].bctype return long_types[t] elif self._headers[column] == "node": - n = self.lst[row].node + n = self._lst[row].node if n is None: return "-" return n.name @@ -83,15 +153,104 @@ class TableModel(QAbstractTableModel): column = index.column() if self._headers[column] == "name": - self.lst[row].name = value + self._undo.push( + SetNameCommand( + self._lst, row, value + ) + ) elif self._headers[column] == "type": - self.lst[row].bctype = value + self._undo.push( + SetTypeCommand( + self._lst, row, BC_types[value] + ) + ) elif self._headers[column] == "node": - self.lst[row].node = value + self._undo.push( + SetNodeCommand( + self._lst, row, self._data.node(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._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() + + def sort(self, _reverse, parent=QModelIndex()): + self.layoutAboutToBeChanged.emit() + + self._undo_stack.push( + SortCommand( + self._lst, 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._lst, "up", row + ) + ) + + self.endMoveRows() + self.layoutChanged.emit() + + def move_down(self, index, parent=QModelIndex()): + if row > len(self._lst): + return + + target = row + + self.beginMoveRows(parent, row + 1, row + 1, parent, target) + + self._undo_stack.push( + MoveCommand( + self._lst, "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() + class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): def __init__(self, title="BoundaryConditions", study=None, parent=None): @@ -100,14 +259,109 @@ class BoundaryConditionWindow(ASubMainWindow, ListedSubWindow): ) self._study = study + self._lst = self._study.river.boundary_condition + self.setup_sc() + self.setup_table() + self.setup_connections() + + self.ui.setWindowTitle(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._study.river + data = self._study.river, + undo = self._undo_stack ) table.setModel(self._table) - self.ui.setWindowTitle(title) + self._delegate_type = ComboBoxDelegate( + data = self._study.river, + mode = "type" + ) + self._delegate_node = ComboBoxDelegate( + data = self._study.river, + mode = "node" + ) + + table.setItemDelegateForColumn( + 1, self._delegate_type + ) + table.setItemDelegateForColumn( + 2, self._delegate_node + ) + + 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_edit").triggered.connect(self.edit) + self.find(QAction, "action_sort").triggered.connect(self.sort) + + self.undo_sc.activated.connect(self.undo) + self.redo_sc.activated.connect(self.redo) + self.copy_sc.activated.connect(self.copy) + self.paste_sc.activated.connect(self.paste) + + + def 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( + set( + map( + lambda i: i.row(), + table.selectedIndexes() + ) + ) + ) + + def add(self): + if len(self._lst) == 0: + self._table.add(0) + else: + row = self.index_selected_row() + self._table.add(row) + + def delete(self): + rows = self.index_selected_rows() + 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() def edit(self): win = EditBoundaryConditionWindow(data=None, parent=self) diff --git a/src/tools.py b/src/tools.py index 6ae8996b133570388631ba32ff365fea0674dec6..902f2ea9988610965e4c08df42e7db6bddfd7dc0 100644 --- a/src/tools.py +++ b/src/tools.py @@ -90,7 +90,7 @@ def trace(func): value = func(*args, **kwargs) t = time.ctime() - r = f"{head}[{t}] Return {func.__module__}.{Fore.GREEN}{func.__qualname__}{Style.RESET_ALL}" + r = f"{head}[{t}] Return {func.__module__}.{Fore.GREEN}{func.__qualname__}{Style.RESET_ALL}: {value}" print(r) return value