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.