The Document Manager

The DocManager class is one of the more complex classes discussed in this book.

Example 15-2. The document manager class

"""
    docmanager.py — manager class for document/view mappings

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl

"""

from qt import *

TRUE=1
FALSE=0


class DocManagerError(Exception):pass

class NoSuchDocumentError(DocManagerError):

    ERR = "Document %s with title %s is not managed by this DocumentManager"

    def __init__(self, document):
        self.errorMessage = ERR % (str(document), document.title(), str())

    def __repr__(self):
        return self.errorMessage

    def __str__(self):
        return self.errorMessage

class DocumentsRemainingError(DocManagerError):

    def __init__(self, document):
        self.errorMessage = "There are still documents remaining."

    def __repr__(self):
        return self.errorMessage

    def __str__(self):
        return self.errorMessage
    

If you have a complex class like the document manager, it is often useful to create a few specific exception classes. You can still raise exceptions that will be mere messages in a string — but these have been deprecated since Python 2.0. For the document manager we have a small hierarchy of exceptions, with a base exception (DocManagerError), and two specific exceptions, NoSuchDocumentError and DocumentsRemainingError. The first exception is raised when an attempt is made to delete a document which is not managed by the document manager. This can happen when you need more than one document manager, for instance. The second is raised when an attempt is made to delete all open documents, but one or more of them could not be closed.

class DocManager(QObject):
    """
    The DocManager manages the creation and removal of documents
    and views.
    """
    def __init__(self, parent, viewManager = None):
        QObject.__init__(self)
        self._viewToDocMap = {}
        self._docToViewMap = {}
        self._parent=parent
        if viewManager:
            self._viewManager = viewManager
        else:
            self._viewManager = parent
    

Two very simple datastructures manage all the information in the document manage. The first is _viewToDocMap, which maps documents to views (one document can be associated with a list of views). The other datastructure, _docToViewMap, maps views to documents. Note the single underscore before the variable names; this indicates that you shouldn't try to use the variable outside its class, in this case DocManager. The viewManager is the object that collects all views and shows them in the application letterbox between toolbars and statusbars.

    def numberOfDocuments(self):
        return len(self._docToViewMap)

    def numberOfViews(self):
        return len(self._viewToDocMap)

    def views(self, document):
        return self._docToViewMap[document]

    def _createView(self, document, viewClass):
        view = viewClass(self._viewManager,
                         document,
                         None,
                         QWidget.WDestructiveClose)
        view.installEventFilter(self._parent)
        if self._viewToDocMap == {}:
            view.showMaximized()
        else:
            view.show()
        if self._docToViewMap.has_key(document):
            index = len(self._docToViewMap[document]) + 1
        else:
            index = 1
        view.setCaption(document.title() + " %s" % index)
        return view
    

The function _createView(self, document, viewClass) not only maps views to documents, but also creates the view objects. Note the QWidget.WDestructiveClose flag — if this is not passed to the QWidget-derived view class, the view will not disappear from the screen when closed! If this view is the first, then it it will be maximized. This is one area where the docmanager still assumes a traditional MDI paradigm — we'll massage this out in the next chapter. Note also that we keep count of the number of views in each document, and then set the caption accordingly.

Note also that we ‘install' the event filter of the parent object — that is, the application — in the view. This overrides the default event handling of the view object, and makes it possible to use the document manager object.

    def createDocument(self, documentClass, viewClass):
        document = documentClass()
        view = self._createView(document, viewClass)
        if self._docToViewMap.has_key(document):
            self._docToViewMap[document].append(view)
        else:
            self._docToViewMap[document] = [view]
        self._viewToDocMap[view] = document
        self.emit(PYSIGNAL("sigNumberOfDocsChanged"),())
        return document
    

The createDocument(self, documentClass, viewClass) command actually instantiates the document. When that's done, a view is created and mapped to the document. Note the signal we emit here: it can be useful for the application object to know that the number of documents has been changed. For instance, the "save" menu option must be enabled when the first document is created.

    def addView(self, document, viewClass):
        if self._docToViewMap.has_key(document):
            view = self._createView(document, viewClass)
            self._docToViewMap[document].append(view)
            self._viewToDocMap[view] = document
            return view
        else:
            raise DocManagerError(document)
    

