We will start with a preferences dialog.
Nowadays, the taste is for dialogs with a strip of icons to the
left that somehow indicates what section should be shown. But we
will start out with a simple tabbed dialog that PyQt supports
out of the box, and for which we don't have to draw icons
(that's always the difficult bit, creating the artwork).
Creating the settings dialog window
The first part of the drill is well
known: compile the frmsettings.ui file to
Python using pyuic.
pyuic -x frmsettings.ui > frmsettings.py
You can either call this generated
dialog directly from KalamApp, or you
can subclass it and add some intelligence. Since intelligence
is what is needed to synchronize the switches between
interface paradigm, we will go ahead and add subclass the
design and add some.
"""
dlgsettings.py - Settings dialog for Kalam.
See: frmsettings.ui
copyright: (C) 2001, Boudewijn Rempt
email: boud@rempt.xs4all.nl
"""
import os, sys
from qt import *
import kalamconfig
from frmsettings import FrmSettings
class DlgSettings(FrmSettings):
def __init__(self,
parent = None,
name = None,
modal = 0,
fl = 0):
FrmSettings.__init__(self, parent, name, modal, fl)
self.textFont = kalamconfig.get("textfont")
self.textBackgroundColor = kalamconfig.get("textbackground")
self.textForegroundColor = kalamconfig.get("textforeground")
self.MDIBackgroundColor = kalamconfig.get("mdibackground")
self.initEditorTab()
self.initInterfaceTab()
self.initDocumentTab()
The DlgSettings
dialog is a subclass of FrmSettings,
which we created with Designer. In the constructor we create
four objects for housekeeping purposes, to store changed
settings until the user chooses to apply them by pressing OK,
or to cancel.
These objects represent the editor font,
the editor text color, the editor background color and the
background color of the MDI workspace. As you can see from the
calls to kalamconfig, actually
implementing this dialog necessitated quite a few changes to
the kalamconfig module.
The full source of
kalamconfig is not so interesting for
this chapter, but it is available with the rest of the code.
To summarize the development: all settings are now retrieved
and set through a single pair of get/set functions. There are
a lot more settings, too. If a setting requires special
handling, then the relevant get/set function is retrieved from
a dictionary (you can just as easily store references to
functions or classes in a dictionary as in strings, since
everything is considered an object) and executed with
apply(). If a setting is changed, a
signal is emitted from the QApplication
instance, which can be reached with the global variable
qApp. Note how the actual signal identifier
is constructed dynamically:
#
# kalamconfig.py
# Get and set - set emits a signal via Config.notifier
#
customGetSetDictionary = {
"style" : (getStyle, setStyle),
"workspace" : (getWorkspace, setWorkspace),
"textfont" : (getTextFont, setTextFont),
"textforeground" : (getTextForegroundColor, setTextForegroundColor),
"textbackground" : (getTextBackgroundColor, setTextBackgroundColor),
"mdibackground" : (getMDIBackgroundColor, setMDIBackgroundColor),
}
def set(attribute, value):
if customGetSetDictionary.has_key(attribute):
apply(customGetSetDictionary[attribute][1], (value,))
else:
setattr(Config, attribute, value)
qApp.emit(PYSIGNAL("sig" + str(attribute) + "Changed"),
(value,))
def get(attribute):
if customGetSetDictionary.has_key(attribute):
value = apply(customGetSetDictionary[attribute][0])
else:
value = getattr(Config, attribute)
return value
But, let us continue with
dlgsettings.py. There are three tab
pages, and every tab pages has its own initialization function.
def initEditorTab(self):
self.txtEditorPreview.setFont(self.textFont)
pl = self.txtEditorPreview.palette()
pl.setColor(QColorGroup.Base, self.textBackgroundColor)
pl.setColor(QColorGroup.Text, self.textForegroundColor)
self.cmbLineWrapping.setCurrentItem(kalamconfig.get("wrapmode"))
self.spinLineWidth.setValue(kalamconfig.get("linewidth"))
self.connect(self.bnBackgroundColor,
SIGNAL("clicked()"),
self.slotBackgroundColor)
self.connect(self.bnForegroundColor,
SIGNAL("clicked()"),
self.slotForegroundColor)
self.connect(self.bnFont,
SIGNAL("clicked()"),
self.slotFont)
The editor tab shows a nice preview of
the font and color combination the user has chosen. Setting
these colors, however, is not as straightforward as you might
think. Qt widget colors are governed by a complex system based
around palettes. A palette (QPalette)
contains three color groups
(QColorGroup), one that is used if the
widget is active, one that is used if the widget is disabled,
and one that is used if the widget is inactive.
A QColorGroup in its turn, is a set
of colors with certain roles:
Background - general
background color.
Foreground - general
foreground color.
Base - background color text
entry widgets
Text - the foreground color
used with Base.
Button - general button
background color
ButtonText - for button
texts
Light - lighter than Button
color.
Midlight - between Button
and Light.
Dark - darker than Button.
Mid - between Button and
Dark.
Shadow - a very dark color.
Highlight - a color to
indicate a selected or highlighted item.
HighlightedText - a text
color that contrasts to Highlight.
All colors are normally calculated from
the Background color. Setting the background color of the
editor with the convenience function
setBackgroundColor() won't have an
effect; we must use the Base color in the relevant
QColorGroup.
This system is certainly quite complex,
but it allows for tremendous flexibility. Using it isn't too
arduous. First, we retrieve the palette from the editor
widget:
pl = self.txtEditorPreview.palette()
pl.setColor(QColorGroup.Base, self.textBackgroundColor)
pl.setColor(QColorGroup.Text, self.textForegroundColor)
Then we can use the function
setColor, which takes a colorgroup role
and a QColor as arguments. Note that if
we use these functions to change the colors of a widget after
it has been shown for the first time, we must call
repaint(TRUE) to force the widget to
redraw itself. Otherwise Qt's highly optimized drawing engine
becomes confused. This will be done in the slot function
that's connected to the clicked() signal
of the color choice buttons.
def initInterfaceTab(self):
self.initStylesCombo()
self.initWindowViewCombo()
self.lblBackgroundColor.setBackgroundColor(self.MDIBackgroundColor)
self.connect(self.bnWorkspaceBackgroundColor,
SIGNAL("clicked()"),
self.slotWorkspaceBackgroundColor)
The preview for the interface style is
initialized in initWindowViewCombo. Note
that QLabel is rather more simple in
its needs than QMultiLineEdit as
regards colors. Here, you can just use the convenience
function setBackgroundColor
(setEraseColor() in Qt 3) to show the
preview color for the MDI workspace.
def initDocumentTab(self):
self.initEncodingCombo()
self.chkAddNewLine.setChecked(kalamconfig.get("forcenewline"))
This must be the least complex tab, but
no doubt we will be adding to it during the course of our
development of Kalam.
def initStylesCombo(self):
self.cmbStyle.clear()
styles = kalamconfig.stylesDictionary.keys()
styles.sort()
try:
currentIndex = styles.index(kalamconfig.Config.style)
except:
currentIndex = 0
kalamconfig.setStyle(styles[0])
self.cmbStyle.insertStrList(styles)
self.cmbStyle.setCurrentItem(currentIndex)
self.connect(self.cmbStyle,
SIGNAL("activated(const QString &)"),
self.setStyle)
To make life a lot easer, we have
defined a dictionary that maps user-understandable style names
to QStyle classes in
kalamconfig. Note that we need, in order
to find out which one is the current style, not the result of
kalamconfig.get("style"), since that
returns a QStyle object, but the actual
string in the
Config.style
variable.
# kalamconfig.py - styles dictionary
stylesDictionary = {
"Mac OS 8.5" : QPlatinumStyle,
"Windows 98" : QWindowsStyle,
"Motif" : QMotifStyle,
"Motif+" : QMotifPlusStyle,
"CDE" : QCDEStyle
}
The keys of this dictionary are used to
fill the style combo. Python dictionaries are unordered, and
to ensure that the same style is alwas at the same place in
the combobox, we have to sort the list of keys. Sorting a list
is done in place in Python, and that
means that calling
sort() on a list doesn't return a list.
If we'd written:
styles = kalamconfig.stylesDictionary.keys().sort()
instead, styles would have been set to
None... Activating an entry in the styles
combobox emits a signal that is routed to the
setStyle() function:
def setStyle(self, style):
kalamconfig.set("style", str(style))
qApp.setStyle(kalamconfig.get("style")())
Changing a style is instantaneous in
Kalam, if only because it is fun to
run through all the styles and see the
application changing under your fingers. Therefore, we
immediately update the style setting, and call
qApp.setStyle() to propagate the changes
to the application widgets.
def initWindowViewCombo(self):
self.cmbWindowView.clear()
workspaces = kalamconfig.workspacesDictionary.keys()
workspaces.sort()
try:
currentIndex = workspaces.index(kalamconfig.Config.workspace)
except:
currentIndex = 0
kalamconfig.setWorkspace(workspaces[0])
self.cmbWindowView.insertStrList(workspaces)
self.cmbWindowView.setCurrentItem(currentIndex)
self.connect(self.cmbWindowView,
SIGNAL("activated(const QString &)"),
self.setWorkspacePreview)
Setting up the workspace selection
combobox is similar to setting up the styles combobox. The
only interesting point is the connection to
setWorkspacePreview. This function
updates the small image that shows what each option means.
These images were made from snapshots, and scaled down with
Pixie, a KDE graphics application (which is now
obsolete).
def setWorkspacePreview(self, workspace):
workspace = str(workspace) + ".png"
# XXX - when making installable, fix this path
pixmap = QPixmap(os.path.join("./pixmaps",
workspace))
self.pxViewSample.setPixmap(pixmap)
As you can see, application development
is messy, and I don't want to hide all the mess from you.
Later, when we make the application distributable in Chapter 26, we
will have to come back to this function and devise a way to
make Kalam retrieve its pictures
from the installation directory.
def initEncodingCombo(self):
self.cmbEncoding.clear()
encodings = kalamconfig.codecsDictionary.keys()
encodings.sort()
try:
currentIndex = encodings.index(kalamconfig.get("encoding"))
except:
currentIndex = 0
Config.encoding = encodings[0]
self.cmbEncoding.insertStrList(encodings)
self.cmbEncoding.setCurrentItem(currentIndex)
The list of encodings is defined in
kalamconfig, just like the list of
styles and interface types:
# kalamconfig.py - encodings dictionary
codecsDictionary = {
"Unicode" : "utf8",
"Ascii": "ascii",
"West Europe (iso 8859-1)": "iso-8859-1",
"East Europe (iso 8859-2)": "iso-8859-2",
"South Europe (iso 8859-3)": "iso-8859-3",
"North Europe (iso 8859-4)": "iso-8859-4",
"Cyrilic (iso 8859-5)": "iso-8859-5",
"Arabic (iso 8859-6)": "iso-8859-6",
"Greek (iso 8859-7)": "iso-8859-7",
"Hebrew (iso 8859-8)": "iso-8859-8",
"Turkish (iso 8859-9)": "iso-8859-9",
"Inuit (iso 8859-10)": "iso-8859-10",
"Thai (iso 8859-11)": "iso-8859-11",
"Baltic (iso 8859-13)": "iso-8859-13",
"Gaeilic, Welsh (iso 8859-14)": "iso-8859-14",
"iso 8859-15": "iso-8859-15",
"Cyrillic (koi-8)": "koi8_r",
"Korean (euc-kr)": "euc_kr"}
A QMultiLineEdit widget always used
Unicode internally, but these codecs are used as a default
setting for loading and saving files. Users load an
ascii file, edit it in Unicode, and save it back to ascii.
Theoretically, you can retrieve the users preferences from his
locale. The operating system defines the preferred
encoding, but people seldom work with one encoding, and
Kalam is meant to provide users with a
choice.
While the selection of codecs in Python is large, not all
important encodings are available from Python.
Japanese (jis, shift-jis, euc-jp), Chinese (gbk) and Tamil
(tscii) are only available in Qt (QTextCodec classes), and not in
Python. Codecs for the tiscii encoding used for Devagari are
not available anywhere. You can download separate Japanese
codecs for Python from
http://pseudo.grad.sccs.chukyo-u.ac.jp/~kajiyama/python/.
(euc-jp, shift_jis, iso-2022-jp)
Note also that iso-8859-8 is visually ordered, and you
need Qt 3.0 with the QHebrewCodec to translate iso-8859-8
correctly to Unicode.
def slotForegroundColor(self):
color = QColorDialog.getColor(self.textForegroundColor)
if color.isValid():
pl = self.txtEditorPreview.palette()
pl.setColor(QColorGroup.Text, color)
self.textForegroundColor = color
self.txtEditorPreview.repaint(1)
def slotBackgroundColor(self):
color = QColorDialog.getColor(self.textBackgroundColor)
if color.isValid():
pl = self.txtEditorPreview.palette()
pl.setColor(QColorGroup.Base, color)
self.textBackgroundColor = color
self.txtEditorPreview.repaint(1)
def slotWorkspaceBackgroundColor(self):
color = QColorDialog.getColor(self.MDIBackgroundColor)
if color.isValid():
self.MDIBackgroundColor = color
self.lblBackgroundColor.setBackgroundColor(color)
Each of the color selection buttons is
connected to one of these color slot functions. Note that
QFontDialog, in contrast with
QColorDialog, returns a tuple
consisting of a QFont and a value that
indicates whether the user pressed OK or Cancel.
QColorDialog only returns a color; if
the color is invalid, then the user pressed Cancel. This can
be confusing, especially since an invalid
QColor is just black. Note that we have
to call repaint(1), here, to make sure
the editor preview is updated.
def slotFont(self):
(font, ok) = QFontDialog.getFont(kalamconfig.getTextFont(),
self)
if ok:
self.txtEditorPreview.setFont(font)
self.textFont = font
The QFontDialog
does return a tuple—and if
ok is true, then we update the font of the
preview and also set the textFont variable
to reflect the users choice.
Finally, there's a bit of code appended to
DlgSettings, to make it possible to run
the dialog on its own (to test all functionality):
if __name__ == '__main__':
a = QApplication(sys.argv)
QObject.connect(a,SIGNAL('lastWindowClosed()'),a,SLOT('quit()'))
w = DlgSettings()
a.setMainWidget(w)
w.show()
a.exec_loop()
Calling the settings dialog window
In order to be able to call the dialog window, we must
first create a new QAction and add it to
a likely menu. This is done in KalamApp:
# kalamapp.py
def initActions(self):
self.actions = {}
...
#
# Settings actions
#
self.actions["settingsSettings"] = QAction("Settings",
"&Settings",
QAccel.stringToKey(""),
self)
self.connect(self.actions["settingsSettings"],
SIGNAL("activated()"),
self.slotSettingsSettings)
...
def initMenuBar(self):
...
self.settingsMenu = QPopupMenu()
self.actions["settingsSettings"].addTo(self.settingsMenu)
self.menuBar().insertItem("&Settings", self.settingsMenu)
...
The settingsSettings
is connected to a new slot in
KalamApp:
# Settings slots
def slotSettingsSettings(self):
dlg = DlgSettings(self,
"Settings",
TRUE,
Qt.WStyle_Dialog)
The dialog window is constructed as a
function-local variable. That means that if the function
reaches its end, the dlg object is deleted.
A settings dialog is typically modal. Whether a dialog is
created modal or non-modal is determined in the constructor.
The first argument to
DlgSettings.__init__() is the parent
window, in this case KalamApp. The
second argument is a name. The third argument determines
whether the dialog is modal—TRUE means modal, FALSE
means non-modal. FALSE is also the default. The last argument
can be any combination of widget flags. For a dialog box,
Qt.WStyle_Dialog seems rather appropriate.
Note that in Qt 3, this flag is renamed to
Qt.WType_Dialog There are a whole lot of
flags (the following list is based on Qt 2 - there have been
some changes):
WType_TopLevel - a toplevel
window
WType_Modal - Makes the widget modal
and inplies WStyle_Dialog.
WType_Popup - this widget is a popup
top-level window, it is modal, but has a window system
frame appropriate for popup menus.
WType_Desktop - This widget is the
desktop - you can actually use PyQt to paint on you
desktop.
WStyle_NormalBorder - The window has
a normal border.
WStyle_DialogBorder - A thin dialog
(if you windowmanager on X11 supports that).
WStyle_NoBorder - gives a borderless
window. However, it is better to use WStyle_NoBorderEx
instead, because this flag will make the window completely
unusable on X11.
WStyle_NoBorderEx - gives a
borderless window.
WStyle_Title - The window jas a
title bar.
WStyle_SysMenu - adds a window
system menu.
WStyle_Minimize - adds a minimize
button. On Windows this must be combined with
WStyle_SysMenu to work.
WStyle_Maximize - adds a maximize
button. See WStyle_Minimize.
WStyle_MinMax - is equal to
WStyle_Minimize|WStyle_Maximize. On Windows this must be
combined with WStyle_SysMenu to work.
WStyle_ContextHelp - adds a context
help button to dialogs.
WStyle_Tool - A tool window is a
small window that contains tools (for instance, drawing
tools, or the step buttons of a debugger). The tool window
will always be kept on top of its parent, if there is
one.
WStyle_StaysOnTop - the window
should stay on top of all other windows.
WStyle_Dialog - indicates that the
window is a dialog window. The window will not get its own
taskbar entry and be kept on top of its parent by the
window system. This is the flag QDialog uses, and it is
not necessary for us to explicitly pass it to
DlgSettings.
WDestructiveClose - makes Qt delete
this object when the object has accepted closeEvent().
Don't use this for dialog windows, or your application
will crash.
WPaintDesktop - gives this widget
paint events for the desktop.
WPaintUnclipped - makes all painters
operating on this widget unclipped. Children of this
widget, or other widgets in front of it, do not clip the
area the painter can paint on.
WPaintClever - indicates that Qt
should not try to optimize repainting for the widget, but
instead pass the window system repaint events directly on
to the widget.
WResizeNoErase - indicates that
resizing the widget should not erase it. This allows
smart-repainting to avoid flicker.
WMouseNoMask - indicates that even
if the widget has a mask, it wants mouse events for its
entire rectangle.
WNorthWestGravity - indicates that
the widget contents are north-west aligned and static. On
resize, such a widget will receive paint events only for
the newly visible part of itself.
WRepaintNoErase - indicates that the
widget paints all its pixels. Updating, scrolling and
focus changes should therefore not erase the widget. This
allows smart-repainting to avoid flicker.
WGroupLeader - makes this widget or
window a group leader. Modality of secondary windows only
affects windows within the same group.
You can combine these flags with the or
(or |) operator.
Showing a modal dialog is a matter of
simply calling exec_loop():
dlg.exec_loop()
if dlg.result() == QDialog.Accepted:
kalamconfig.set("textfont", dlg.textFont)
kalamconfig.set("workspace", str(dlg.cmbWindowView.currentText()))
kalamconfig.set("style", str(dlg.cmbStyle.currentText()))
kalamconfig.set("textbackground", dlg.textBackgroundColor)
kalamconfig.set("textforeground", dlg.textForegroundColor)
kalamconfig.set("mdibackground", dlg.MDIBackgroundColor)
kalamconfig.set("wrapmode", dlg.cmbLineWrapping.currentItem())
kalamconfig.set("linewidth", int(str(dlg.spinLineWidth.text())))
kalamconfig.set("encoding", str(dlg.cmbEncoding.currentText()))
kalamconfig.set("forcenewline", dlg.chkAddNewLine.isChecked())
If the execution loop of a modal dialog
terminates, the dialog object is not destroyed, and you can
use the reference to the object to retrieve the contents of
its widgets. By calling result() on the
dialog object you can determine whether the user pressed OK or
Cancel.
In this example, if the user presses OK,
all relevant settings in kalamconfig are
updated. This causes kalamconfig to emit
change signals that are caught by all relevant objects.
The workspace object
is updated:
def initWorkSpace(self):
workspace = kalamconfig.get("workspace")(self)
workspace.setBackgroundColor(kalamconfig.get("mdibackground"))
self.connect(qApp,
PYSIGNAL("sigmdibackgroundChanged"),
workspace.setBackgroundColor)
self.setCentralWidget(workspace)
return workspace
All view objects
are updated, too. Some of the changes can be directly
connected to the editor widget, the font setting, while others need
a bit of processing, like the wrap mode:
# kalamview.py - extract
...
import kalamconfig
...
class KalamView(QWidget):
def __init__(self, parent, doc, *args):
...
self.editor.setFont(kalamconfig.get("textfont"))
self.setWordWrap(kalamconfig.get("wrapmode"))
self.setBackgroundColor(kalamconfig.get("textbackground"))
self.setTextColor(kalamconfig.get("textforeground"))
...
self.connect(qApp,
PYSIGNAL("siglinewidthChanged"),
self.editor.setWrapColumnOrWidth)
self.connect(qApp,
PYSIGNAL("sigwrapmodeChanged"),
self.setWordWrap)
self.connect(qApp,
PYSIGNAL("sigtextfontChanged"),
self.editor.setFont)
self.connect(qApp,
PYSIGNAL("sigtextforegroundChanged"),
self.setTextColor)
self.connect(qApp,
PYSIGNAL("sigtextbackgroundChanged"),
self.setBackgroundColor)
...
def setTextColor(self, qcolor):
pl = self.editor.palette()
pl.setColor(QColorGroup.Text, qcolor)
self.editor.repaint(TRUE)
def setBackgroundColor(self, qcolor):
pl = self.editor.palette()
pl.setColor(QColorGroup.Base, qcolor)
self.editor.setBackgroundColor(qcolor)
self.editor.repaint(TRUE)
def setWordWrap(self, wrapmode):
if wrapmode == 0:
self.editor.setWordWrap(QMultiLineEdit.NoWrap)
elif wrapmode == 1:
self.editor.setWordWrap(QMultiLineEdit.WidgetWidth)
else:
self.editor.setWordWrap(QMultiLineEdit.FixedColumnWidth)
self.editor.setWrapColumnOrWidth(kalamconfig.get("linewidth"))
...
Not all changes can be activated while the
application is running. The workspace style is determined when
the application is restarted. It is nice and courteous to
inform the user so. The best place to do that is in
slotSettingsSettings():
def slotSettingsSettings(self):
...
if dlg.result() == QDialog.Accepted:
...
workspace = str(dlg.cmbWindowView.currentText())
if kalamconfig.Config.workspace <> workspace:
kalamconfig.set("workspace", workspace)
QMessageBox.information(self,
"Kalam",
"Changes to the interface style " +
"will only be activated when you " +
"restart the application.")
...