The actual application

As with the document-view framework, you can view the QMainWindow derived class as the central application controller. It takes the user input and translates that to calls to either the data or the GUI interface.

Even though the MDIApp class might appear a bit complex (and certainly very long!) it is much simpler than it would be with everything from the DocManager added to it. The creation of QActions, and the attendant fringe decorations such as menu's and toolbars, is quite standard:

Example 15-5. The application class

"""
mdiapp.py — application class for the mdi framework

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

from qt import *

from mdiview import MDIView
from mdidoc import MDIDoc
from docmanager import DocManager

from resources import *

class MDIApp(QMainWindow):
    """
    MDIApp combines MDIDoc and MDIView into an single
    window, multiple sub-window, multiple document application.
    """
    def __init__(self, *args):
        apply(QMainWindow.__init__,(self, ) + args)
        self.setCaption("MDI Application Framework")
        self.workspace = self.initWorkSpace()

        self.docManager=DocManager(self, self.workspace)
        self.connect(self.docManager,
                     PYSIGNAL("sigNumberOfDocsChanged"),
                     self.setActionsEnabled)
        self.initActions()
        self.initMenuBar()
        self.initToolBar()
        self.initStatusBar()
        self.setActionsEnabled()
    #
    # GUI initialization
    #
        
    def initActions(self):
        fileNewIcon=QIconSet(QPixmap(filenew))
        fileQuitIcon=QIconSet(QPixmap(filequit))
        fileOpenIcon=QIconSet(QPixmap(fileopen))
        fileSaveIcon=QIconSet(QPixmap(filesave))
        
        self.actions = {}
  
        self.actions["fileNew"] = QAction("New",
                                           fileNewIcon,
                                           "&New",
                                           QAccel.stringToKey("CTRL+N"),
                                           self)
        self.connect(self.actions["fileNew"],
                     SIGNAL("activated()"),
                     self.slotFileNew)


        self.actions["fileOpen"] = QAction("Open",
                                           fileOpenIcon,
                                           "&Open",
                                           QAccel.stringToKey("CTRL+O"),
                                           self)
        self.connect(self.actions["fileOpen"],
                     SIGNAL("activated()"),
                     self.slotFileOpen)

        self.actions["fileSave"] = QAction("Save",
                                           fileSaveIcon,
                                           "&Save",
                                           QAccel.stringToKey(""),
                                           self)
        self.connect(self.actions["fileSave"],
                     SIGNAL("activated()"),
                     self.slotFileSave)

        self.actions["fileSaveAs"] = QAction("Save as",
                                             fileSaveIcon,
                                             "&Save as",
                                             QAccel.stringToKey(""),
                                             self)
        self.connect(self.actions["fileSaveAs"],
                     SIGNAL("activated()"),
                     self.slotFileSaveAs)

        self.actions["fileClose"] = QAction("Close",
                                            "&Close Document",
                                           QAccel.stringToKey("CTRL+W"),
                                           self)
        self.connect(self.actions["fileClose"],
                     SIGNAL("activated()"),
                     self.slotFileClose)
        
        self.actions["fileQuit"] = QAction("Exit",
                                           fileQuitIcon,
                                           "E&xit",
                                           QAccel.stringToKey("CTRL+Q"),
                                           self)
        self.connect(self.actions["fileQuit"],
                     SIGNAL("activated()"),
                     self.slotFileQuit)

        self.actions["editDoc"] = QAction("Edit",
                                           fileQuitIcon,
                                           "&Edit",
                                           QAccel.stringToKey("CTRL+E"),
                                           self)
        self.connect(self.actions["editDoc"],
                     SIGNAL("activated()"),
                     self.slotEditDoc)

        self.actions["windowCloseWindow"] = QAction(self)
        self.actions["windowCloseWindow"].setText("Close Window")
        self.actions["windowCloseWindow"].setAccel(QAccel.
                                                   stringToKey("CTRL+W"))
        self.actions["windowCloseWindow"].setMenuText("&Close Window")
        self.connect(self.actions["windowCloseWindow"],
                     SIGNAL("activated()"),
                     self.slotWindowCloseWindow)

        self.actions["windowNewWindow"] = QAction(self)
        self.actions["windowNewWindow"].setText("New Window")
        self.actions["windowNewWindow"].setMenuText("&New Window")
        self.connect(self.actions["windowNewWindow"],
                     SIGNAL("activated()"),
                     self.slotWindowNewWindow)

        self.actions["windowCascade"] = QAction(self)
        self.actions["windowCascade"].setText("Cascade")
        self.actions["windowCascade"].setMenuText("&Cascade")
        self.connect(self.actions["windowCascade"],
                     SIGNAL("activated()"),
                     self.workspace.cascade)
  
        self.actions["windowTile"] = QAction(self)
        self.actions["windowTile"].setText("Tile")
        self.actions["windowTile"].setMenuText("&Tile")
        self.connect(self.actions["windowTile"],
                     SIGNAL("activated()"),
                     self.workspace.tile)

        self.actions["windowAction"] = QActionGroup(self, None, FALSE)
        self.actions["windowAction"].insert(self.actions["windowCloseWindow"])
        self.actions["windowAction"].insert(self.actions["windowNewWindow"])
        self.actions["windowAction"].insert(self.actions["windowCascade"])
        self.actions["windowAction"].insert(self.actions["windowTile"])

        self.actions["helpAboutApp"] = QAction(self)
        self.actions["helpAboutApp"].setText("About")
        self.actions["helpAboutApp"].setMenuText("&About...")
        self.connect(self.actions["helpAboutApp"],
                     SIGNAL("activated()"),
                     self.slotHelpAbout)

    

The set of actions included in this framework is not complete, of course. Ideally, you would want accelerators for switching between views, and a lot of application specific actions. We'll be adding these over the next few chapters.

    def initMenuBar(self):
        self.fileMenu = QPopupMenu()
        self.actions["fileNew"].addTo(self.fileMenu)
        self.actions["fileOpen"].addTo(self.fileMenu)
        self.actions["fileSave"].addTo(self.fileMenu)
        self.actions["fileSaveAs"].addTo(self.fileMenu)
        self.actions["fileClose"].addTo(self.fileMenu)
        self.fileMenu.insertSeparator()
        self.actions["fileQuit"].addTo(self.fileMenu)
        self.menuBar().insertItem("&File", self.fileMenu)

        self.editMenu = QPopupMenu()
        self.actions["editDoc"].addTo(self.editMenu)
        self.menuBar().insertItem("&Edit", self.editMenu)

        self.windowMenu = QPopupMenu()
        self.windowMenu.setCheckable(TRUE)
        self.connect(self.windowMenu,
                     SIGNAL("aboutToShow()"),
                     self.slotWindowMenuAboutToShow)
        self.menuBar().insertItem("&Window", self.windowMenu)
        
        self.helpMenu = QPopupMenu()
        self.actions["helpAboutApp"].addTo(self.helpMenu)
        self.menuBar().insertItem("&Help", self.helpMenu)
        
    def initToolBar(self):
        self.fileToolbar = QToolBar(self, "file operations")
        self.actions["fileNew"].addTo(self.fileToolbar)
        self.actions["fileQuit"].addTo(self.fileToolbar)
        QWhatsThis.whatsThisButton(self.fileToolbar)

    def initStatusBar(self):
        self.statusBar().message("Ready...")
    

We have created menus, toolbars and statusbars so often by now that this is merely an exercise in cutting and pasting. However, note that we create a Window menu, but we don't add the actions to that menu. This is because the contents of the window menu are dynamic. Just before showing the window menu, when the signal "aboutToShow()" is emitted, we will be building the menu from the list of views managed by the document manager. This is done in the slotWindowMenuAboutToShow slot function.

    def initWorkSpace(self):
        workspace=QWorkspace(self)
        self.setCentralWidget(workspace)
        return workspace
    

For now, the view manager is simply an instance of QWorkSpace, which is a very simple class that manages widgets as sub-windows to itself. For it to manage widgets, they should be created with the workspace as parent. QWorkSpace has two methods: activeWindow(), which returns the widget that currently has focus, and windowList(), which returns the list of all windows.

Furthermore, there are two slots: cascade() and tile(), that arrange the widgets managed by the workspace. Lastly, there is one signal you can connect to: windowActivated(), which is fired whenever a widget is activated — i.e. gets focus.

    def setActionsEnabled(self):
        enabled = self.docManager.numberOfDocuments()
        self.actions["fileSave"].setEnabled(enabled)
        self.actions["fileClose"].setEnabled(enabled)
        self.actions["editDoc"].setEnabled(enabled)
    

If there is no document loaded by the application, functions like ‘save', ‘close' or ‘edit' are not terribly relevant. It's better to disable them then. By requesting the number of documents managed by the document manager, we can easily achieve this. After all, no documents is zero, which is false for Python, and more than zero documents is always true.

The next section is concerned with the implementation of the slots called by the QAction objects that we just created:

    #
    # Slot implementations
    #

    def slotFileNew(self):
        document = self.docManager.createDocument(MDIDoc, MDIView)
    

Creating a document is now simply a matter of asking the document manager to do it — just as we did in the test script.

    def slotFileOpen(self):
        fileName = QFileDialog.getOpenFileName(None, None, self)

        if not fileName.isEmpty():
            document=MDIDoc()
            document.open(fileName)
            view = self.docManager.addDocument(document, MDIView)
            view.setFocus()
    

Opening a file is slightly more complicated; we need to be sure that the user actually selected a file before a file can be opened. Remember that all Qt classes return QString objects, not Python string objects. As a result, we have to use isEmpty() instead of comparing with None.

If the filename is not empty, we create an empty document, ask that document to open the file, and then add the document to the document manager. Of course, this complexity can also be removed to the document manager, by adding an openDocument(self, fileName, documentClass, viewClass) function to DocManager.

    def slotFileSave(self, document=None):
        if document == None:
            document = self.docManager.activeDocument()
        if document.pathName() == None:
            self.slotFileSaveAs()
        else:
            try:
                document.save()
            except Exception, e:
                QMessageBox.critical(self,
                                     "Error",
                                     "Could not save the current document")

    def slotFileSaveAs(self, doc=None):
        fileName = QFileDialog.getSaveFileName(None, None, self)
        if not fileName.isEmpty():
            if doc == None:
                doc = self.docManager.activeDocument()
            try:
                doc.save(str(fileName))
            except:
                QMessageBox.critical(self,
                                     "Error",
                                     "Could not save the current document")
    

Saving a document entails some complexity: the document may or may not have a filename; if not, the user should supply one. Saving could fail for a variety of reasons. Nothing is so frustrating as losing your data because you simply wanted to save it. An application should handle save errors very carefully to ensure no data is lost.

    def slotFileClose(self):
        doc=self.docManager.activeDocument()
        self.docManager.closeDocument(doc)

    def slotFileQuit(self):
        try:
            self.docManager.closeAllDocuments()
        except:
            return
        qApp.quit()
    

Closing a document and quitting the application are closely related processes. Note the call to qApp.quit() — this is only reached when closing all documents succeeds.

    def slotEditDoc(self):
        doc = self.docManager.activeDocument()
        doc.slotModify()

    def slotWindowCloseWindow(self):
        self.workspace.activeWindow().close()
    

Closing a single window might mean that the document will be closed, too — if it is the last or only view the document has. By retrieving the active window from the workspace, and calling the close() function on it, a closeEvent will be generated. This will be caught by the event filter defined below, which calls the appropriate functions in the document manager.

    def slotWindowNewWindow(self):
        doc = self.docManager.activeDocument()
        self.docManager.addView(doc, MDIView)

    def slotHelpAbout(self):
        QMessageBox.about(self,
                          "About...",
                          "MDI Framework\n" +
                          "Inspired by the KDevelop templates.\n" +
                          "(c) 2001 by Boudewijn Rempt")
    

Adding a new window is very simple: retrieve the currently active document, and ask the document manager to add a view for that document.

    def slotWindowMenuAboutToShow(self):
        self.windowMenu.clear()
        self.actions["windowNewWindow"].addTo(self.windowMenu)
        self.actions["windowCascade"].addTo(self.windowMenu)
	self.actions["windowTile"].addTo(self.windowMenu)
        self.windowMenu.insertSeparator()
        self.actions["windowCloseWindow"].addTo(self.windowMenu)

        if self.workspace.windowList()==[]:
            self.actions["windowAction"].setEnabled(FALSE)
        else:
            self.actions["windowAction"].setEnabled(TRUE)
        self.windowMenu.insertSeparator()

        i=0 # window numbering
        self.menuToWindowMap={}
        for window in self.workspace.windowList():
            i+=1
            index=self.windowMenu.insertItem(("&%i " % i) +
                                             str(window.caption()),
                                             self.slotWindowMenuActivated)
            self.menuToWindowMap[index]=window
            if self.workspace.activeWindow()==window:
                self.windowMenu.setItemChecked(index, TRUE)

    def slotWindowMenuActivated(self, index):
        self.menuToWindowMap[index].setFocus()
    

Here, we dynamically create the window menu just before it is shown. The four menu options—new window, cascade, tile and close—are part of a single QActionGroup, and can be enabled or disabled together. Of course, the same could be done with the other actions that are only enabled when there are actually documents in existence. Note also that we add accelerators by numbering the views (this will, of course, stop being sensible once we have more than nine open windows).

    #
    # Toplevel event filter
    #

    def eventFilter(self, object, event):
        if (event.type() == QEvent.Close):
            if (object != self):
                if self.docManager.closeView(object):
                    event.accept()
                else:
                    event.ignore()
            else:
                try:
                    self.docManager.closeAllDocuments()
                    event.accept()
                except Exception, e:
                    return TRUE
        return QWidget.eventFilter(self, object, event)
    

Qt events contrast with Qt signals in that they are typically created by user actions, such as key presses or mouse actions. Signals are mostly emitted by objects on themselves.

An event filter is an object that receives all events for the object to which it applies. You can install eventfilters that are created for one object in other objects. In this case, all views share the same event filter as the application object. An eventfilter must return either true or false—true if the event should not be propagated further, and false if someone should handle the event.

Here, we check whether the event is of the type QEvent.close — if that is so, we check whether it is meant for the main application window (that's us— the self). In that case, all documents must be closed. This event is generated when the user closes the application.

If the event is meant for one of the sub-windows, the document manager is asked to close the view. If that is successful, the event is accept()-ed, and will not be propagated any further.

    #
    # Functions called from the document manager
    #
def queryCloseDocument(self, document):
        r = QMessageBox.information(self,
                                    str(self.caption()),
                                    "Do you want to close %s?" %
                                    document.title(),
                                    "Yes",
                                    "No",
                                    None,
                                    0, 1)
        if r == 0:
            return QMessageBox.Yes
        else:
            return QMessageBox.No

    def querySaveDocument(self, document):
        r = QMessageBox.information(self,
                                    str(self.caption()),
                                    "Do you want to save your changes to " +
                                    "%s?" %
                                    document.title(),
                                    "Yes",
                                    "No",
                                    "Cancel",
                                    0, 2)
        if r == 0:
            return QMessageBox.Yes
        elif r == 1:
            return QMessageBox.No
        else:
            return QMessageBox.Cancel

    def queryDiscardDocument(self, document):
        r = QMessageBox.warning(self,
                                str(self.caption()),
                                "Could not save %s.\n" % document.title() +
                                "Do you want to discard your changes?",
                                "Yes",
                                "No",
                                None,
                                0, 1)
        if r == 0:
            return QMessageBox.Yes
        else:
            return QMessageBox.No

    def queryFileName (self, document=None):
        fileName = QFileDialog.getSaveFileName(None, None, self)
        if not fileName.isEmpty():
            return str(fileName)
        else:
            return "untitled"
    

These calls to QMessageBox and the standard file dialog QFileDialog are made from the document manager. This makes sure that the document manager can also work without a GUI.

The QMessageBox class is a bit messy, by Qt standards. There are two ways of specifying buttons: by string, or by identity. These identities, like QMessageBox.Yes are defined in the class. If you use these constants in your calls to QMessageBox.warning(), for instance, then the return value will be the identity of the button pressed.

However, if you want the added flexibility of translatable strings, you cannot use the identities. You can call functions like QMessageBox.warning() with strings, but the return value will be the position of the key pressed, starting with 0 and going from left to right.

I want to use the identities in the document manager — this makes the code a lot clearer. But I wanted to use strings in the actual message boxes. That's why I translate the position of the button pressed to the correct identity.