Implementing configurations settings for Kalam

Working with configuration settings can be divided into two main procedures: giving your application classes access to the configuration data, and loading and saving that data. We'll start by looking at the first problem, and then at loading and saving. In the next chapter, we'll round out Kalam by creating a preferences dialog.

Handling configuration data in your application

Before we start saving and restoring configuration settings, we should have a clear idea of how to handle them in the application. Configuration data typically must be available everywhere in the application, because all objects must be able to query and store settings at will.

In other languages, such as Visual Basic, you would use a module with global variables to store configuration data; in a language like Java or C++, you would use a singleton object—that is, an object with a hidden constructor that can only be instantiated once. Python, however, does not support these constructions.

Of course, there is an alternative. In a sense, class definitions are global. Every module that imports a certain class gets exactly the same class. Keep in mind that a class is just an object, of the type class. You can associate variables not only with an object, as in:

class SomeClass:

   def __init__(self):
       self.someVariable=1

someInstance=SomeClass()
print someInstance.someVariable
    

But also with a class:

class SomeClass:

    classVariable=1

print SomeClass.classVariable
    

These class variables are accessed via the name of the class, instead of the name of an instance of that class. Class variables are shared by all instances of a class.

The ideal solution to creating a "global" configuration repository is to define a class that contains all configuration data as class variables. It's also possible to encapsulate the configuration data repository in a single class variable. You cannot call functions on a class - there is no equivalent to the ‘static' methods of Java. If we need functions to work on the configuration data, we must either define those functions at module level, or as functions of an object that is a class variable of the configuration module. An example would be a function to create a QFont out of a fontname string.

Well — that was the theory. Let's now look at the code needed to implement configuration data for Kalam. It's pretty similar to the snippets we saw above:

"""
kalamconfig.py - Configuration class for the Kalam Unicode Editor

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

import sys, os
from qt import *


class Config:

    APPNAME = "kalam"
    APPVERSION = "ch13"
    CONFIGFILE = ".kalam-ch13"

    currentStyle="Platinum"
    viewmanager="tabmanager"

    app_x=0
    app_y=0
    app_w=640
    app_h=420

    fontfamily="courier"
    pointsize=12
    weight=50
    italic=0
    encoding=22


def getApplicationFont():
    return QFont(Config.fontfamily,
                 Config.pointsize,
                 Config.weight,
                 Config.italic,
                 Config.encoding )
      

As you can see, it's just a simple matter of a class with a bunch of class variables that represent pertinent values. However, because these values will be saved to a file, you cannot associate real objects with the keys. To make it easier to retrieve a font based on the values stored in the configuration file, there is module-level helper function, getApplicationFont(), which constructs a QFont on the fly.

A similar function exists to set the font:

def setApplicationFont(qfont):
     Config.fontfamily = qfont.family()
     Config.pointsize = qfont.pointSize()
     Config.weight = qfont.weight()
     Config.italic = qfont.italic()
     Config.encoding = qfont.encoding()
      

As you can see, we store our settings in a flat namespace, in which every key must be unique. This is just like the properties system used in Java, but more complex systems can be very useful. For instance, the Windows registry is one gigantic tree, and even the files created by ConfigParser have sections and subsections. For highly complex configuration needs, there is the shlex Python module, which you can use to define configuration languages.

Saving and loading the configuration data

Retrieving and saving the configuration data can be made as complex or easy as you want. We have already discussed the possibility of using _winreg or ConfigParser for the saving and retrieving of configuration data.

What we are going to, however, is far more simple. When we load the settings, we just read every line in the configuration file, and add a variable to the Config class that represents the value:

def readConfig(configClass = Config):
    sys.stderr.write( "Initializing configuration\n")
    try:
        for line in open(os.path.join(os.environ["HOME"],
                                      Config.CONFIGFILE)).readlines():
            k, v=tuple(line.split("="))
            v=v[:-1]
            if v=="None\n":
                v=None
            elif type:
               try:
                    v=int(v)
                except ValueError:
                    pass
            setattr(configClass, k, v)
    except IOError:
        sys.stderr.write( "Creating first time configuration\n")
      

To add the variable to the Config we use the standard Python function setattr() — this function is one of the delights that make Python so dynamic.

Note the special treatment of the value that is represented by "None" in the configuration file: if "None" is encountered the value of the configuration key is set to a real None object. This contrast with the situation where the value is simply empty: then the value is set to an empty string ("").

Currently, the configuration file format only supports two types: strings and integers. The distinction is made by brute force: we simply try to convert the value to an integer, and if we succeed, it stays an integer. If the conversion raises a ValueError, we assume the value should remain a string.

By now you might be wondering when we will be reading in the configuration values. The simple answer is that we will do so when the KalamConfig module is first imported. At the bottom of the module the function readConfig(Config) is called, and is only executed once:

readConfig()
      

Saving the configuration values to disk is a simple matter of looping over the contents of the attributes of the Config class — that is, the __dict__, __methods__ and __members__ dictionaries that are part of the object's hidden attributes. We retrieve these with the dir() function:

def writeConfig(configClass = Config):
    sys.stderr.write( "Saving configuration\n")
    configFile=open(os.path.join(os.environ["HOME"],".kalamrc"),"w+")
    for key in dir(Config):
        if key[:2]!='__':
            val=getattr(Config, key)
            if val==None or val=="None":
                line=str(key) + "=\n"
            else:
                line=str(key) + "=" + str(val) + "\n"
            configFile.write(line)
    configFile.flush()
      

The actual values are retrieved with the opposite of setattr(): getattr(). As a first check, attributes with a double underscore as prefix are not saved: those are internal attributes to the Config class. If the value is the None object, we print the string "None". Because it is quite possible that some values are QString objects, and because you cannot save these, everything is converted to a plain Python string.

Finally, you might need functions that get and set more complex objects in the Config. These can be simple module level functions that work on the class:

def getTextFont():
    return QFont(Config.fontfamily,
                 Config.pointsize,
                 Config.weight,
                 Config.italic,
                 Config.encoding )

def setTextFont(qfont):
     Config.fontfamily = qfont.family()
     Config.pointsize = qfont.pointSize()
     Config.weight = qfont.weight()
     Config.italic = qfont.italic()
     Config.encoding = qfont.encoding()
      

Using configuration data from the application

By now we have a simple configuration data mechanism, and it's time to use it. Earlier we defined a few settings: the position and size of the application window, the widget style that is to be used, and the interface paradigm. First, we will write some code to actually use these settings. Then we will write code to save changes when the application is closed.

Font settings

The font to be used in the editor window can be set and retrieved with the get and set functions we defined above. The KalamView class is the place to use this setting.

"""
from qt import *
import kalamconfig
from resources import TRUE, FALSE

class KalamView(QWidget):

    def __init__(self, parent, doc, *args):
        apply(QWidget.__init__,(self, parent) + args)
        ...
        self.editor=QMultiLineEdit(self)
        self.editor.setFont(kalamconfig.getTextFont())
        self.layout.addWidget(self.editor)
      

We import the configuration module, not the Config class from the configuration module. After creating the editor widget, we simply set the font with a call to self.editor.setFont(kalamconfig.getTextFont()).

Window geometry

Applying the geometry is just as easy. It's very pleasant for users when an application pops up its windows at the same place and in the same size as the user left them. This is part of session management, which is very advanced in the KDE environment, but less so for Windows. Qt 3 offers support for session management with QSessionManager and QApplication, but we'll take care of session management ourselves at this time.

Setting the correct size and position of a window, and also the correct widget style, is done in the central application object, KalamApp:

from qt import *
...

import kalamconfig
...

class KalamApp(QMainWindow):
    """KalamApp is the toplevel application window of the kalam unicode editor
    application.
    """
    def __init__(self, *args):
        apply(QMainWindow.__init__,(self, ) + args)

        ...

        self.initSettings()

        ...

    #
    # GUI initialization
    #
    def initSettings(self):
        qApp.setStyle(kalamconfig.getStyle())
        self.setGeometry(kalamconfig.Config.app_x,
                         kalamconfig.Config.app_y,
                         kalamconfig.Config.app_w,
                         kalamconfig.Config.app_h)

        

Here, too, we import the kalamconfig module. The function initSettings() is called from the constructor {__init__()}

This function will be extended with other application level settings during development of Kalam.

Determining the widget style

First, we set the desired widget style. Users can also set the widget style using a command-line option, and Qt can even figure out which style fits best with a users platform. But some people have strong preferences, and will want to configure their preferred style. It is easy enough to determine and use the platform default if no special style is set.

The getStyle() and setStyle are quite interesting, from a Python point of view:

def __extractStyle(style):
    if type(style) == InstanceType:
        return style.__class__.__name__
    elif type(style) == StringType:
        return style
    else:
        return "QPlatinumStyle"
        

I wanted this to be as flexible as possible, showing the dynamic nature of Python. The __extractStyle function takes the current style object that is used by the application. We find this by calling qApp.style(). qApp is a global variable that points to the QApplication object.

