I am making a to-do tree application (Earlybird) in Python that will show checkable tasks/subtasks in a tree view. It includes higher-level groups of tasks grouped into blocks (e.g., Work block, Home block) that do not have checkboxes. It looks like this:This is is my first application using Python, and object-oriented programming in general. I am usually a Matlab programmer. Hence, feedback on any level of what I've done would be very helpful. I'm about to put the application on Github, and add a ton of features, so now is the perfect time to throw some dynamite at it.The application has two main classes:EarlybirdTree: the core tree view. This is subclassed from QTreeView, and defines basic methods (loading and saving files, adding edited items to the undo stack, etc).. EarlybirdMain: a simple wrapper for EarlybirdTree objects. This is subclassed from QMainWindow, and allows the user to interact with the tree's methods using menus and toolbars.The following code includes the above two py files, as well as an earlybird data file (testFile.eb). I store the data in json format. Each main function also adds a separate view of the undostack, just for convenience. Note to keep this from blowing up, I haven't included the functionality for adding/removing items and many other bells and whistles.earlybirdTree.py# -*- coding: utf-8 -*-earlybirdTree.py    Defines the EarlyBirdTree class, a QTreeView subclass that displays a     custom QStandardItemModel as a simple to-do tree. The data is saved    as a custom json file.import sysimport osimport jsonfrom PySide import QtGui, QtCoreclass StandardItemModel(QtGui.QStandardItemModel):    '''Items will emit this signal when edited'''    itemDataChanged = QtCore.Signal(object, object, object, object)class StandardItem(QtGui.QStandardItem):    ''''Subclass QStandardItem to reimplement setData to emit itemDataChanged'''    def setData(self, newValue, role=QtCore.Qt.UserRole + 1):        #print setData called with role , role  #for debugging        if role == QtCore.Qt.EditRole:            oldValue = self.data(role)            QtGui.QStandardItem.setData(self, newValue, role)            model = self.model()            if model is not None and oldValue != newValue:                model.itemDataChanged.emit(self, oldValue, newValue, role)            return True        if role == QtCore.Qt.CheckStateRole:            oldValue = self.data(role)            QtGui.QStandardItem.setData(self, newValue, role)                        model = self.model()            if model is not None and oldValue != newValue:                                             model.itemDataChanged.emit(self, oldValue, newValue, role)            return True        QtGui.QStandardItem.setData(self, newValue, role)class EarlybirdTree(QtGui.QTreeView):    '''The earlyBird to do tree view, the core class for the application.'''    def __init__(self, parent=None, filename = None):        QtGui.QTreeView.__init__(self, parent=None)        self.parent = parent        self.filename = filename        self.model = StandardItemModel()        self.rootItem = self.model.invisibleRootItem()                self.setModel(self.model)        self.makeConnections()        self.undoStack = QtGui.QUndoStack(self)        self.setStyleSheet(QTreeView::item:hover{background-color:#999966;})          self.headerLabels = [Task]        self.model.setHorizontalHeaderLabels(self.headerLabels)         if self.filename:            self.loadEarlybirdFile(self.filename)    def makeConnections(self):        '''Connect all the signals-slots needed.'''        self.model.itemDataChanged.connect(self.itemDataChangedSlot)    def itemDataChangedSlot(self, item, oldValue, newValue, role):        '''Slot used to push changes of existing items onto undoStack'''        if role == QtCore.Qt.EditRole:            command = CommandTextEdit(self, item, oldValue, newValue,                Text changed from '{0}' to '{1}'.format(oldValue, newValue))            self.undoStack.push(command)            return True        if role == QtCore.Qt.CheckStateRole:            command = CommandCheckStateChange(self, item, oldValue, newValue,                 CheckState changed from '{0}' to '{1}'.format(oldValue, newValue))            self.undoStack.push(command)            return True      def clearModel(self):        '''Clears data from model,clearing the view, but repopulates headers/root.        Used whenever an .eb file is loaded, or newFile method instantiated'''        self.model.clear()        self.model.setHorizontalHeaderLabels(self.headerLabels)        self.rootItem = self.model.invisibleRootItem()       def newFile(self):        '''Creates blank tree'''        if not self.undoStack.isClean() and self.saveCheck():            self.saveTodoData()        self.filename = None        self.clearModel()        self.undoStack.clear()    def closeEvent(self, event):        '''Typically closeevent is called by a QMainWindow wrapper,         but sometimes we do view these guys standalone'''        if not self.undoStack.isClean() and self.saveCheck():            self.fileSave()        self.close()         '''    ***    Next five methods are part of mechanics for loading .eb files    ***    '''    def loadEarlybirdFile(self, filename = None):        '''Opens todo tree file (.eb) and populates model with data.'''        if not self.undoStack.isClean() and self.saveCheck():            self.saveTodoData()         directoryName = os.path.dirname(filename) if filename else .        if not filename:            filename, foo = QtGui.QFileDialog.getOpenFileName(None,                    Load earlybird file, directoryName,                     (*.eb))                  if filename:            with open(filename) as f:                fileData = json.load(f)            if self.populateModel(fileData, filename):                self.expandAll()                self.filename = filename                self.undoStack.clear()                return True                return False       def populateModel(self, fileData, filename):        '''Verify that top-level items are blocks, and call methods to load data.'''        if taskblocks not in fileData:            print Warning: Cannot load {0}.\n\                  Top level must contain taskblocks..format(filename)            return False         if tasks in fileData:            print Warning: only reads taskblocks from top level.\n\                  Igorning top-level tasks in {0}..format(filename)        taskblockList = fileData[taskblocks]        self.clearModel()        return self.loadTaskblocks(taskblockList)    def loadTaskblocks(self, taskblockList):          '''Load task blocks into the model'''        for (blockNum, taskblock) in enumerate(taskblockList):             blockNameItem = StandardItem(taskblock[blockname])                   self.rootItem.appendRow(blockNameItem)            if tasks in taskblock:                taskList = taskblock[tasks]                self.loadTasks(taskList, blockNameItem)         return True          def loadTasks(self, taskList, parentItem):        '''Recursively load tasks until we hit a base task (a task w/o any subtasks).'''        for (taskNum, task) in enumerate(taskList):            taskNameItem = StandardItem(task[name])            taskNameItem.setCheckable(True)            #print task and done, task[name], task[done]            if task[done]:                taskNameItem.setCheckState(QtCore.Qt.Checked)                       else:                taskNameItem.setCheckState(QtCore.Qt.Unchecked)            parentItem.appendRow(taskNameItem) #add children only to column 0              if tasks in task:                subtaskList = task[tasks]                return self.loadTasks(subtaskList, taskNameItem)     '''    ****    Next seven methods are part of the saving mechanics    ***    '''    def saveCheck(self):        '''If the document has been changed since last clean state, ask if the user        wants to save the changes.'''        if QtGui.QMessageBox.question(self,                Earlybird save check,                Save unsaved changes first?,                QtGui.QMessageBox.Yes|QtGui.QMessageBox.No) == QtGui.QMessageBox.Yes:            return True        else:            return False    def saveTodoData(self):         '''Save data from the tree in json format'''        if self.filename:            dictModel = self.modelToDict()            with open(self.filename, 'w') as fileToWrite:                json.dump(dictModel, fileToWrite, indent=2)        else:            self.saveTodoDataAs()        self.undoStack.clear()    def saveTodoDataAs(self):        '''Save data in model as...x'''        dir = os.path.dirname(self.filename) if self.filename is not None else .        self.filename, flt = QtGui.QFileDialog.getSaveFileName(None,                EarlyBird: Load data file, dir, EarlyBird data (*.eb))                   if self.filename:            print Saving: , self.filename #for debugging            dictModel = self.modelToDict()            with open(self.filename, 'w') as fileToWrite:                json.dump(dictModel, fileToWrite, indent=2)        self.undoStack.clear()    def modelToDict(self):  #def modelToDict(self, parentItem = self.rootItem):        '''Takes model presently in view, and saves all data as dictionary.        Called by self.saveTodoData() and self.saveTodoDataAs()'''        dictModel = {}               if self.rootItem.rowCount():                       dictModel[taskblocks]= self.createTaskblockList(self.rootItem)            return dictModel    def createTaskblockList(self, parentItem):        '''Creates list of task blocks, and their tasks (latter using createTasklist).        Called by modelToDict which is used to save the model as a dictionary'''        numChildren = parentItem.rowCount()        if numChildren:            taskblockList = [None] * numChildren            childList = self.getChildren(parentItem)            for childNum in range(numChildren):                childItem = childList[childNum]                childTaskblockData = {}                childTaskblockData[blockname]=childItem.text()                               #now see if the block has children (tasks)                if childItem.rowCount():                    childTaskblockData[tasks] = self.createTaskList(childItem)                taskblockList[childNum] = childTaskblockData            return taskblockList        else:            return None    def createTaskList(self, parentItem):        '''Recursively traverses model creating list of tasks to        be saved as json'''        numChildren = parentItem.rowCount()        if numChildren:            taskList = [None] * numChildren            childList = self.getChildren(parentItem)            for childNum in range(numChildren):                childItem = childList[childNum]                childTaskData = {}                childTaskData[name] = childItem.text()                childTaskData[done] = True if childItem.checkState() else False                #now see if the present child has children                if childItem.rowCount():                    childTaskData[tasks] = self.createTaskList(childItem)                taskList[childNum] = childTaskData            return taskList        else:            return None    def getChildren(self, parentItem):        '''Returns list of child items of parentItem. Used when converting        model to dictionary for saving as json'''        numChildren = parentItem.rowCount()        if numChildren > 0:            childItemList = [None] * numChildren            for childNum in range(numChildren):                childItemList[childNum] = parentItem.child(childNum, 0)        else:            childItemList = None        return childItemListclass CommandTextEdit(QtGui.QUndoCommand):    '''Command for undoing/redoing text edit changes, to be placed in undostack'''    def __init__(self, earlybirdTree, item, oldText, newText, description):        QtGui.QUndoCommand.__init__(self, description)        self.item = item        self.tree = earlybirdTree        self.oldText = oldText        self.newText = newText    def redo(self):              self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot)         self.item.setText(self.newText)        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot)     def undo(self):        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot)         self.item.setText(self.oldText)        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) class CommandCheckStateChange(QtGui.QUndoCommand):    '''Command for undoing/redoing check state changes, to be placed in undostack'''    def __init__(self, earlybirdTree, item, oldCheckState, newCheckState, description):        QtGui.QUndoCommand.__init__(self, description)        self.item = item        self.tree = earlybirdTree        self.oldCheckState = QtCore.Qt.Unchecked if oldCheckState == 0 else QtCore.Qt.Checked        self.newCheckState = QtCore.Qt.Checked if oldCheckState == 0 else QtCore.Qt.Unchecked    def redo(self):        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot)         self.item.setCheckState(self.newCheckState)        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot)     def undo(self):        self.item.model().itemDataChanged.disconnect(self.tree.itemDataChangedSlot)        self.item.setCheckState(self.oldCheckState)        self.item.model().itemDataChanged.connect(self.tree.itemDataChangedSlot) def main():    ebApp = QtGui.QApplication(sys.argv)    firstEb = EarlybirdTree(filename = testFile.eb)    firstEb.show()    undoView = QtGui.QUndoView(firstEb.undoStack)    undoView.show()    sys.exit(ebApp.exec_())if __name__ == __main__:    main()earlybirdMain.py# -*- coding: utf-8 -*-earlybirdMain.py:     A wrapper for the EarlybirdTree class (defined in earlybirdTree.py).    The wrapper allows for simple menu/toolbar-based user interaction    with an earlybird to do tree. Includes undo/redo functionality.import sysimport osfrom PySide import QtGui, QtCorefrom earlybirdTree import EarlybirdTreeclass EarlybirdMain(QtGui.QMainWindow):    '''Main window to wrap an EarlybirdTree'''    def __init__(self, filename = None):        QtGui.QMainWindow.__init__(self)        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)         self.view = EarlybirdTree(self, filename)         self.model = self.view.model        self.windowTitleSet()        self.setCentralWidget(self.view)        self.createStatusBar()        self.createActions()        self.createToolbars()        self.createMenus()    def createToolbars(self):        '''Create toolbars for actions on files and items'''        self.fileToolbar = self.addToolBar(File actions)        self.fileToolbar.addAction(self.fileNewAction)        self.fileToolbar.addAction(self.fileOpenAction)        self.fileToolbar.addAction(self.fileSaveAction)        self.fileToolbar.addAction(self.fileSaveAsAction)        self.itemToolbar = self.addToolBar(Item actions)        self.itemToolbar.addAction(self.undoAction)        self.itemToolbar.addAction(self.redoAction)    def closeEvent(self, event):        '''If data has been changed, ask user if they want to save it'''        if not self.view.undoStack.isClean() and self.view.saveCheck():            self.view.fileSave()        self.close()    def createMenus(self):        '''Create menu for actions on files'''        self.fileMenu = self.menuBar().addMenu(&File)        self.fileMenu.addAction(self.fileOpenAction)            self.fileMenu.addAction(self.fileNewAction)        self.fileMenu.addAction(self.fileSaveAction)        self.fileMenu.addAction(self.fileSaveAsAction)    def createActions(self):        '''Create all actions to be used in toolbars/menus: calls createAction()'''        #File actions        self.fileNewAction = self.createAction(&New, slot = self.newFile,                shortcut = QtGui.QKeySequence.New, tip = New file,                status = Create a new file)        self.fileOpenAction = self.createAction(&Open..., slot = self.fileOpen,                shortcut = QtGui.QKeySequence.Open, tip = Open file,                status = Open an existing earlybird tree)        self.fileSaveAction = self.createAction(&Save, slot = self.fileSave,                shortcut = QtGui.QKeySequence.Save, tip = Save file,                status = Save file)        self.fileSaveAsAction = self.createAction(Save &As, slot = self.fileSaveAs,                shortcut = QtGui.QKeySequence.SaveAs, tip = Save file as, status = Save file as)        #Item actions        self.undoAction = self.createAction(Undo, slot = self.view.undoStack.undo,               shortcut = QtGui.QKeySequence.Undo, tip = Undo,               status = Undo changes)            self.redoAction = self.createAction(Redo, slot = self.view.undoStack.redo,               shortcut = QtGui.QKeySequence.Redo, tip = Redo,               status = Redo changes)    def createAction(self, text, slot=None, shortcut=None,                      tip=None, status = None):        '''Function called to create each individual action'''        action = QtGui.QAction(text, self)        if shortcut is not None:            action.setShortcut(shortcut)        if tip is not None:            action.setToolTip(tip)        if status is not None:            action.setStatusTip(status)        if slot is not None:            action.triggered.connect(slot)        return action     def createStatusBar(self):                                  self.status = self.statusBar()        self.status.setSizeGripEnabled(False)        self.status.showMessage(Ready)            def fileSaveAs(self):        self.view.saveTodoDataAs()        self.windowTitleSet()    def fileSave(self):        if self.view.filename:                    self.view.saveTodoData()        else:            self.view.saveTodoDataAs()            self.windowTitleSet()    def fileOpen(self):        '''Load earlybird file from memory.'''        if self.view.loadEarlybirdFile():            self.model = self.view.model              self.windowTitleSet()            if self.view.filename:                filenameNopath = QtCore.QFileInfo(self.view.filename).fileName()                self.status.showMessage(Opened file: {0}.format(filenameNopath))    def newFile(self):        '''Opens new blank earlybird file'''        self.view.newFile()        self.windowTitleSet()    def windowTitleSet(self):        '''Displays filename as window title, if it exists.'''        if self.view.filename:            self.setWindowTitle(Earlybird - {}[*].format(os.path.basename(self.view.filename)))        else:            self.setWindowTitle(Earlybird - <untitled>)def main():       ebApp = QtGui.QApplication(sys.argv)    mainEb = EarlybirdMain(filename = None)#simpleTodo.eb    mainEb.show()    undoView = QtGui.QUndoView(mainEb.view.undoStack)    undoView.show()    sys.exit(ebApp.exec_())if __name__ == __main__:    main()testFile.eb{  taskblocks: [    {      tasks: [        {          done: false,           name: Rake leaves        },         {          done: true,           name: Eat dinner        }      ],       blockname: Home    },     {      tasks: [        {          done: false,           name: Analysis        },         {          tasks: [            {              done: false,               name: Start Github project            },             {              done: true,               name: Write readme.md            },             {              done: false,               name: Implement functions            }          ],           done: false,           name: Graphing project        }      ],       blockname: Work    }  ]}The toughest part of the application to figure out was the undo/redo functionality. I have asked three questions about this at SO before finally settling on the strategy above.
To do tree application with undo/redo functionality
python;json;to do list;pyqt;pyside