Sailfish OS workshop: UI Development

Gave short twenty minute live coding presentation on Sailfish OS UI development at International Sailfish Community Event 2016.

Material:
Theme cheat sheet
Silica cheat sheet
Icon reference
Example code



Responsive Layouts

A layout provides visual structure for the presented information and user interface controls that allow user to navigate and modify the information. Layout principles in digital media are derived from traditional printing media. Users don’t read but scan information. They expect most recent and critical information to be located close to the top-left corner of the view. Important information and actions are often granted a bit more space on the display. Margins are used to improve readability by creating clear visual groups. Spacing between the items in groups should generally be less than the size of individual items to communicate relation. Alignment is another way to create visual groups, for example vertical items that align horizontally form a list. Lack of alignment leads to unorganised look and takes focus way from the presented information. In most cases soft strategies like thoughtful use of margins and alignment provides enough visual structure for the layouts, reducing the need for explicit borders and backgrounds.

Widgets aligned on a layout grid
Widgets aligned on a layout grid

Good layouts are designed for a specific device form factor. For example on mobile devices it is often good idea to display the most common actions close to the bottom edge of the display for easy access. If you design the interface for one-handed use also take the reach of the thumb into account. Most people are right-handed making bottom-right corner of the display easiest to reach, though too much right-handed focus can make the interface cumbersome for the left-handed people. Some user interfaces allow mirroring of the controls for left-handed users.

Layouts are commonly based on only a handful of global geometry values with larger values being multiples of the smaller ones to guarantee nice alignment and grouping of the content within an invisible grid structure. Each visual item is assigned a rectangle bounding box within the grid. A good layouting system allows developers to visualise the layout grid and item borders to make debugging of alignment issues easier.

Most views are made of some kind of list or grid layouts. This is not surprising, rectangular shapes and linear stacking is optimal for flat display surfaces, leads to even margins between the items and allows the content to align nicely with the surrounding display edges. Non-linear layouts with curved paths can be used for more striking effect, but are rarely appropriate for everyday interfaces where usability trumps the more flashy qualities.

Read more



Composing Views

Most applications are made of multiple views, with each view playing a particular role in the design. Simple applications like calculators can have only one view, whereas more complex applications like system settings often contain dozens of views. On mobile the views are generally displayed in full screen, on desktop within a window and on browser inside the browser tabs. Each view can be presented as a hierarchical tree structure with top-level parent items often acting as containers for the visible items. Simple containers just provide normalised co-ordinate system for the child items, more complex ones also functionality like scrolling, clipping and layouting of the child items. Child items inherit many other properties through the parent chain like the visibility, opacity, scale and rotation.

A view and its simplified tree presentation
A view and its simplified tree presentation

Read more



Book Project:
How to Build Beautiful User Interfaces

Last spring marked 10 years of working in the mobile industry for me. During that time I have participated in building user interfaces and frameworks in quite a few different programming languages and operating systems for many different mobile devices and products in Nokia and Jolla. For almost as long I have been jotting down notes about the craft, with the intent of some day using the material as a basis of a book. Most books about user interfaces are either written by engineers targeting specific programming languages and frameworks with limited understanding of design concerns, or written by designers lacking credible technical backbone and understanding of how to realise the designs they present. Most interesting things happen at the intersection of disciplines, my aim is to write a book that merges the two fields in one seamless, coherent body of work.

Read more



How to Be a Programmer

Learning to program is a lot like learning a foreign language. To be any good in it, you need to read a lot of other people’s code and write a lot of new code. Getting fluent in programming takes time, but like any practice reading somebody else’s code becomes easier the more you do it, and the more you write the faster you are able to produce working functionality and less mistakes you will make while writing. Young developers joining the industry often start their careers by maintaining existing software and end up writing fairly little new code, which is a real shame as that means it takes them awfully long to get productive and confident enough to trust their own thinking.

Read more



Live Pixels

Last weekend I started playing with the new Qt 5 particle effect system. One interesting effect I found in the examples was using custom particles and OpenGL shaders to color the moving particles, producing a cool-looking image on the screen made of “live” pixels. For this blog entry I have drawn an image of a skull, which in the example gets rotated and scaled across easing curves to further enhance the effect and finally fed to the particle system.

The original drawing with and without the effect
Show code »

livepixels.qml:

import QtQuick 2.0
import QtQuick.Particles 2.0