Adding a new view to an existing document is fairly simple: just create the view and map it to a document, and vice versa. Note that if the document does not exist, we raise a DocManagerError — the document object apparently doesn't belong to this manager.

    def addDocument(self, document, viewClass):
        view = self._createView(document, viewClass)
            
        if self._docToViewMap.has_key(document):
            self._docToViewMap[document].append(view)
        else:
            self._docToViewMap[document] = [view]
        self._viewToDocMap[view] = document
        self.emit(PYSIGNAL("sigNumberOfDocsChanged"),())
        return view
    

Of course, it must be possible to add an existing document to the document manager. This is used when the user opens a document.

    def activeDocument(self):
        if self._viewManager.activeWindow() is not None:
            return self._viewToDocMap[self._viewManager.activeWindow()]
        else:
            return None
    

Since the QWorkSpace class, which is the model for the view manager, knows which window is active, we can use that to determine which document is active.

    def _saveDocument(self, document):
        if document.pathName() == None:
            document.setPathName(self._parent.queryFileName(document))
        try:
            document.save()
        except Exception, e:
            QMessageBox.critical(self,
                                 "Error",
                                 "Could not save the current document: " + e)
            raise e
    

The things that can go wrong when trying to save a document are manifold — however, we assume that the document knows when to shout "Exception". If that happens, the user is informed, and the exception re-raised.

    def _queryCloseDocument(self, document):
        if self._parent.queryCloseDocument(document) == QMessageBox.No:
            return FALSE
        if document.modified():
            save = self._parent.querySaveDocument(document)
            if save == QMessageBox.Yes:
                try:
                    self._saveDocument(document)
                    return TRUE
                except Exception, e:
                    if self._parent.queryDiscardDocument(document) <> \
                       QMessageBox.Yes:
                        return FALSE
                    else:
                        return TRUE
            elif save == QMessageBox.No:
                return TRUE
            elif save == QMessageBox.Cancel:
                return FALSE
        return TRUE
    

The aim of _queryCloseDocument is to determine what the user really wants when he closes a document—an action that can throw quite a few wobblies. At every step the function asks the user what he wants. Does he want to save the data? And in case saving doesn't succeed, does he want to discard the document? Or would he prefer to keep the document open, and go on throwing foul looks at an application that contains his precious data, which he cannot save?

    def _removeView(self, view, document):
        try:
            self._docToViewMap[document].remove(view)
            del self._viewToDocMap[view]
        except ValueError, e:
            pass # apparently already deleted

    def closeView(self, view):
        document=self._viewToDocMap[view]
        if len(self._docToViewMap[document])==1:
            if self._queryCloseDocument(document):
                self._removeView(view, document)
                del self._docToViewMap[document]
                return TRUE
            else:
                return FALSE
        else:
            self._removeView(view, document)
            return TRUE

    def closeDocument(self, document):
        l=self._docToViewMap[document][:]
        for view in l:
            if view.close(TRUE) == FALSE:
                return FALSE
        self.emit(PYSIGNAL("sigNumberOfDocsChanged"),())
        return TRUE

    def closeAllDocuments(self):
        for document in self._docToViewMap.keys():
            if not self.closeDocument(document):
                raise DocumentsRemainingError()
    

Getting rid of documents and views can become quite complicated if you take into consideration all the various methods available: a user can click on the close button in the titlebar of the application, or in the view, or activate the "close" QAction. In order to catch the first possibility, we need to use event filters. Clicking on the close button does not generate a signal we can connect to. That being so, we should only call close() on the view, if we know that the closing has not been initiated through the event filter (otherwise we would fire the event filter again).

However, when the user selects "close document" or "close all documents" from the menu or the toolbar, close() will not be automatically called on the view — we have to do this ourselves. By looping through all views in the document, and closing them, we will generate an event: the event will be handled by the event filter, which will call closeView() for us. And closeView() will ask the user whether it really wants to close the document if the view is the last one.

It's an interesting exercise to follow this happening with the BlackAdder debugger.