The view

It is certainly possible to use Python and PyQt to write a custom editing component — you shoud probably base it on the QScrollView class. But making your own editor would entail a lot of very complicated work, mostly involved with datastructures to store text, text attributes, painting text and keeping track of the cursor position. And don't forget font handling, which gets complicated with Unicode. It would make quite an interesting project, but what's the use of a rich GUI library if you don't use it?

Therefore I propose to start out using the standard QMultiLineEdit widget. With PyQt for Qt 3.0, we can convert kalam to use the new editor widget, QTextEdit, which supports embedded pictures, hyperlinks and rich text. For now, we will have to be satisfied with plain text in a single font and a single color.

However, there is one problem with using a QMultiLineEdit editor widget as a view on the text: the widget itself contains a copy of the text. A QMultiLineEdit is a conflation of document and view. This means that we will have to synchronize the text in the document and in the view — recall that with our framework there can be more than one view on the same document. It is inevitable that we waste a lot of time copying text between views and documents. This shows that we should have implemented our own editor widget, one that is based on a separation of GUI and data.

The initial wrapping of QMultiLineEdit is pretty easy:

"""
kalamview.py - the editor view component for Kalam

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
from resources import TRUE, FALSE


class KalamMultiLineEdit(QMultiLineEdit):
    
    def event(self, e):
        if e.type() == QEvent.KeyPress:
            QMultiLineEdit.keyPressEvent(self, e)
            return TRUE
        else:
            return QMultiLineEdit.event(self, e)
    

By default the QWidget's event() function filters out all tab (and shift-tab) presses. Those keys are used for focus management, and move the focus to the next widget. This is not what we want in an editor, where pressing tab should insert a TAB character in the text. By overriding the default event() function, we can correct this behavior. If the type—and there are more than seventy event types in PyQt—is QEvent.KeyPress, we send the event directly to the keyPressEvent method, instead of moving focus. In all other cases, we let our parent class, QMultiLineEdit handle the event.

The view class encapsulates the editor widget we previously created:

class KalamView(QWidget):
    """
    The KalamView class can represent object of class
    KalamDoc on screen, using a standard edit control.

    signals:
          sigCaptionChanged
    """
    def __init__(self, parent, doc, *args):
        apply(QWidget.__init__,(self, parent) + args)

        self.layout=QHBoxLayout(self)
        self.editor=KalamMultiLineEdit(self)
        self.layout.addWidget(self.editor)
        self.doc = doc
        self.editor.setText(self.doc.text())

        self.connect(self.doc,
                     PYSIGNAL("sigDocTitleChanged"),
                     self.setCaption)
        self.connect(self.doc,
                     PYSIGNAL("sigDocTextChanged"),
                     self.setText)
        self.connect(self.editor,
                     SIGNAL("textChanged()"),
                     self.changeDocument)

        self._propagateChanges = TRUE
    

The basic view is a plain QWidget that contains a layout manager (QHBoxLayout) that manages a KalamMultiLineEdit widget. By strictly wrapping the KalamMultiLineEdit functionality, instead of inheriting and extending, it will be easier to swap this relatively underpowered component for something with a bit more oomph and espieglerie, such as QTextEdit or KDE's editor component, libkwrite. Or, perhaps, a home-grown editor component we wrote in Python...

In the framework, we set the background color initially to green; the same principle holds here, only now we set the text initially to the text of the document.

The first two connections speak for themselves: if the title of the document changes, the caption of the window should change; and if the text of the document changes (perhaps through editing in another view), our text should change, too.

The last connection is a bit more interesting. Since we are wrapping a QMultiLineEdit in the KalamView widget, we have to pass changes in the editor to the outside world. The textChanged() signal is fired whenever the user changes the text in a QMultiLineEdit widget (for instance, by pasting a string or by typing characters).

When you use functions that are not defined as slots in C++ to change the text programmatically, textChanged() is not emitted. We will wrap these functions and make them emit signals, too.

    def setCaption(self, caption):
        QWidget.setCaption(self, caption)
        self.emit(PYSIGNAL("sigCaptionChanged"),
                  (self, caption))

    def document(self):
        return self.doc

    def closeEvent(self, e):
        pass

    def close(self, destroy=0):
        return QWidget.close(self, destroy)

    def changeDocument(self):
        if self._propagateChanges:
            self.doc.setText(self.editor.text(), self)

    def setText(self, text, view):
        if self != view:
            self._propagateChanges = FALSE
            self.editor.setText(text)
            self._propagateChanges = TRUE
      

The function changeDocument() is called whenever the textChanged() signal is emitted by the editor widget. Since we have a reference to the document in every view, we can call setText on the document directly. Note that we pass the document the changed text and a reference to this view.

The document again passes the view reference on when a sigDocTextChanged Python signal is emitted from the document. This signal is connected to all views that represent the document, and makes sure that the setText() function is called.

In the setText() function the view reference is used to check whether the changes originate from this view: if that is so, then it is nonsense to change the text. If this view is currently a 'slave' view — then the text of the QMultiLineEdit should be updated. Updating the text causes a textChanged() signal to be emitted — creating a recursion into oblivion.

To avoid the recursion, you can use the flag variable _propagateChanges. If this variable is set to FALSE, then the changeDocument() will not call the setText() function of the document.

Another solution would be to temporarily disconnect the textChanged() signal from the changeDocument() function. Theoretically, this would give a small performance benefit, since the signal no longer has to be routed nor the function called— but in practice, the difference is negligible. Connecting and disconnecting signal takes some time, too. Try the following alternative implementation of setText():

    def setText(self, text, view):
        if self != view:
            self.disconnect(self.editor,
                            SIGNAL("textChanged()"),
                            self.changeDocument)
            self.editor.setText(text)
            self.connect(self.editor,
                         SIGNAL("textChanged()"),
                         self.changeDocument)
    

Note that changing the text of a QMultiLineEdit does not change the cursor position in the editor. This makes life a lot easier, because otherwise we would have to move the cursor back to the original position ourselves in all dependent views. After all, the purpose of having multiple views on the same document is to enable the user to have more than one cursor location at the same time.