Rectangle{
    width: 800; height: 480
    ParticleSystem {
        width: image.width
        height: image.height
        anchors.centerIn: parent
        Emitter {
            size: 15
            sizeVariation: 10
            speed: PointDirection { xVariation: 8; yVariation: 8 }
            emitRate: 10000
            lifeSpan: 2000
            anchors.fill: parent
        }
        CustomParticle {
            property real maxWidth: parent.width
            property real maxHeight: parent.height
            property variant particleTexture: ShaderEffectSource {
                sourceItem: Image { source: "particle.png" }
                hideSource: true
            }
            property variant pictureTexture: ShaderEffectSource {
                sourceItem: sourceImage
                hideSource: true
                live: true
            }
            vertexShader:"
                uniform highp float maxWidth;
                uniform highp float maxHeight;
                varying highp vec2 fTex2;
                varying lowp float fFade;
                uniform lowp float qt_Opacity;

                void main() {
                    fTex2 = vec2(qt_ParticlePos.x / maxWidth, qt_ParticlePos.y / maxHeight);
                    highp float t = (qt_Timestamp - qt_ParticleData.x) / qt_ParticleData.y;
                    fFade = min(t*4., (1.-t*t)*.75) * qt_Opacity;
                    defaultMain();
                }
            "
            fragmentShader: "
                uniform sampler2D particleTexture;
                uniform sampler2D pictureTexture;
                varying highp vec2 qt_TexCoord0;
                varying highp vec2 fTex2;
                varying lowp float fFade;
                void main() {
                    gl_FragColor = texture2D(pictureTexture, fTex2) * texture2D(particleTexture, qt_TexCoord0).w * fFade;
            }"
        }
    }
    Item {
        id: sourceImage
        width: image.width; height: image.height
        anchors.centerIn: parent
        Image{
            id: image
            source: "skull.png"
            scale: 1 + 0.15*Math.random()
            Behavior on scale {
                NumberAnimation { duration: 3000; easing.type: Easing.InOutQuad }
            }
            Timer {
                interval: 3000
                repeat: true; running: true
                onTriggered: parent.scale = 1 + 0.15*Math.random()
            }
            transform: Rotation {
                id: rotation
                property real value: Math.random()
                property bool reverse: Math.random() > 0.5
                origin.x: image.width/2; origin.y: image.height/2
                axis.x: Math.random() < 0.2 || value < 0.5
                axis.y: Math.random() < 0.2 || value >= 0.5
                SequentialAnimation on angle {
                    loops: Animation.Infinite
                    NumberAnimation {
                        from: 0.0; to: rotation.reverse ? -8.0 : 8.0
                        duration: 2500; easing.type: Easing.InOutQuad
                    }
                    NumberAnimation {
                        from: rotation.reverse ? -8.0 : 8.0; to: 0.0
                        duration: 2500; easing.type: Easing.InOutQuad
                    }
                }
            }
        }
    }
}



Falling Cubes

Falling Cubes example shows two hundred cubes falling across the view port. The cubes have been colored using non-realistic Gooch shading, where the mesh surface color is mixed with warm and cold colors using surface’s normals. Gooch shader has been written in OpenGL GLSL Shading Language. Falling animation is implemented using SmoothedAnimation element.

Show code »

fallingcubes.qml:

import Qt3D 1.0
import QtQuick 1.0
import Qt3D.Shapes 1.0

Viewport {
    id: fallingCubes
    width: 800; height: 480
    property color coolColor: Qt.rgba(0.3 + 0.1*Math.random(),
                                      0.3 * Math.random(),
                                      0.5 + 0.1*Math.random())
    Item3D {
        x: -4.2; y: -1.62; scale: 0.23
        Quad {
            x: 18.4; y: 6.8; z: -10; scale: 18
            transform: Rotation3D {
                angle: 90; axis: Qt.vector3d(1.0, 0.0, 0.0)
            }
            effect: Effect {
                property real fade
                SequentialAnimation on fade {
                    id: fadeAnimation
                    property real offset: Math.random()
                    property real duration: 4000+4000*Math.random()
                    running: true; loops: Animation.Infinite
                    NumberAnimation {
                        from: 0.0; to: 1.0
                        duration: fadeAnimation.duration/2
                    }
                    NumberAnimation {
                        from: 1.0; to: 0.0
                        duration: fadeAnimation.duration/2
                    }
                }
                color: Qt.rgba(0.95 + 0.05*Math.random(),
                               0.5 + 0.2*fade,
                               0.1 + 0.1*Math.random())
            }
        }
        Repeater {
            model: 200
            FallingCube { coolColor: fallingCubes.coolColor }
        }
    }
}

