MAY
29
2011

File selector in QML and PySide

Today I wrote a file selector in QML. This was not trivial because QML has no standard element for drilling down in a tree model. So I wrote one. A bit of Python was needed to expose the file system to the QML as a data model.

I've played with Bup a bit lately and wanted to write a GUI for it. Normal Qt widgets would do, but when the bup developers asked if it would run on MeeGo, I had a look at QML.

QML File Selector

Update: check the comments for a new version.

The Python part of the code is simple and short:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
from PySide import QtCore, QtGui, QtDeclarative

app = QtGui.QApplication(sys.argv)
model = QtGui.QDirModel()
view = QtDeclarative.QDeclarativeView()
view.rootContext().setContextProperty("dirModel", model)
view.setSource(QtCore.QUrl.fromLocalFile("list.qml"))
view.show()

sys.exit(app.exec_())

The QML is rather long. I post it here so other QML developers can easily find it and experiment with it.
The selector can be navigated with arrow keys and mouse. The lists can be flicked. Save this QML as 'list.qml' so that the Python code can find it.

import QtQuick 1.0

Rectangle {
    id: page
    width: 400; height: 240;
    anchors.fill: parent
    VisualDataModel {
        id: listModel
        model: dirModel
        Item {
            id: itemDelegate
            width: listView.width; height: 25
            Rectangle {
                id: content
                anchors.fill: parent
                color: "transparent"
                Text { text: fileName }
            }
            states: State {
                name: "active"; when: itemDelegate.activeFocus
                PropertyChanges { target: content; color: "#FFDDDD" }
            }
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    listView.currentIndex = index
                    itemDelegate.forceActiveFocus()
                    if (model.hasModelChildren) {
                        animModel.rootIndex = listModel.modelIndex(index)
                        animation.running = true
                    }
                }
            }
            Keys.onRightPressed: {
                if (model.hasModelChildren) {
                    animModel.rootIndex = listModel.modelIndex(index)
                    animation.running = true
                }
            }
            Keys.onLeftPressed: {
                // if statement does not work as intended
                if (listModel.parentModelIndex() != listModel.rootIndex) {
                    listView.x = -listView.width
                    listModel.rootIndex = listModel.parentModelIndex()
                    leftAnimation.running = true
                }
            }
            Keys.onUpPressed: {
                if (index > 0) {
                    listView.currentIndex = index - 1
                } else if (listView.keyNavigationWraps) {
                    listView.currentIndex = listView.count - 1
                }
                animModel.rootIndex = listModel.modelIndex(listView.currentIndex)
            }
            Keys.onDownPressed: {
                if (listModel.count > index + 1) {
                    listView.currentIndex = index + 1
                } else if (listView.keyNavigationWraps) {
                    listView.currentIndex = 0
                }
                animModel.rootIndex = listModel.modelIndex(listView.currentIndex)
            }
        }
    }
    VisualDataModel {
        id: animModel
        model: dirModel
        Rectangle {
            width: listView.width; height: 25
            Text { text: fileName }
        }
        rootIndex: listModel.modelIndex(0)
    }
    SequentialAnimation {
        id: animation
        NumberAnimation {
            target: listView
            property: "x"
            to: -listView.width
            duration: 100
        }
        ScriptAction {
            script: {
                listView.x = 0
                listModel.rootIndex = animModel.rootIndex
                animModel.rootIndex = listModel.modelIndex(0)
            }
        }
    }
    SequentialAnimation {
        id: leftAnimation
        NumberAnimation {
            target: listView
            property: "x"
            to: 0
            duration: 100
        }
        ScriptAction {
            script: {
                animModel.rootIndex = listModel.modelIndex(0)
            }
        }
    }
    ListView {
        id: listView
        x: 0; y: 0
        width: page.width * 0.8
        height: page.height
        model: listModel
        focus: true
        keyNavigationWraps: true
    }
    ListView {
        id: animBox
        x: listView.width; y: listView.y
        width: listView.width
        height: listView.height
        model: animModel
    }
}

Comments

Here is a new version. It works much nicer already. It does expose some flaws in the QML implementation 4.7.2: it may crash and valgrind lists a plethora of problems.

C++ to call the QML:

#include <QtGui/QApplication>
#include <QtGui/QDirModel>
#include <QtDeclarative/QDeclarativeView>
#include <QtDeclarative/QDeclarativeContext>

int
main(int argc, char** argv) {
    QApplication app(argc, argv);
    QDirModel* model = new QDirModel();
    QDeclarativeView* view = new QDeclarativeView();
    view->setResizeMode(QDeclarativeView::SizeRootObjectToView);
    view->rootContext()->setContextProperty("dirModel", model);
    view->setSource(QUrl::fromLocalFile("list.qml"));
    view->show();
    return app.exec();
}

and a new version that remembers the positions of the directories you have been:

import QtQuick 1.0

/** This QML displays a file selector.
    The file selector remembers the previous locations: if a directory is left
    and entered later, the active entry is the same one as when the directory
    was last left. **/

Rectangle {
    width: 400; height: 240
    anchors.fill: parent
    property real viewOffset: 40;
    property real viewWidth: 320;//0.9 * width
    property variant cursorPositions: [-1]
    property variant position: []
    function copy(array) {
        var a = [], i;
        for (i = 0; i < array.length; i += 1) {
            a[i] = (array[i].length !== undefined) ?copy(array[i]) :array[i];
        }
        return a;
    }
    // retrieve the indexes for the current level
    function getIndexes(position, cursorPositions) {
        var indexes = cursorPositions, i, j, p;
        for (i = 0; i < position.length; i += 1) {
            p = position[i];
            if (indexes.length < p + 2) {
                for (j = indexes.length; j < p + 2; j += 1) {
                    indexes[j] = [-1];
                }
            }
            indexes[0] = p;
            indexes = indexes[p + 1];
        }
        return indexes;
    }
    /** Previous positions are cached in the nexted cursorPositions array. At
        position 0, the last position at that level is stored, at the following
        positions, nested arrays are placed that cache positions for the
        underlying levels. **/
    function enter(model, index) {
        if (!model.hasModelChildren) {
            return;
        }
        var p = copy(position), c = copy(cursorPositions), indexes;
        p[p.length] = index;
        indexes = getIndexes(p, c);
        if (indexes[0] === -1) {
            indexes[0] = 0;
        }
        position = p;
        cursorPositions = c;

        prevModel.rootIndex = listModel.rootIndex;
        prevView.currentIndex = listView.currentIndex;
        listModel.rootIndex = listModel.modelIndex(index);
        listView.currentIndex = indexes[0];
        updateNextModel();
        enterAnimation.running = true
    }
    function leave() {
        if (position.length === 0) {
            return;
        }
        var p = copy(position), c = copy(cursorPositions), indexes, parentIndex;
        indexes = getIndexes(p, c);
        indexes[0] = listView.currentIndex;
        parentIndex = p[p.length - 1];
        p.length -= 1;
        position = p;
        cursorPositions = c;

        nextModel.rootIndex = listModel.rootIndex;
        nextView.currentIndex = listView.currentIndex;
        listModel.rootIndex = listModel.parentModelIndex();
        listView.currentIndex = parentIndex;
        prevModel.rootIndex = listModel.parentModelIndex();
        if (p.length > 0) {
            prevView.currentIndex = p[p.length - 1];
        } else {
            prevView.currentIndex = -1;
        }
        leaveAnimation.running = true;
    }
    function updateNextModel() {
        nextModel.rootIndex = listModel.modelIndex(listView.currentIndex)
        var p = copy(position), c = copy(cursorPositions), indexes;
        p[p.length] = listView.currentIndex;
        indexes = getIndexes(p, c);
        nextView.currentIndex = indexes[0];
    }
    VisualDataModel {
        id: listModel
        model: dirModel
        Item {
            id: itemDelegate
            width: listView.width; height: 25
            Rectangle {
                id: content
                anchors.fill: parent
                color: "transparent"
                Text { text: fileName }
            }
            states: State {
                name: "active"; when: listView.currentIndex == index
                PropertyChanges { target: content; color: "#FFDDDD" }
            }
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    listView.currentIndex = index;
                    nextModel.rootIndex = listModel.modelIndex(index);
                    enter(model, index)
                }
            }
            Keys.onRightPressed: {
                enter(model, index);
            }
            Keys.onLeftPressed: {
                leave();
            }
            Keys.onUpPressed: {
                if (index > 0) {
                    listView.currentIndex = index - 1
                } else if (listView.keyNavigationWraps) {
                    listView.currentIndex = listView.count - 1
                }
                updateNextModel();
            }
            Keys.onDownPressed: {
                if (listModel.count > index + 1) {
                    listView.currentIndex = index + 1
                } else if (listView.keyNavigationWraps) {
                    listView.currentIndex = 0
                }
                updateNextModel();
            }
        }
    }
    VisualDataModel {
        id: prevModel
        model: dirModel
        Item {
            width: viewWidth; height: 25
            Rectangle {
                id: content
                anchors.fill: parent
                color: "transparent"
                Text { text: fileName }
            }
            states: State {
                name: "active"; when: prevView.currentIndex == index
                PropertyChanges { target: content; color: "#ffeeee" }
            }
        }
    }
    VisualDataModel {
        id: nextModel
        model: dirModel
        Item {
            width: viewWidth; height: 25
            Rectangle {
                id: content
                anchors.fill: parent
                color: "transparent"
                Text { text: fileName }
            }
            states: State {
                name: "active"; when: nextView.currentIndex == index
                PropertyChanges { target: content; color: "#ffeeee" }
            }
        }
        rootIndex: listModel.modelIndex(0)
    }
    SequentialAnimation {
        id: enterAnimation
        NumberAnimation {
            target: listView
            property: "x"
            from: viewOffset + viewWidth
            to: viewOffset
            duration: 100
        }
    }
    SequentialAnimation {
        id: leaveAnimation
        NumberAnimation {
            target: listView
            property: "x"
            from: viewOffset - viewWidth 
            to: viewOffset
            duration: 100
        }
    }
    ListView {
        id: listView
        x: viewOffset; y: 0
        width: viewWidth 
        height: parent.height
        model: listModel
        focus: true
        keyNavigationWraps: true
        highlightMoveDuration: 1
    }
    ListView {
        id: prevView
        x: listView.x - viewWidth; y: listView.y
        width: viewWidth
        height: parent.height
        model: prevModel
        currentIndex: -1
        highlightMoveDuration: 1
    }
    ListView {
        id: nextView
        x: listView.x + viewWidth; y: listView.y
        width: viewWidth 
        height: parent.height
        model: nextModel
        currentIndex: -1
        highlightMoveDuration: 1
    }
}

By Jos van den Oever at Mon, 05/30/2011 - 06:18