Monday, July 12, 2010

Porting ejpi: Dynamic Layouts with (Py)Qt

Previous and Future ejpi UI

The current ejpi layout does not seem very dynamic
In the history column an error info box may appear that you can click away (custom made, not the new one available in the latest versions of GTK).  The "Trigonometry" button brings up a Touch Selector for choosing which secondary keyboard you want visible.

Some problems with this layout:
  • Buttons are oddly shaped
  • The keys would have to be customized for portrait and landscape
  • Showing two keyboards would take up too much space in portrait mode
  • Not enough space left to increase the button text size
Below I created a mockup of the adjusted layout in portrait mode

Showing only one keyboard at a time will allow the buttons to be a bit nicer and gives more space for the history.  Switching the secondary keyboard was two clicks away.  With only one keyboard switches might be more frequent so we'll switch to tabs with icon representations for single click access (despite what I remember of the Maemo guidelines discouraging tabs).  Moving the Push key out makes it available when any keyboard is selected and reduces the need to specialize the keyboard layouts based on orientation.

We've not changed too much in terms of how dynamic the UI is.  The only change is on rotation we will need to switch the keyboards from being to the right of the history to being below the history.

Comparing GTK and Qt Layout systems

GTK's layout system centers around container widgets that provide holes for their children to populate.  Also the HBox are VBox are very static.  Their base class is either abstract or an interface so you have to actually swap out the class in the widget hierarchy to change between a horizontal and vertical layout.

The impression I have of Qt so far is that it separates out the concept of layout from the widget container.  A way to think of it is the widget is the canvas to draw on while the layout constrains the sizes and locations of the things to be drawn.  I actually removed a layout at one point and the widgets stayed visible and in the same location as before.  You can have layout hierarchies but the API  support for layouts is drastically limited as compared to widgets.  Also the separate widget and layout hierarchies can complicate things.

Implementing My Dynamic Layouts

Of course when I got started I didn't understand the difference between the way they did layouts.  I tried to implement everything as if it was GTK.  I had complex layout hierarchies that I was trying to swap out at run time.  It seems simple to change up a flat layout hierarchy but when you are trying to work off of an abstraction the API disparity between widgets and layouts increases the complexity.  I tried multiple approaches to get it all to work to no avail.

I might have come across one possible approach but I ran into the need to do some casting because the API returned QLayoutItem proxies rather than proxies for the derived class QHBoxLayout, etc.  With PyGTK I never had to worry about casting so when hitting it with PyQt I gave up in disgust at how nasty that feels in Python.  Please note that I am a C++ developer by trade and have no problems with it in C++.

It was when I finally connected how layouts work in Qt that I made some progress.  I would create intermediary widgets that I'd assign my layouts to.  I could then add/remove/show/hide them all I wanted without issue.  I think this is the way it was meant to be done.  I think one of the reasons I hit a wall with this as compared to others is my abhorrence for inheriting from QWidget.  In Gonvert I even changed part of my user experience because I could not access certain event information unless I inherited from the Qt widget.

Its kind of hard to do code samples without gutting my entire application and plopping it in here so I'll just provide one class and how it sets up its widget/layout hierarchy:

class QErrorDisplay(object):

    def __init__(self):
        self._messages = []

        icon = QtGui.QIcon.fromTheme("gtk-dialog-error")
        self._severityIcon = icon.pixmap(32, 32)
        self._severityLabel = QtGui.QLabel()
        self._severityLabel.setPixmap(self._severityIcon)

        self._message = QtGui.QLabel()
        self._message.setText("Boo")

        icon = QtGui.QIcon.fromTheme("gtk-close")
        self._closeLabel = QtGui.QPushButton(icon, "")
        self._closeLabel.clicked.connect(self._on_close)

        self._controlLayout = QtGui.QHBoxLayout()
        self._controlLayout.addWidget(self._severityLabel)
        self._controlLayout.addWidget(self._message)
        self._controlLayout.addWidget(self._closeLabel)

        self._topLevelLayout = QtGui.QHBoxLayout()
        self._topLevelLayout.addLayout(self._controlLayout)
        self._widget = QtGui.QWidget()
        self._widget.setLayout(self._topLevelLayout)
        self._hide_message()

    @property
    def toplevel(self):
        return self._widget

    ...

Note the toplevel property.  Kind of nasty but its what I use to insert this into the rest of the class hierarchy

        self._controlLayout = QtGui.QVBoxLayout()
        self._controlLayout.addWidget(self._errorDisplay.toplevel)
        self._controlLayout.addWidget(self._historyView.toplevel)
        self._controlLayout.addLayout(self._userEntryLayout)


Something else neat that I noticed is that in Qt, QHBoxLayout and QVBoxLayout are just used for convenience when working with QBoxLayout which dynamically supports switching from horizontal and vertical layouts.  This made me happy with Qt for once.  I can't imagine there being significance enough of a difference that they should be separate in GTK or if there is that a dynamically switchable layout couldn't be supported.  I can't speak too positively of Qt or people might worry.  I think its a disservice that Qt has QHBoxLayout and QVBoxLayout and recommends their usage.  Do you really need an extra class to not have to type in one extra parameter into the constructor?  Really?

        if maeqt.screen_orientation() == QtCore.Qt.Vertical:
            defaultLayoutOrientation = QtGui.QBoxLayout.TopToBottom
        else:
            defaultLayoutOrientation = QtGui.QBoxLayout.LeftToRight
        self._layout = QtGui.QBoxLayout(defaultLayoutOrientation)
        # Actual handling of rotation with setDirection not implemented yet


Conclusion

QVBoxLayout and QHBoxLayout are your enemy, QBoxLayout is your friend.

Attach layouts to QWidget's to make layout work easy.  It is sad that PyQt requires the use of casting.

I think there is suppose to be some new UI stuff to make dynamic layouts with fancy transitions easy.  As I will talk about when I discuss QTreeView's it is a bit annoying trying to figure out what is the recommended approach for what situation so I just went with (1) what I found documentation for and (2) what seemed familiar.