An instance in Python has a number of ‘hidden' fields and methods that each have a special meaning. One of these is __init__(), which is called when the object is first created. Another is __class__, which returns the class that the object was created from. You can use this to make more instances, but in this case we are interested in the string that contains the name of the class. You can retrieve the name with another ‘hidden' variable of the class class: __name__.

def setStyle(style):
    if type(style) == types.StringType:
        Config.currentStyle = style
    elif type(style) == types.InstanceType:
        Config.currentStyle = __extractStyle(style)
      

Setting the style in the context of kalamconfig means setting the "currentStyle" attribute of Config to a string that represents the style. If the input to setStyle() is already a string (that is, if the type is types.StringType), then we simply set it. Otherwise, we use the function defined above to get a string that equals the name of the style class.

def getStyle():
    # Basic sanity check - you don't want to eval arbitrary code
    if not hasattr(Config, "currentStyle"):
        print "ok", repr(qApp.style())
        Config.currentStyle = __extractStyle(qApp.style())

    if (Config.currentStyle[0] != "Q" or
        Config.currentStyle[-5:] != "Style"  or
        Config.currentStyle.find(" ") > 0):
        Config.currentStyle = "QPlatinumStyle"

    try:
        # you shouldn't use eval for this, but it is a nice opportunity
        # for showing how it works. Normally you'd use a dictionary of
        # style names.
        return eval(Config.currentStyle)()
    except NameError, e:
        print "No such style: defaulting to Platinum"
        return QPlatinumStyle()
      

Getting a QStyle object of the right type is a bit more complex. Of course, you will most often use a simple dictionary that maps style names to style classes:

        styleDict = { "platinum": QPlatinumStyle, ...}
      

This is not particularly flexible. Here, we use eval to create an object from the name of a class. Look carefully at:

        return eval(Config.currentStyle)()
      

This means that, if the variable Config.currentStyle contains a string that is equal to classname and that is known to Python (that is, it can be found in one of the imported modules), eval() will return that class. The brackets after eval make an instance of the class.

Beware: using eval is dangerous. For example, what if someone hacked your .kalam-ch13 configuration file and set the entry currentStyle to os.rmdir('/')? If you were fool enough to run Kalam as root on Unix, you'd lose your system—irretrievably.

This is why I checked the existence and believability of the currentStyle string before eval-ing it. I only used eval to show you that it exists— for your own sake, don't use eval trivially! We'll return to eval and its friends in Chapter 20.

Setting the viewmanager

The last task we handle in this chapter is the choosing of the view manager. The available choices include tabbed windows, mini-windows, splitters, stacks — the lot. This time, we will use a dictionary that maps viewmanager names to actual classes. This is only to show you how it works - in general, it's a good rule to not mix and match approaches as we have done here, but to choose one method, and stick to it.

"""
kalamconfig.py - Configuration class for the Kalam Unicode Editor

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

import sys, os, types
from qt import *

import tabmanager, listspace, splitspace, stackspace, workspace

workspacesDictionary = {
    "tabmanager" : tabmanager.TabManager,
    "listspace"  : listspace.ListSpace,
    "splitspace" : splitspace.SplitSpace,
    "stackspace" : stackspace.StackSpace,
    "workspace"  : workspace.WorkSpace,
    }

class Config:
...
        

First, a dictionary (workspacesDictionary) is created that contains a mapping from strings to the actual classes. Of course, in order to be able to access those classes, they will have to be imported.

def getViewManager():
    try:
        return workspacesDictionary[Config.viewmanager]
    except:
        return tabmanager.TabManager

def setViewManager(viewmanager):
    Config.viewmanager = viewmanager.__class__.__name__
        

These two functions get and set the viewmanager style. If the style given in Config doesn't exist, a KeyError will be raised, in which case we simply return a sensible default.

The getViewManager() is called from the initWorkSpace() function in kalamapp.py:

    ...
    def initWorkSpace(self):
        workspace = kalamconfig.getViewManager()(self)
        self.setCentralWidget(workspace)
        return workspace
    ...
        

Catching the changes when the application closes

The configuration should be written to a file when the app closes. There are two places where Kalam can end: slotFileQuit(), and in the eventhandler eventFilter().

    ...
    #
    # Slot implementations
    #

    def slotFileQuit(self):
        try:
            self.docManager.closeAllDocuments()
        except:
            return
        kalamconfig.writeConfig()
        qApp.quit()
    ...
    #
    # 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()
                    kalamconfig.writeConfig()
                    event.accept()
                except Exception, e:
                    event.ignore()
        return QWidget.eventFilter(self, object, event)
     ...
      

After all, it is simply a matter of calling writeConfig() at the right moment.