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.
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
new version
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