#!/usr/bin/env python ############################################################################# ## ## Copyright (C) 2017 Riverbank Computing Limited. ## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). ## All rights reserved. ## ## This file is part of the examples of PyQt. ## ## $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 Nokia Corporation and its Subsidiary(-ies) 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 PyQt5.QtCore import (QAbstractItemModel, QFile, QIODevice, QItemSelectionModel, QModelIndex, Qt) from PyQt5.QtWidgets import QApplication, QMainWindow import editabletreemodel_rc from ui_mainwindow import Ui_MainWindow class TreeItem(object): def __init__(self, data, parent=None): self.parentItem = parent self.itemData = data self.childItems = [] def child(self, row): return self.childItems[row] def childCount(self): return len(self.childItems) def childNumber(self): if self.parentItem != None: return self.parentItem.childItems.index(self) return 0 def columnCount(self): return len(self.itemData) def data(self, column): return self.itemData[column] def insertChildren(self, position, count, columns): if position < 0 or position > len(self.childItems): return False for row in range(count): data = [None for v in range(columns)] item = TreeItem(data, self) self.childItems.insert(position, item) return True def insertColumns(self, position, columns): if position < 0 or position > len(self.itemData): return False for column in range(columns): self.itemData.insert(position, None) for child in self.childItems: child.insertColumns(position, columns) return True def parent(self): return self.parentItem def removeChildren(self, position, count): if position < 0 or position + count > len(self.childItems): return False for row in range(count): self.childItems.pop(position) return True def removeColumns(self, position, columns): if position < 0 or position + columns > len(self.itemData): return False for column in range(columns): self.itemData.pop(position) for child in self.childItems: child.removeColumns(position, columns) return True def setData(self, column, value): if column < 0 or column >= len(self.itemData): return False self.itemData[column] = value return True class TreeModel(QAbstractItemModel): def __init__(self, headers, data, parent=None): super(TreeModel, self).__init__(parent) rootData = [header for header in headers] self.rootItem = TreeItem(rootData) self.setupModelData(data.split("\n"), self.rootItem) def columnCount(self, parent=QModelIndex()): return self.rootItem.columnCount() def data(self, index, role): if not index.isValid(): return None if role != Qt.DisplayRole and role != Qt.EditRole: return None item = self.getItem(index) return item.data(index.column()) def flags(self, index): if not index.isValid(): return 0 return Qt.ItemIsEditable | super(TreeModel, self).flags(index) def getItem(self, index): if index.isValid(): item = index.internalPointer() if item: return item return self.rootItem def headerData(self, section, orientation, role=Qt.DisplayRole): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.rootItem.data(section) return None def index(self, row, column, parent=QModelIndex()): if parent.isValid() and parent.column() != 0: return QModelIndex() parentItem = self.getItem(parent) childItem = parentItem.child(row) if childItem: return self.createIndex(row, column, childItem) else: return QModelIndex() def insertColumns(self, position, columns, parent=QModelIndex()): self.beginInsertColumns(parent, position, position + columns - 1) success = self.rootItem.insertColumns(position, columns) self.endInsertColumns() return success def insertRows(self, position, rows, parent=QModelIndex()): parentItem = self.getItem(parent) self.beginInsertRows(parent, position, position + rows - 1) success = parentItem.insertChildren(position, rows, self.rootItem.columnCount()) self.endInsertRows() return success def parent(self, index): if not index.isValid(): return QModelIndex() childItem = self.getItem(index) parentItem = childItem.parent() if parentItem == self.rootItem: return QModelIndex() return self.createIndex(parentItem.childNumber(), 0, parentItem) def removeColumns(self, position, columns, parent=QModelIndex()): self.beginRemoveColumns(parent, position, position + columns - 1) success = self.rootItem.removeColumns(position, columns) self.endRemoveColumns() if self.rootItem.columnCount() == 0: self.removeRows(0, self.rowCount()) return success def removeRows(self, position, rows, parent=QModelIndex()): parentItem = self.getItem(parent) self.beginRemoveRows(parent, position, position + rows - 1) success = parentItem.removeChildren(position, rows) self.endRemoveRows() return success def rowCount(self, parent=QModelIndex()): parentItem = self.getItem(parent) return parentItem.childCount() def setData(self, index, value, role=Qt.EditRole): if role != Qt.EditRole: return False item = self.getItem(index) result = item.setData(index.column(), value) if result: self.dataChanged.emit(index, index) return result def setHeaderData(self, section, orientation, value, role=Qt.EditRole): if role != Qt.EditRole or orientation != Qt.Horizontal: return False result = self.rootItem.setData(section, value) if result: self.headerDataChanged.emit(orientation, section, section) return result def setupModelData(self, lines, parent): parents = [parent] indentations = [0] number = 0 while number < len(lines): position = 0 while position < len(lines[number]): if lines[number][position] != " ": break position += 1 lineData = lines[number][position:].trimmed() if lineData: # Read the column data from the rest of the line. columnData = [s for s in lineData.split('\t') if s] if position > indentations[-1]: # The last child of the current parent is now the new # parent unless the current parent has no children. if parents[-1].childCount() > 0: parents.append(parents[-1].child(parents[-1].childCount() - 1)) indentations.append(position) else: while position < indentations[-1] and len(parents) > 0: parents.pop() indentations.pop() # Append a new item to the current parent's list of children. parent = parents[-1] parent.insertChildren(parent.childCount(), 1, self.rootItem.columnCount()) for column in range(len(columnData)): parent.child(parent.childCount() -1).setData(column, columnData[column]) number += 1 class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.setupUi(self) headers = ("Title", "Description") file = QFile(':/default.txt') file.open(QIODevice.ReadOnly) model = TreeModel(headers, file.readAll()) file.close() self.view.setModel(model) for column in range(model.columnCount()): self.view.resizeColumnToContents(column) self.exitAction.triggered.connect(QApplication.instance().quit) self.view.selectionModel().selectionChanged.connect(self.updateActions) self.actionsMenu.aboutToShow.connect(self.updateActions) self.insertRowAction.triggered.connect(self.insertRow) self.insertColumnAction.triggered.connect(self.insertColumn) self.removeRowAction.triggered.connect(self.removeRow) self.removeColumnAction.triggered.connect(self.removeColumn) self.insertChildAction.triggered.connect(self.insertChild) self.updateActions() def insertChild(self): index = self.view.selectionModel().currentIndex() model = 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 = model.index(0, column, index) model.setData(child, "[No data]", Qt.EditRole) if model.headerData(column, Qt.Horizontal) is None: model.setHeaderData(column, Qt.Horizontal, "[No header]", Qt.EditRole) self.view.selectionModel().setCurrentIndex(model.index(0, 0, index), QItemSelectionModel.ClearAndSelect) self.updateActions() def insertColumn(self): model = self.view.model() column = self.view.selectionModel().currentIndex().column() changed = model.insertColumn(column + 1) if changed: model.setHeaderData(column + 1, Qt.Horizontal, "[No header]", Qt.EditRole) self.updateActions() return changed def insertRow(self): index = self.view.selectionModel().currentIndex() model = self.view.model() if not model.insertRow(index.row()+1, index.parent()): return self.updateActions() for column in range(model.columnCount(index.parent())): child = model.index(index.row()+1, column, index.parent()) model.setData(child, "[No data]", Qt.EditRole) def removeColumn(self): model = self.view.model() column = self.view.selectionModel().currentIndex().column() changed = model.removeColumn(column) if changed: self.updateActions() return changed def removeRow(self): index = self.view.selectionModel().currentIndex() model = self.view.model() if (model.removeRow(index.row(), index.parent())): self.updateActions() def updateActions(self): hasSelection = not self.view.selectionModel().selection().isEmpty() self.removeRowAction.setEnabled(hasSelection) self.removeColumnAction.setEnabled(hasSelection) hasCurrent = self.view.selectionModel().currentIndex().isValid() self.insertRowAction.setEnabled(hasCurrent) self.insertColumnAction.setEnabled(hasCurrent) if hasCurrent: self.view.closePersistentEditor(self.view.selectionModel().currentIndex()) row = self.view.selectionModel().currentIndex().row() column = self.view.selectionModel().currentIndex().column() if self.view.selectionModel().currentIndex().parent().isValid(): self.statusBar().showMessage("Position: (%d,%d)" % (row, column)) else: self.statusBar().showMessage("Position: (%d,%d) in top level" % (row, column)) if __name__ == '__main__': import sys app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())