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