FallingCube.qml:

import Qt3D 1.0
import QtQuick 1.0
import Qt3D.Shapes 1.0

Item3D {
    id: fallingCube
    property color coolColor
    function reset() {
        yBehavior.enabled = false;
        fallingCube.y = 25
        yBehavior.enabled = true;
        fallingCube.x = 40*Math.random();
        fallingCube.y = -15
    }
    Component.onCompleted: y = -15
    x: 40*Math.random(); y: 30*Math.random()-10; z: 20*(Math.random()-0.5)
    Behavior on x { SmoothedAnimation { velocity: 2*Math.random() } }
    Behavior on y {
        id: yBehavior
        SmoothedAnimation { velocity: 6+Math.random()*6 }
    }
    Timer {
        interval: 100; repeat: true; running: true
        onTriggered: if (fallingCube.y < -9) fallingCube.reset()
    }
    Cube {
        scale: 2
        effect: GoochShading {
            coolColor: fallingCube.coolColor
            warmColor: Qt.rgba(0.9 + 0.1*Math.random(),
                               0.2 + 0.2*Math.random(),
                               0.2*Math.random())
        }
        transform: Rotation3D {
            axis: Qt.vector3d(Math.random(), Math.random(), Math.random())
            SequentialAnimation on angle {
                id: angleAnimation
                property real offset: 360*Math.random()
                property real duration: 3000+3000*Math.random()
                running: true; loops: Animation.Infinite
                NumberAnimation {
                    from: angleAnimation.offset; to: 360
                    duration: (360-angleAnimation.offset)*angleAnimation.duration/360
                }
                NumberAnimation {
                    from: 0; to: angleAnimation.offset
                    duration: angleAnimation.offset*angleAnimation.duration/360
                }
            }
        }
    }
}

GoochShading.qml:

import Qt3D 1.0
import QtQuick 1.0

ShaderProgram {
    property color warmColor
    property color coolColor
    vertexShader: "
        attribute highp vec4 qt_Vertex;
        attribute highp vec3 qt_Normal;

        uniform lowp vec4 warmColor;
        uniform lowp vec4 coolColor;

        uniform highp mat3 qt_NormalMatrix;
        uniform highp mat4 qt_ModelViewProjectionMatrix;
        uniform highp mat4 qt_ModelViewMatrix;

        varying float facingProjector;

        struct qt_SingleLightParameters {
            mediump vec4 position;
            mediump vec3 spotDirection;
            mediump float spotExponent;
            mediump float spotCutoff;
            mediump float spotCosCutoff;
            mediump float constantAttenuation;
            mediump float linearAttenuation;
            mediump float quadraticAttenuation;
        };
        uniform qt_SingleLightParameters qt_Light;

        void main(void)
        {
            highp vec4 vertex = qt_ModelViewMatrix * qt_Vertex;
            highp vec3 normal = normalize(qt_NormalMatrix * qt_Normal);
            highp vec3 light = normalize(qt_Light.position.xyz - vertex.xyz);
            facingProjector = clamp(dot(normal, light), 0.0, 1.0);
            gl_Position = qt_ModelViewProjectionMatrix * qt_Vertex;
        }
    "
    fragmentShader: "
        uniform lowp vec4 warmColor;
        uniform lowp vec4 coolColor;

        varying float facingProjector;

        void main(void)
        {
            highp float ratio = 0.5 * (facingProjector + 1.0);
            gl_FragColor = mix(warmColor, coolColor, ratio);
        }
    "
}



Overlapping Letters

Show code »

helloworld.qml:

import QtQuick 1.0

Rectangle {
    width: 800; height: 200
    property string label: "Hello world!"
    property variant fontFamilies: [ "Georgia", "Verdana", "Tahoma",
                                     "Lucida Console", "Century Gothic",
                                     "Courier New",  "Times New Roman"]
    function randomFont() {
        return fontFamilies[Math.floor(Math.random()*fontFamilies.length)]
    }
    Row {
        id: row
        spacing: -10-10*Math.random()
        anchors.centerIn: parent
        Repeater {
            model: label.length
            Text {
                text: label[index]
                opacity: 0.5+0.5*Math.random()
                anchors.verticalCenter: parent.verticalCenter
                property real distance: 2*Math.abs(index-label.length/2) /
                                        label.length
                font {
                    family: randomFont()
                    capitalization: Font.AllUppercase
                    pixelSize: 130+30*Math.random()-50*distance
                }
                Repeater {
                    model: 5+5*Math.random()
                    Text {
                        text: parent.text
                        opacity: 0.6+0.4*Math.random()
                        anchors.centerIn: parent
                        font {
                            family: randomFont()
                            capitalization: Font.AllUppercase
                            pixelSize: 130+30*Math.random()-50*distance
                        }
                    }
                }
            }
        }
    }
}

