Saturday, May 3, 2014

Remembering

So I've moved on to a big, amorphous chunk of code, the "main window" of the app. This is what creates the main window, has all the menu "actions" like Save-As and Open, and generally sets things up and tears things down. It's a lot of code but I've started by nibbling at the edges, with initializing and shutting down. This involves using QSettings.

A QSettings object provides a way to tuck items into a persistent key-value store and get them back later. One common use is to remember the program's screen geometry. The final steps in the main window's termination code are:

        self.settings.setValue("mainwindow/size",self.size())
        self.settings.setValue("mainwindow/position",self.pos())
        self.settings.setValue("mainwindow/splitter",self.splitter.saveState())

The size, screen position, and the relative position of the "splitter" that divides the window into two sections are stored away. When the program starts up, the final steps in the main window's GUI initialization are:

        self.resize(self.settings.value("mainwindow/size", QSize(400, 400)))
        self.move(self.settings.value("mainwindow/position", QPoint(100,100)))
        self.splitter.restoreState(
            self.settings.value("mainwindow/splitter",QByteArray()))

The call to self.resize() sets the window size to whatever was stored in the settings, or, if this is the very first time the app's been launched on this system, to a default value of 400x400.

Version 1 stored a number of things in the settings, but the list of things stored will change a lot for V.2. This is partly because V.2 has more global items to remember: it will support user preferences for colors and fonts and paths to this and that, all of which are best remembered in settings. But values saved in QSettings are by definition global to the application. In V.1, lots of things were treated as global that were actually unique to the current document. That was because there was only one document. In V.2 we support multiple open Books (documents) and that, as I have said ruefully a number times lately, changes everything.

For just one example, the Find panel keeps lists of the last ten search-strings and last ten replace-strings. In V.1 it stored those lists in the settings at shutdown. But those lists shouldn't be global; they are document-unique and will change each time the user brings a different Book into focus. So those will get saved and restored from the each Book's meta-data, not from the global settings.

On the other hand, in V.2 I want to do what Firefox does when it starts up: offer to restore the previous session. I do not want to do what a number of Mac OS apps do since the Mountain Lion release, and restore the last session automatically. Maybe you don't want to re-open all those documents. But I want to offer the option, and that means, at shutdown, recording the list of open documents and their paths in the settings; and recovering the list at startup.

QVariant vanishes

As documented for C++ (link above) the QSettings API makes heavy use of the QVariant class, a way to wrap any other class as something that can be stored as a byte-string. The first argument to both value() and setValue() are the character-string keys, but the second in both cases is supposed to be a QVariant. The value being stored from self.size() is a QSize; the value returned by self.pos() is a QPoint, and the splitter status from self.splitter.saveState() is a 23-element QByteArray. C++ doesn't permit passing such a potpourri of types in the same argument position, so Qt tells you to swaddle them decently as QVariants.

Similarly, the value returned by QSettings.value() is documented as being a QVariant; and you are supposed to use one of that QVariant's many "toXxxx" methods to restore it to whatever class was wrapped in it. For example, in V1 of PPQT, the code to restore the main window splitter's state looked like:

 self.hSplitter.restoreState(
              self.settings.value("main/splitter").toByteArray() )

The call to value() evaluates to a QVariant; that object's toByteArray() method reveals that the value is really a QByteArray, and that is accepted by the splitter's restoreState method.

Well, in PyQt5, Maestro Phil and company did away entirely with QVariant calls. Presumably they are still using them under the covers, in the interface to C++, but the Python programmer is not supposed to use them, and in fact there is no way to import the name QVariant from a PyQt5 module. This simplifies the code of a PyQt5 app, but it adds yet another consideration when moving code from PyQt4.

Where does it all go?

Where does the QSettings object actually put the keys and values you give it? In different places depending on the platform. It's documented under "Platform Considerations" in the QSettings page, but here's the bottom line:

Linux
$HOME/.config/orgname/appname.conf
Mac OSX
$HOME/Library/Preferences/com.orgname.appname.plist
Windows (registry)
HKEY_CURRENT_USER\Software\orgname\appname

In each case, orgname and appname are defined at startup to the QApplication, in my case with code like this:

app.setOrganizationName("PGDP")
app.setApplicationName("PPQT2")

Eventually that will be in the top-level module, PPQT2. Since that doesn't exist, it's currently in the unit-test driver, mainwindow_test.py, which prepares an empty settings object, creates the mainwindow object, sends it a close event, and then looks in the settings to verify it stored what it should have.

Actually, QSettings has a multi-level scheme; it will look first for user-level values; then at a system level if necessary. I'm not worrying about that; it's all automatic and platform-independent at the PyQt level. Which is nice.

No comments: