1#!/usr/bin/env python 2 3 4############################################################################# 5## 6## Copyright (C) 2017 Riverbank Computing Limited. 7## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). 8## All rights reserved. 9## 10## This file is part of the examples of PyQt. 11## 12## $QT_BEGIN_LICENSE:BSD$ 13## You may use this file under the terms of the BSD license as follows: 14## 15## "Redistribution and use in source and binary forms, with or without 16## modification, are permitted provided that the following conditions are 17## met: 18## * Redistributions of source code must retain the above copyright 19## notice, this list of conditions and the following disclaimer. 20## * Redistributions in binary form must reproduce the above copyright 21## notice, this list of conditions and the following disclaimer in 22## the documentation and/or other materials provided with the 23## distribution. 24## * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor 25## the names of its contributors may be used to endorse or promote 26## products derived from this software without specific prior written 27## permission. 28## 29## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 30## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 31## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 32## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 34## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 35## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 37## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 39## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." 40## $QT_END_LICENSE$ 41## 42############################################################################# 43 44 45from PyQt5.QtCore import (QAbstractItemModel, QFile, QIODevice, 46 QItemSelectionModel, QModelIndex, Qt) 47from PyQt5.QtWidgets import QApplication, QMainWindow 48 49import editabletreemodel_rc 50from ui_mainwindow import Ui_MainWindow 51 52 53class TreeItem(object): 54 def __init__(self, data, parent=None): 55 self.parentItem = parent 56 self.itemData = data 57 self.childItems = [] 58 59 def child(self, row): 60 return self.childItems[row] 61 62 def childCount(self): 63 return len(self.childItems) 64 65 def childNumber(self): 66 if self.parentItem != None: 67 return self.parentItem.childItems.index(self) 68 return 0 69 70 def columnCount(self): 71 return len(self.itemData) 72 73 def data(self, column): 74 return self.itemData[column] 75 76 def insertChildren(self, position, count, columns): 77 if position < 0 or position > len(self.childItems): 78 return False 79 80 for row in range(count): 81 data = [None for v in range(columns)] 82 item = TreeItem(data, self) 83 self.childItems.insert(position, item) 84 85 return True 86 87 def insertColumns(self, position, columns): 88 if position < 0 or position > len(self.itemData): 89 return False 90 91 for column in range(columns): 92 self.itemData.insert(position, None) 93 94 for child in self.childItems: 95 child.insertColumns(position, columns) 96 97 return True 98 99 def parent(self): 100 return self.parentItem 101 102 def removeChildren(self, position, count): 103 if position < 0 or position + count > len(self.childItems): 104 return False 105 106 for row in range(count): 107 self.childItems.pop(position) 108 109 return True 110 111 def removeColumns(self, position, columns): 112 if position < 0 or position + columns > len(self.itemData): 113 return False 114 115 for column in range(columns): 116 self.itemData.pop(position) 117 118 for child in self.childItems: 119 child.removeColumns(position, columns) 120 121 return True 122 123 def setData(self, column, value): 124 if column < 0 or column >= len(self.itemData): 125 return False 126 127 self.itemData[column] = value 128 129 return True 130 131 132class TreeModel(QAbstractItemModel): 133 def __init__(self, headers, data, parent=None): 134 super(TreeModel, self).__init__(parent) 135 136 rootData = [header for header in headers] 137 self.rootItem = TreeItem(rootData) 138 self.setupModelData(data.split("\n"), self.rootItem) 139 140 def columnCount(self, parent=QModelIndex()): 141 return self.rootItem.columnCount() 142 143 def data(self, index, role): 144 if not index.isValid(): 145 return None 146 147 if role != Qt.DisplayRole and role != Qt.EditRole: 148 return None 149 150 item = self.getItem(index) 151 return item.data(index.column()) 152 153 def flags(self, index): 154 if not index.isValid(): 155 return 0 156 157 return Qt.ItemIsEditable | super(TreeModel, self).flags(index) 158 159 def getItem(self, index): 160 if index.isValid(): 161 item = index.internalPointer() 162 if item: 163 return item 164 165 return self.rootItem 166 167 def headerData(self, section, orientation, role=Qt.DisplayRole): 168 if orientation == Qt.Horizontal and role == Qt.DisplayRole: 169 return self.rootItem.data(section) 170 171 return None 172 173 def index(self, row, column, parent=QModelIndex()): 174 if parent.isValid() and parent.column() != 0: 175 return QModelIndex() 176 177 parentItem = self.getItem(parent) 178 childItem = parentItem.child(row) 179 if childItem: 180 return self.createIndex(row, column, childItem) 181 else: 182 return QModelIndex() 183 184 def insertColumns(self, position, columns, parent=QModelIndex()): 185 self.beginInsertColumns(parent, position, position + columns - 1) 186 success = self.rootItem.insertColumns(position, columns) 187 self.endInsertColumns() 188 189 return success 190 191 def insertRows(self, position, rows, parent=QModelIndex()): 192 parentItem = self.getItem(parent) 193 self.beginInsertRows(parent, position, position + rows - 1) 194 success = parentItem.insertChildren(position, rows, 195 self.rootItem.columnCount()) 196 self.endInsertRows() 197 198 return success 199 200 def parent(self, index): 201 if not index.isValid(): 202 return QModelIndex() 203 204 childItem = self.getItem(index) 205 parentItem = childItem.parent() 206 207 if parentItem == self.rootItem: 208 return QModelIndex() 209 210 return self.createIndex(parentItem.childNumber(), 0, parentItem) 211 212 def removeColumns(self, position, columns, parent=QModelIndex()): 213 self.beginRemoveColumns(parent, position, position + columns - 1) 214 success = self.rootItem.removeColumns(position, columns) 215 self.endRemoveColumns() 216 217 if self.rootItem.columnCount() == 0: 218 self.removeRows(0, self.rowCount()) 219 220 return success 221 222 def removeRows(self, position, rows, parent=QModelIndex()): 223 parentItem = self.getItem(parent) 224 225 self.beginRemoveRows(parent, position, position + rows - 1) 226 success = parentItem.removeChildren(position, rows) 227 self.endRemoveRows() 228 229 return success 230 231 def rowCount(self, parent=QModelIndex()): 232 parentItem = self.getItem(parent) 233 234 return parentItem.childCount() 235 236 def setData(self, index, value, role=Qt.EditRole): 237 if role != Qt.EditRole: 238 return False 239 240 item = self.getItem(index) 241 result = item.setData(index.column(), value) 242 243 if result: 244 self.dataChanged.emit(index, index) 245 246 return result 247 248 def setHeaderData(self, section, orientation, value, role=Qt.EditRole): 249 if role != Qt.EditRole or orientation != Qt.Horizontal: 250 return False 251 252 result = self.rootItem.setData(section, value) 253 if result: 254 self.headerDataChanged.emit(orientation, section, section) 255 256 return result 257 258 def setupModelData(self, lines, parent): 259 parents = [parent] 260 indentations = [0] 261 262 number = 0 263 264 while number < len(lines): 265 position = 0 266 while position < len(lines[number]): 267 if lines[number][position] != " ": 268 break 269 position += 1 270 271 lineData = lines[number][position:].trimmed() 272 273 if lineData: 274 # Read the column data from the rest of the line. 275 columnData = [s for s in lineData.split('\t') if s] 276 277 if position > indentations[-1]: 278 # The last child of the current parent is now the new 279 # parent unless the current parent has no children. 280 281 if parents[-1].childCount() > 0: 282 parents.append(parents[-1].child(parents[-1].childCount() - 1)) 283 indentations.append(position) 284 285 else: 286 while position < indentations[-1] and len(parents) > 0: 287 parents.pop() 288 indentations.pop() 289 290 # Append a new item to the current parent's list of children. 291 parent = parents[-1] 292 parent.insertChildren(parent.childCount(), 1, 293 self.rootItem.columnCount()) 294 for column in range(len(columnData)): 295 parent.child(parent.childCount() -1).setData(column, columnData[column]) 296 297 number += 1 298 299 300class MainWindow(QMainWindow, Ui_MainWindow): 301 def __init__(self, parent=None): 302 super(MainWindow, self).__init__(parent) 303 304 self.setupUi(self) 305 306 headers = ("Title", "Description") 307 308 file = QFile(':/default.txt') 309 file.open(QIODevice.ReadOnly) 310 model = TreeModel(headers, file.readAll()) 311 file.close() 312 313 self.view.setModel(model) 314 for column in range(model.columnCount()): 315 self.view.resizeColumnToContents(column) 316 317 self.exitAction.triggered.connect(QApplication.instance().quit) 318 319 self.view.selectionModel().selectionChanged.connect(self.updateActions) 320 321 self.actionsMenu.aboutToShow.connect(self.updateActions) 322 self.insertRowAction.triggered.connect(self.insertRow) 323 self.insertColumnAction.triggered.connect(self.insertColumn) 324 self.removeRowAction.triggered.connect(self.removeRow) 325 self.removeColumnAction.triggered.connect(self.removeColumn) 326 self.insertChildAction.triggered.connect(self.insertChild) 327 328 self.updateActions() 329 330 def insertChild(self): 331 index = self.view.selectionModel().currentIndex() 332 model = self.view.model() 333 334 if model.columnCount(index) == 0: 335 if not model.insertColumn(0, index): 336 return 337 338 if not model.insertRow(0, index): 339 return 340 341 for column in range(model.columnCount(index)): 342 child = model.index(0, column, index) 343 model.setData(child, "[No data]", Qt.EditRole) 344 if model.headerData(column, Qt.Horizontal) is None: 345 model.setHeaderData(column, Qt.Horizontal, "[No header]", 346 Qt.EditRole) 347 348 self.view.selectionModel().setCurrentIndex(model.index(0, 0, index), 349 QItemSelectionModel.ClearAndSelect) 350 self.updateActions() 351 352 def insertColumn(self): 353 model = self.view.model() 354 column = self.view.selectionModel().currentIndex().column() 355 356 changed = model.insertColumn(column + 1) 357 if changed: 358 model.setHeaderData(column + 1, Qt.Horizontal, "[No header]", 359 Qt.EditRole) 360 361 self.updateActions() 362 363 return changed 364 365 def insertRow(self): 366 index = self.view.selectionModel().currentIndex() 367 model = self.view.model() 368 369 if not model.insertRow(index.row()+1, index.parent()): 370 return 371 372 self.updateActions() 373 374 for column in range(model.columnCount(index.parent())): 375 child = model.index(index.row()+1, column, index.parent()) 376 model.setData(child, "[No data]", Qt.EditRole) 377 378 def removeColumn(self): 379 model = self.view.model() 380 column = self.view.selectionModel().currentIndex().column() 381 382 changed = model.removeColumn(column) 383 if changed: 384 self.updateActions() 385 386 return changed 387 388 def removeRow(self): 389 index = self.view.selectionModel().currentIndex() 390 model = self.view.model() 391 392 if (model.removeRow(index.row(), index.parent())): 393 self.updateActions() 394 395 def updateActions(self): 396 hasSelection = not self.view.selectionModel().selection().isEmpty() 397 self.removeRowAction.setEnabled(hasSelection) 398 self.removeColumnAction.setEnabled(hasSelection) 399 400 hasCurrent = self.view.selectionModel().currentIndex().isValid() 401 self.insertRowAction.setEnabled(hasCurrent) 402 self.insertColumnAction.setEnabled(hasCurrent) 403 404 if hasCurrent: 405 self.view.closePersistentEditor(self.view.selectionModel().currentIndex()) 406 407 row = self.view.selectionModel().currentIndex().row() 408 column = self.view.selectionModel().currentIndex().column() 409 if self.view.selectionModel().currentIndex().parent().isValid(): 410 self.statusBar().showMessage("Position: (%d,%d)" % (row, column)) 411 else: 412 self.statusBar().showMessage("Position: (%d,%d) in top level" % (row, column)) 413 414 415if __name__ == '__main__': 416 417 import sys 418 419 app = QApplication(sys.argv) 420 window = MainWindow() 421 window.show() 422 sys.exit(app.exec_()) 423