Show code »

alphabets.qml:

import QtQuick 1.0

Rectangle {
    width: grid.width+100; height: grid.height+100
    property real letterHeight
    property real letterWidth
    property string alphabets: "abcdefghijklmnopqrstuvwxyzåäö"
    property variant fontFamilies: [ "Castellar", "Century Gothic",
                                     "Courier New", "Bodoni MT Condensed",
                                     "AngsanaUPC", "Bodoni MT Black",
                                     "Copperplate Gothic Bold", "Pistilli",
                                     "Rockwell Extra Bold"]
    function randomFont() {
       return fontFamilies[Math.floor(Math.random()*fontFamilies.length)]
    }
    function resizeLetters() {
        var rows = alphabets.length/grid.columns
        letterHeight = Math.round(grid.height/rows)
        letterWidth = Math.round(grid.width/grid.columns)
        for (var index = 0; index < grid.children.length; index++) {
            var child = grid.children[index]
            if (child.width != undefined) {
               child.height = letterHeight
               child.width = letterWidth
            }
        }
    }
    Timer {
        interval: 50
        running: true
        onTriggered: resizeLetters()
    }
    Grid {
        columns: 6
        anchors { centerIn: parent; verticalCenterOffset: -15 }
        Repeater {
            model: alphabets.length
            Rectangle {
                height: letterHeight
                width: letterWidth
                color:  Qt.rgba(0.4+0.6*Math.random(),
                                0.4+0.6*Math.random(),
                                0.4+0.6*Math.random())
            }
        }
    }
    Grid {
        id: grid
        columns: 6
        anchors { centerIn: parent; verticalCenterOffset: -15 }
        Repeater {
            model: alphabets.length
            Text {
                text: alphabets[index]
                font {
                    pixelSize: 120
                    family: randomFont()
                    capitalization: Font.AllUppercase
                }
                Repeater {
                    model: 3+Math.random()*2
                    Text {
                        text: parent.text
                        anchors.centerIn: parent
                        font {
                            pixelSize: 120
                            family: randomFont()
                            capitalization: Font.AllUppercase
                        }
                    }
                }
            }
        }
    }
}



Color Strips

Finding a good color palette for your project can be difficult. I’d love to learn the mysterious formulas behind harmonic color patterns, but so far coming up with suitable combinations has been mostly a matter of trial and error: the examples below are no different. If you don’t want to go through the hassle of inventing the schemes yourself, Adobe’s Kuler website is filled with ready-made palettes.

Randomly colored wall of horizontal strips

Show code »

ColorColumn.qml:

import QtQuick 1.0

Column {
    width: 800; height: 400
    property real pixelsPerRelativeHeight
    function rectangleColor() {
        var value = Math.random()
        var red = value < 0.4 ? 0.8+0.2*Math.random() : 0.1+0.4*Math.random()
        var green = value > 0.4 && value < 0.8 ? 0.7+0.3*Math.random() : 0.4+0.3*Math.random()
        var blue = value > 0.8 ? 0.6+0.2*Math.random() : 0.2*Math.random()
        return Qt.rgba(red, green, blue)
    }
    Component.onCompleted: {
        var totalRelativeHeight = 0
        for (var index = 0; index < children.length; index++) {
            if (children[index] !== repeater)
                totalRelativeHeight += children[index].relativeHeight
        }
        pixelsPerRelativeHeight = height/totalRelativeHeight
    }
    Repeater {
        id: repeater
        model: 40
        Rectangle {
            width: parent.width
            color: rectangleColor()
            height: Math.ceil(relativeHeight*pixelsPerRelativeHeight)
            property real value: Math.random()
            property real relativeHeight: Math.pow(Math.random(),3)
        }
    }
}


Randomly colored wall of vertical strips

Show code »

ColorRow.qml:

import QtQuick 1.0

