diff --git a/src/Model/BoundaryCondition/BoundaryCondition.py b/src/Model/BoundaryCondition/BoundaryCondition.py index cca0ebe9b162229850ef5d7b2edaaecabe83b09a..6978ca98602e0fe6882069f9f2b3e9f2ffefc1e8 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 209e9ac77bb1dca83576b27901ace0294a2eb9d8..327bc3a55be4770230249ab7683b832d1376cfa6 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 dfddf82cbd315c01875b9b41b3dea9efba8516c1..3cd3ec9fca90be63b098ee81f54c210b04cbb3b0 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 3c76311808dc313c94c538909641b7956a49ff14..07fdeb6f55feb2f9ef66657fa628e7c30aecf1c0 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 0000000000000000000000000000000000000000..9235bee8393635daef6a13fd28ab65e5594e6b40 --- /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 0000000000000000000000000000000000000000..7f3bbef536b4f75f8ada332fcd1dc8216fdfba6b --- /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 895ac1b6f583108492825931c66a34678645c3d2..7de37c5e1bb7e8169478e4c941cd24519d8986ea 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()