From 06934b80680b4cef967c7b8a87580a7d7759a01c Mon Sep 17 00:00:00 2001 From: Jaime Resano Date: Wed, 12 Jan 2022 13:22:05 +0100 Subject: Add editable tree model example Ported from C++. Done-with: Friedemann Kleint Done-with: Cristian Maureira-Fredes Pick-to: 6.2 Change-Id: I9336016daec01b0b0486fcd39fac20c6bbd08970 Reviewed-by: Cristian Maureira-Fredes --- .../itemviews/editabletreemodel/default.txt | 40 ++++ .../editabletreemodel/doc/editabletreemodel.png | Bin 0 -> 111556 bytes .../editabletreemodel/doc/editabletreemodel.rst | 10 + .../editabletreemodel/editabletreemodel.pyproject | 7 + .../widgets/itemviews/editabletreemodel/main.py | 51 +++++ .../itemviews/editabletreemodel/mainwindow.py | 200 +++++++++++++++++ .../itemviews/editabletreemodel/treeitem.py | 131 +++++++++++ .../itemviews/editabletreemodel/treemodel.py | 241 +++++++++++++++++++++ 8 files changed, 680 insertions(+) create mode 100644 examples/widgets/itemviews/editabletreemodel/default.txt create mode 100644 examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.png create mode 100644 examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.rst create mode 100644 examples/widgets/itemviews/editabletreemodel/editabletreemodel.pyproject create mode 100644 examples/widgets/itemviews/editabletreemodel/main.py create mode 100644 examples/widgets/itemviews/editabletreemodel/mainwindow.py create mode 100644 examples/widgets/itemviews/editabletreemodel/treeitem.py create mode 100644 examples/widgets/itemviews/editabletreemodel/treemodel.py diff --git a/examples/widgets/itemviews/editabletreemodel/default.txt b/examples/widgets/itemviews/editabletreemodel/default.txt new file mode 100644 index 000000000..98746548b --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/default.txt @@ -0,0 +1,40 @@ +Getting Started How to familiarize yourself with Qt Designer + Launching Designer Running the Qt Designer application + The User Interface How to interact with Qt Designer + +Designing a Component Creating a GUI for your application + Creating a Dialog How to create a dialog + Composing the Dialog Putting widgets into the dialog example + Creating a Layout Arranging widgets on a form + Signal and Slot Connections Making widget communicate with each other + +Using a Component in Your Application Generating code from forms + The Direct Approach Using a form without any adjustments + The Single Inheritance Approach Subclassing a form's base class + The Multiple Inheritance Approach Subclassing the form itself + Automatic Connections Connecting widgets using a naming scheme + A Dialog Without Auto-Connect How to connect widgets without a naming scheme + A Dialog With Auto-Connect Using automatic connections + +Form Editing Mode How to edit a form in Qt Designer + Managing Forms Loading and saving forms + Editing a Form Basic editing techniques + The Property Editor Changing widget properties + The Object Inspector Examining the hierarchy of objects on a form + Layouts Objects that arrange widgets on a form + Applying and Breaking Layouts Managing widgets in layouts + Horizontal and Vertical Layouts Standard row and column layouts + The Grid Layout Arranging widgets in a matrix + Previewing Forms Checking that the design works + +Using Containers How to group widgets together + General Features Common container features + Frames QFrame + Group Boxes QGroupBox + Stacked Widgets QStackedWidget + Tab Widgets QTabWidget + Toolbox Widgets QToolBox + +Connection Editing Mode Connecting widgets together with signals and slots + Connecting Objects Making connections in Qt Designer + Editing Connections Changing existing connections diff --git a/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.png b/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.png new file mode 100644 index 000000000..b50c792aa Binary files /dev/null and b/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.png differ diff --git a/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.rst b/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.rst new file mode 100644 index 000000000..c936972a0 --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/doc/editabletreemodel.rst @@ -0,0 +1,10 @@ +Editable Tree Model Example +=========================== + +A Python application that demonstrates the analogous example in C++ +`Editable Tree Model Example `_ + +.. image:: editabletreemodel.png + :width: 611 + :alt: editabletreemodel screenshot + diff --git a/examples/widgets/itemviews/editabletreemodel/editabletreemodel.pyproject b/examples/widgets/itemviews/editabletreemodel/editabletreemodel.pyproject new file mode 100644 index 000000000..1e67c727b --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/editabletreemodel.pyproject @@ -0,0 +1,7 @@ +{ + "files": ["main.py", + "mainwindow.py", + "treeitem.py", + "treemodel.py", + "default.txt"] +} diff --git a/examples/widgets/itemviews/editabletreemodel/main.py b/examples/widgets/itemviews/editabletreemodel/main.py new file mode 100644 index 000000000..22f535c1f --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/main.py @@ -0,0 +1,51 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + + +import sys +from PySide6.QtWidgets import QApplication +from mainwindow import MainWindow + + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) diff --git a/examples/widgets/itemviews/editabletreemodel/mainwindow.py b/examples/widgets/itemviews/editabletreemodel/mainwindow.py new file mode 100644 index 000000000..eaa9db969 --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/mainwindow.py @@ -0,0 +1,200 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +import sys +from pathlib import Path + +from PySide6.QtCore import (QAbstractItemModel, QItemSelectionModel, + QModelIndex, Qt, Slot) +from PySide6.QtWidgets import (QAbstractItemView, QMainWindow, QTreeView, + QWidget) +from PySide6.QtTest import QAbstractItemModelTester + +from treemodel import TreeModel + + +class MainWindow(QMainWindow): + def __init__(self, parent: QWidget = None): + super().__init__(parent) + self.resize(573, 468) + + self.view = QTreeView() + self.view.setAlternatingRowColors(True) + self.view.setSelectionBehavior(QAbstractItemView.SelectItems) + self.view.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) + self.view.setAnimated(False) + self.view.setAllColumnsShowFocus(True) + self.setCentralWidget(self.view) + + menubar = self.menuBar() + file_menu = menubar.addMenu("&File") + self.exit_action = file_menu.addAction("E&xit") + self.exit_action.setShortcut("Ctrl+Q") + self.exit_action.triggered.connect(self.close) + + actions_menu = menubar.addMenu("&Actions") + actions_menu.triggered.connect(self.update_actions) + self.insert_row_action = actions_menu.addAction("Insert Row") + self.insert_row_action.setShortcut("Ctrl+I, R") + self.insert_row_action.triggered.connect(self.insert_row) + self.insert_column_action = actions_menu.addAction("Insert Column") + self.insert_column_action.setShortcut("Ctrl+I, C") + self.insert_column_action.triggered.connect(self.insert_column) + actions_menu.addSeparator() + self.remove_row_action = actions_menu.addAction("Remove Row") + self.remove_row_action.setShortcut("Ctrl+R, R") + self.remove_row_action.triggered.connect(self.remove_row) + self.remove_column_action = actions_menu.addAction("Remove Column") + self.remove_column_action.setShortcut("Ctrl+R, C") + self.remove_column_action.triggered.connect(self.remove_column) + actions_menu.addSeparator() + self.insert_child_action = actions_menu.addAction("Insert Child") + self.insert_child_action.setShortcut("Ctrl+N") + self.insert_child_action.triggered.connect(self.insert_child) + help_menu = menubar.addMenu("&Help") + about_qt_action = help_menu.addAction("About Qt", qApp.aboutQt) + about_qt_action.setShortcut("F1") + + self.setWindowTitle("Editable Tree Model") + + headers = ["Title", "Description"] + + file = Path(__file__).parent / "default.txt" + self.model = TreeModel(headers, file.read_text(), self) + + if "-t" in sys.argv: + QAbstractItemModelTester(self.model, self) + self.view.setModel(self.model) + self.view.expandAll() + + for column in range(self.model.columnCount()): + self.view.resizeColumnToContents(column) + + selection_model = self.view.selectionModel() + selection_model.selectionChanged.connect(self.update_actions) + + self.update_actions() + + @Slot() + def insert_child(self) -> None: + selection_model = self.view.selectionModel() + index: QModelIndex = selection_model.currentIndex() + model: QAbstractItemModel = self.view.model() + + if model.columnCount(index) == 0: + if not model.insertColumn(0, index): + return + + if not model.insertRow(0, index): + return + + for column in range(model.columnCount(index)): + child: QModelIndex = model.index(0, column, index) + model.setData(child, "[No data]", Qt.EditRole) + if not model.headerData(column, Qt.Horizontal): + model.setHeaderData(column, Qt.Horizontal, "[No header]", + Qt.EditRole) + + selection_model.setCurrentIndex( + model.index(0, 0, index), QItemSelectionModel.ClearAndSelect + ) + self.update_actions() + + @Slot() + def insert_column(self) -> None: + model: QAbstractItemModel = self.view.model() + column: int = self.view.selectionModel().currentIndex().column() + + changed: bool = model.insertColumn(column + 1) + if changed: + model.setHeaderData(column + 1, Qt.Horizontal, "[No header]", + Qt.EditRole) + + self.update_actions() + + @Slot() + def insert_row(self) -> None: + index: QModelIndex = self.view.selectionModel().currentIndex() + model: QAbstractItemModel = self.view.model() + parent: QModelIndex = index.parent() + + if not model.insertRow(index.row() + 1, parent): + return + + self.update_actions() + + for column in range(model.columnCount(parent)): + child: QModelIndex = model.index(index.row() + 1, column, parent) + model.setData(child, "[No data]", Qt.EditRole) + + @Slot() + def remove_column(self) -> None: + model: QAbstractItemModel = self.view.model() + column: int = self.view.selectionModel().currentIndex().column() + + if model.removeColumn(column): + self.update_actions() + + @Slot() + def remove_row(self) -> None: + index: QModelIndex = self.view.selectionModel().currentIndex() + model: QAbstractItemModel = self.view.model() + + if model.removeRow(index.row(), index.parent()): + self.update_actions() + + @Slot() + def update_actions(self) -> None: + selection_model = self.view.selectionModel() + has_selection: bool = not selection_model.selection().isEmpty() + self.remove_row_action.setEnabled(has_selection) + self.remove_column_action.setEnabled(has_selection) + + current_index = selection_model.currentIndex() + has_current: bool = current_index.isValid() + self.insert_row_action.setEnabled(has_current) + self.insert_column_action.setEnabled(has_current) + + if has_current: + self.view.closePersistentEditor(current_index) + msg = f"Position: ({current_index.row()},{current_index.column()})" + if not current_index.parent().isValid(): + msg += " in top level" + self.statusBar().showMessage(msg) diff --git a/examples/widgets/itemviews/editabletreemodel/treeitem.py b/examples/widgets/itemviews/editabletreemodel/treeitem.py new file mode 100644 index 000000000..0f761b199 --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/treeitem.py @@ -0,0 +1,131 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + + +class TreeItem: + def __init__(self, data: list, parent: 'TreeItem' = None): + self.item_data = data + self.parent_item = parent + self.child_items = [] + + def child(self, number: int) -> 'TreeItem': + if number < 0 or number >= len(self.child_items): + return None + return self.child_items[number] + + def last_child(self): + return self.child_items[-1] if self.child_items else None + + def child_count(self) -> int: + return len(self.child_items) + + def child_number(self) -> int: + if self.parent_item: + return self.parent_item.child_items.index(self) + return 0 + + def column_count(self) -> int: + return len(self.item_data) + + def data(self, column: int): + if column < 0 or column >= len(self.item_data): + return None + return self.item_data[column] + + def insert_children(self, position: int, count: int, columns: int) -> bool: + if position < 0 or position > len(self.child_items): + return False + + for row in range(count): + data = [None] * columns + item = TreeItem(data.copy(), self) + self.child_items.insert(position, item) + + return True + + def insert_columns(self, position: int, columns: int) -> bool: + if position < 0 or position > len(self.item_data): + return False + + for column in range(columns): + self.item_data.insert(position, None) + + for child in self.child_items: + child.insert_columns(position, columns) + + return True + + def parent(self): + return self.parent_item + + def remove_children(self, position: int, count: int) -> bool: + if position < 0 or position + count > len(self.child_items): + return False + + for row in range(count): + self.child_items.pop(position) + + return True + + def remove_columns(self, position: int, columns: int) -> bool: + if position < 0 or position + columns > len(self.item_data): + return False + + for column in range(columns): + self.item_data.pop(position) + + for child in self.child_items: + child.remove_columns(position, columns) + + return True + + def set_data(self, column: int, value): + if column < 0 or column >= len(self.item_data): + return False + + self.item_data[column] = value + return True + + def __repr__(self) -> str: + result = f"" + result += f", {len(self.child_items)} children>" + return result diff --git a/examples/widgets/itemviews/editabletreemodel/treemodel.py b/examples/widgets/itemviews/editabletreemodel/treemodel.py new file mode 100644 index 000000000..b4e807349 --- /dev/null +++ b/examples/widgets/itemviews/editabletreemodel/treemodel.py @@ -0,0 +1,241 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: http://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + + +from PySide6.QtCore import QModelIndex, Qt, QAbstractItemModel, Signal +from treeitem import TreeItem + + +class TreeModel(QAbstractItemModel): + # Define signals + dataChanged = Signal(QModelIndex, QModelIndex, object) + headerDataChanged = Signal(Qt.Orientation, int, int) + + def __init__(self, headers: list, data: str, parent=None): + super().__init__(parent) + + self.root_data = headers + self.root_item = TreeItem(self.root_data.copy()) + self.setup_model_data(data.split("\n"), self.root_item) + + def columnCount(self, parent: QModelIndex = None) -> int: + return self.root_item.column_count() + + def data(self, index: QModelIndex, role: int = None): + if not index.isValid(): + return None + + if role != Qt.DisplayRole and role != Qt.EditRole: + return None + + item: TreeItem = self.get_item(index) + + return item.data(index.column()) + + def flags(self, index: QModelIndex) -> Qt.ItemFlags: + if not index.isValid(): + return Qt.NoItemFlags + + return Qt.ItemIsEditable | QAbstractItemModel.flags(self, index) + + def get_item(self, index: QModelIndex = QModelIndex()) -> TreeItem: + if index.isValid(): + item: TreeItem = index.internalPointer() + if item: + return item + + return self.root_item + + def headerData(self, section: int, orientation: Qt.Orientation, + role: int = Qt.DisplayRole): + if orientation == Qt.Horizontal and role == Qt.DisplayRole: + return self.root_item.data(section) + + return None + + def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex: + if parent.isValid() and parent.column() != 0: + return QModelIndex() + + parent_item: TreeItem = self.get_item(parent) + if not parent_item: + return QModelIndex() + + child_item: TreeItem = parent_item.child(row) + if child_item: + return self.createIndex(row, column, child_item) + return QModelIndex() + + def insertColumns(self, position: int, columns: int, + parent: QModelIndex = QModelIndex()) -> bool: + self.beginInsertColumns(parent, position, position + columns - 1) + success: bool = self.root_item.insert_columns(position, columns) + self.endInsertColumns() + + return success + + def insertRows(self, position: int, rows: int, + parent: QModelIndex = QModelIndex()) -> bool: + parent_item: TreeItem = self.get_item(parent) + if not parent_item: + return False + + self.beginInsertRows(parent, position, position + rows - 1) + column_count = self.root_item.column_count() + success: bool = parent_item.insert_children(position, rows, column_count) + self.endInsertRows() + + return success + + def parent(self, index: QModelIndex = QModelIndex()) -> QModelIndex: + if not index.isValid(): + return QModelIndex() + + child_item: TreeItem = self.get_item(index) + if child_item: + parent_item: TreeItem = child_item.parent() + else: + parent_item = None + + if parent_item == self.root_item or not parent_item: + return QModelIndex() + + return self.createIndex(parent_item.child_number(), 0, parent_item) + + def removeColumns(self, position: int, columns: int, + parent: QModelIndex = QModelIndex()) -> bool: + self.beginRemoveColumns(parent, position, position + columns - 1) + success: bool = self.root_item.remove_columns(position, columns) + self.endRemoveColumns() + + if self.root_item.column_count() == 0: + self.removeRows(0, self.rowCount()) + + return success + + def removeRows(self, position: int, rows: int, + parent: QModelIndex = QModelIndex()) -> bool: + parent_item: TreeItem = self.get_item(parent) + if not parent_item: + return False + + self.beginRemoveRows(parent, position, position + rows - 1) + success: bool = parent_item.remove_children(position, rows) + self.endRemoveRows() + + return success + + def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: + if parent.isValid() and parent.column() > 0: + return 0 + + parent_item: TreeItem = self.get_item(parent) + if not parent_item: + return 0 + return parent_item.child_count() + + def setData(self, index: QModelIndex, value, role: int) -> bool: + if role != Qt.EditRole: + return False + + item: TreeItem = self.get_item(index) + result: bool = item.set_data(index.column(), value) + + if result: + self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) + + return result + + def setHeaderData(self, section: int, orientation: Qt.Orientation, value, + role: int = None) -> bool: + if role != Qt.EditRole or orientation != Qt.Horizontal: + return False + + result: bool = self.root_item.set_data(section, value) + + if result: + # todo: Check if emit headerDataChanged signal is correct + # emit headerDataChanged(orientation, section, section) + self.headerDataChanged(orientation, section, section) + + return result + + def setup_model_data(self, lines: list, parent: TreeItem): + parents = [parent] + indentations = [0] + + for line in lines: + line = line.rstrip() + if line and "\t" in line: + + position = 0 + while position < len(line): + if line[position] != " ": + break + position += 1 + + column_data = line[position:].split("\t") + column_data = [string for string in column_data if string] + + if position > indentations[-1]: + if parents[-1].child_count() > 0: + parents.append(parents[-1].last_child()) + indentations.append(position) + else: + while position < indentations[-1] and parents: + parents.pop() + indentations.pop() + + parent: TreeItem = parents[-1] + col_count = self.root_item.column_count() + parent.insert_children(parent.child_count(), 1, col_count) + + for column in range(len(column_data)): + child = parent.last_child() + child.set_data(column, column_data[column]) + + def _repr_recursion(self, item: TreeItem, indent: int = 0) -> str: + result = " " * indent + repr(item) + "\n" + for child in item.child_items: + result += self._repr_recursion(child, indent + 2) + return result + + def __repr__(self) -> str: + return self._repr_recursion(self.root_item) -- cgit v1.2.3