Row {
    width: 800; height: 400
    property real pixelsPerRelativeWidth
    function rectangleColor() {
        var isRed = Math.random() < 0.1
        var grayness = 0.2+0.6*Math.random()
        var red = isRed ? grayness+0.6*Math.random()*(1-grayness) : grayness
        var green = isRed ? 0.5*grayness+0.5*Math.random()*grayness : 1.3*grayness
        var blue = isRed ? 0.8*grayness+0.4*Math.random()*grayness : 1.3*grayness
        return Qt.rgba(red, green, blue)
    }
    Component.onCompleted: {
        var totalRelativeWidth = 0
        for (var index = 0; index < children.length; index++) {
            if (children[index] !== repeater)
                totalRelativeWidth += children[index].relativeWidth
        }
        pixelsPerRelativeWidth = width/totalRelativeWidth
    }
    Repeater {
        id: repeater
        model: 80
        Rectangle {
            height: parent.height
            color: rectangleColor()
            width: Math.ceil(relativeWidth*pixelsPerRelativeWidth)
            property real relativeWidth: Math.pow(Math.random(),3)
        }
    }
}


Vertical segments of horizontal strips

Show code »

colorstrips_rowofcolumns.qml:

import QtQuick 1.0

Row {
    width: 800; height: 400
    property real pixelPerRelativeWidth
    Component.onCompleted: {
        var totalRelativeWidth = 0
        for (var index = 0; index < children.length; index++) {
            if (children[index] !== repeater)
                totalRelativeWidth += children[index].relativeWidth
        }
        pixelPerRelativeWidth = width/totalRelativeWidth
    }
    Repeater {
        id: repeater
        model: 50
        ColorColumn {
            height: parent.height
            width: Math.ceil(relativeWidth*pixelPerRelativeWidth)
            property real relativeWidth: Math.pow(Math.random(),3)
            function rectangleColor() {
                var value = Math.random()
                var blue = value < 0.4 ? 0.8+0.2*Math.random() : 0.1+0.4*Math.random()
                var red = value > 0.4 && value < 0.8 ? 0.7+0.3*Math.random() : 0.4+0.3*Math.random()
                var green = value > 0.8 ? 0.2+0.2*Math.random() : 0.2*Math.random()
                return Qt.rgba(red, green, blue)
            }
        }
    }
}


Horizontal segments of vertical strips

Show code »

colorstrips_columnofrows.qml:

import QtQuick 1.0

Column {
    width: 800; height: 400
    property real pixelsPerRelativeHeight
    Component.onCompleted: {
        var totalRelativeHeight = 0
        for (var index = 0; index < children.length; index++) {
            if (children[index] !== repeater)
                totalRelativeHeight += children[index].relativeHeight
        }
        pixelsPerRelativeHeight = height/totalRelativeHeight
    }
    Repeater {
        id: repeater
        model: 40
        ColorRow {
            width: parent.width
            height: Math.ceil(relativeHeight*pixelsPerRelativeHeight)
            property real relativeHeight: Math.pow(Math.random(),3)
            function rectangleColor() {
                var isBlue = Math.random() < 0.2
                var grayness = 0.2+0.6*Math.random()
                var red = isBlue ? 0.5*grayness+0.5*Math.random()*grayness : grayness
                var green = isBlue ? grayness+0.6*Math.random()*(1-grayness) : 1.3*grayness
                var blue = isBlue ? 0.7*grayness+0.3*Math.random()*grayness : 1.3*grayness
                return Qt.rgba(red, green, blue)
            }
        }
    }
}



Horizontal Flow

Horizontal Flow example shows how to implement a horizontally flickable list of items like a list of album covers. The component is based on the PathView element. Also, horizontal alphabetic scrollbar is provided on the bottom of the page for quickly jumping to a new position. Horizontal Flow also supports arrow key-based navigation for media center-style UIs.

Show code »

horizontalflow.qml:

import QtQuick 1.0

Rectangle {
    color: "black"
    width: 800; height: 280
    HorizontalPathView {
        id: horizontalPathView
        model: Model { id: model }
        delegate: Delegate {}
        anchors.fill: parent
    }
    AlphabeticScrollbar {
        view: horizontalPathView
        alphabets: model.alphabets
        alphabetIndeces: model.alphabetIndeces
        anchors {
            left: parent.left
            right: parent.right
            bottom: parent.bottom
            margins: 3
        }
    }
}

HorizontalPathView.qml:

import QtQuick 1.0

PathView {
    id: horizontalPathView
    focus: true
    highlight: Item {}
    pathItemCount: 11
    preferredHighlightBegin: 0.5; preferredHighlightEnd: 0.5
    path: Path {
        startX: -400; startY: horizontalPathView.height/2-70
        PathAttribute { name: "iconScale"; value: 0.5 }
        PathQuad {
            x: horizontalPathView.width/2
            y: horizontalPathView.height/2-20
            controlX: horizontalPathView.width/4
            controlY: horizontalPathView.height/2-45
        }
        PathAttribute { name: "iconScale"; value: 1.0 }
        PathQuad {
            x: horizontalPathView.width+400
            y: horizontalPathView.height/2-70
            controlX: 3*horizontalPathView.width/4
            controlY: horizontalPathView.height/2-45
        }
        PathAttribute { name: "iconScale"; value: 0.5 }
    }
    Keys.onLeftPressed: decrementCurrentIndex()
    Keys.onRightPressed: incrementCurrentIndex()
    function zLevel(index) {
        var dist = Math.abs((currentIndex - index) % count)
        if (dist > (pathItemCount/2.0 + 1))
            dist = count - dist
        return Math.floor(pathItemCount/2.0) - dist
    }
}

Model.qml:

import QtQuick 1.0

ListModel {
    property variant alphabets: []
    property variant alphabetIndeces: []
    function calculateAlphabets() {
        var newAlphabets = []
        var newAlphabetIndeces = []
        var previousItem, item = " "
        for (var index = 0; index < count; index++) {
            previousItem = item
            item = get(index).item
            if (previousItem.charAt(0) != item.charAt(0)) {
                newAlphabets[newAlphabets.length] = item.charAt(0)
                newAlphabetIndeces[newAlphabetIndeces.length] = index
            }
        }
        alphabets = newAlphabets
        alphabetIndeces = newAlphabetIndeces
    }
    Component.onCompleted: populate()
    function populate() {
        // go from A to Z
        for (var index = 65; index < 91; index++) {
            var alphabet = String.fromCharCode(index)
            var alphabetCount = Math.floor(Math.random()*5)
            for (var index2 = 0; index2 < alphabetCount; index2++)
                append({"number": count, "item": alphabet})
        }
        calculateAlphabets()
    }
}

Delegate.qml:

import QtQuick 1.0

Rectangle {
    width: 200; height: 200
    scale: PathView.iconScale
    z: PathView.view.zLevel(index)
    color: Qt.rgba(0.5+(PathView.view.count - number)*Math.random()/PathView.view.count,
                   0.3+number*Math.random()/PathView.view.count, 0.3*Math.random(), 0.7)
    signal clicked
    Keys.onReturnPressed: clicked()
    onClicked: PathView.view.currentIndex = index
    MouseArea {
        id: delegateMouse
        anchors.fill: parent
        onClicked: parent.clicked()
    }
    Rectangle {
        color: Qt.rgba(0.2, 0.3, 0.8)
        opacity: delegateMouse.pressed ? 0.8 : 0.0
        anchors.fill: parent
    }
    Text {
        smooth: true
        color: "white"
        font.pixelSize: 30
        text: "ITEM\n" + item
        anchors.centerIn: parent
        horizontalAlignment: Text.AlignHCenter
    }
}

AlphabeticScrollbar.qml:

import QtQuick 1.0

MouseArea {
    width: 400; height: 50
    property variant view
    property variant alphabets
    property variant alphabetIndeces
    property int currentIndex
    property real letterWidth: (width + 10)/alphabets.length
    onPressed: updatePosition(mouse.x)
    onPositionChanged: updatePosition(mouse.x)
    function updatePosition(x) {
        var index = Math.round((x-10)/letterWidth);
        if (index >= 0 && index < alphabetIndeces.length) {
            currentIndex = index;
            view.currentIndex = alphabetIndeces[index];
        }
    }
    Rectangle {
        radius: 10
        color: "white"
        visible: parent.pressed
        height: letterWidth+60; width: letterWidth+30
        x: Math.min(Math.max(0,parent.mouseX-width/2), parent.width-width)
        anchors { bottom: parent.bottom; bottomMargin: -10 }
        Text {
            font.pixelSize: 34
            text: alphabets[currentIndex]
            anchors { centerIn: parent; verticalCenterOffset: -20 }
        }
    }
    Rectangle {
        height: 20
        color: Qt.rgba(1.0, 1.0, 1.0, 0.2)
        anchors {left: parent.left; right: parent.right; bottom: parent.bottom }
    }
}