Stacked Pages App

May 27, 2016 — Leave a comment

Edited 2016-06-06: Buttons with focusPoliy: Qt.NoFocus

Edited 2016-06-05: Updated for Qt 5.7 RC

Overview

You should have read the blog about my first app using Qt 5.7 Material Style and also explored the sources before reading on.

This is part 2 of a series of demo apps helping you to understand Qt 5.7 for x-platform development on Android and iOS.

This time I’ll explain HowTo use StackView – one of the new Qt Quick Controls 2. This app will also use Buttons as ‘Raised Buttons‘ and ‘Flat Buttons‘.

Coming from BlackBerry10 Cascades ? StackView is similar to Cascades NavigationPane: you can push() pages on top and pop() pages to remove them from the stack.

You should have installed Qt 5.7 RC.

New to Qt 5 development ? Please read my blog series from the beginning to learn some basic stuff and to understand this app.

android_page_02

The Source

This app can be downloaded as Open Source from  github: https://github.com/ekke/stacked_pages_x

Project structure and .pro

The project structure is similar to demo-app-one-page-x.

proj_structure_stack

There’s a new folder Sources/pages where the qml files of pages 1 to 5 are stored. PageOne is the root page and PageTwo … PageFive can be pushed on the stack of pages.

main.cpp and ApplicationUI (C++) are also similar to the one-page-x-app.

UI Constants (C++) / Buttons (QML)

A new property was added to UI Constants ThemePalette:

  • flatButtonTextColor

Using Flat Buttons uncolored, the text color is different for dark / light theme. Here are Flat Buttons from PageThree: the left one colored with accentColor, the right one uncolored automatically changing color if theme changes:

android_page_03_flat

Raised Buttons have a colored background and text color depends from background color (accent, primary, primary light, primary dark) Here are Raised Buttons with accent color (‘POP’ Button) and primary color (‘PUSH 3’, ‘GOTO 5’ Button) from PageTwo:

android_page_02_buttons

At Bottom right position the Floating Action Button (FAB) is placed.

Attention: If Raised Buttons are colored with primaryColor, you should use a different color for the FAB. FAB’s stay at a fixed position and can overlap other parts while scrolling. You can try this out changing the orientation to Landscape. Here’s PageTwo in Landscape:

android_page_02_landscape

stacked-pages-x app is using primary dark for FAB’s.

Custom Controls: Raised and Flat Buttons

Qt 5.7 provides a new Button Control. stacked-pages app uses two customized Buttons: a Raised one and a Flat one. Both are found under Sources/common. Read more about Material Raised and Flat Buttons here from Google Material Design Guide.

ButtonRaised.qml:

Button {
    id: button
    // default: textOnPrimary
    property alias textColor: buttonText.color
    // default: primaryColor
    property alias buttonColor: buttonBackground.color
    focusPolicy: Qt.NoFocus
    Layout.fillWidth: true
    Layout.preferredWidth : 1
    leftPadding: 6
    rightPadding: 6
    contentItem: Text {
        id: buttonText
        text: button.text
        opacity: enabled ? 1.0 : 0.3
        color: textOnPrimary
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        elide: Text.ElideRight
        font.capitalization: Font.AllUppercase
    }
    background:
        Rectangle {
        id: buttonBackground
        implicitHeight: 48
        color: primaryColor
        radius: 2
        opacity: button.pressed ? 0.75 : 1.0
        layer.enabled: true
        layer.effect: DropShadow {
            verticalOffset: 2
            horizontalOffset: 1
            color: dropShadow
            samples: button.pressed ? 20 : 10
            spread: 0.5
        }
    } // background
} // button

ButtonFlat.qml:

Button {
    id: button
    // default: flatButtonTextColor
    property alias textColor: buttonText.color
    focusPolicy: Qt.NoFocus
    Layout.fillWidth: true
    Layout.preferredWidth : 1
    leftPadding: 6
    rightPadding: 6
    contentItem: Text {
        id: buttonText
        text: button.text
        opacity: enabled ? 1.0 : 0.3
        color: flatButtonTextColor
        horizontalAlignment: Text.AlignHCenter
        verticalAlignment: Text.AlignVCenter
        elide: Text.ElideRight
        font.capitalization: Font.AllUppercase
        font.weight: Font.Medium
    }
    background:
        Rectangle {
        id: buttonBackground
        implicitHeight: 48
        Layout.minimumWidth: 88
        color: button.pressed ? buttonText.color : "transparent"
        radius: 2
        opacity: button.pressed ? 0.12 : 1.0
    } // background
} // button

You see how easy it is to customize Qt Quick Controls 2 Button and it’s also easy to use these customized Controls inside the app. Here are the three Raised Buttons from PageTwo:

RowLayout {
    // implicite fillWidth = true
    spacing: 10
    ButtonRaised {
        text: "Pop"
        buttonColor: accentColor
        onClicked: {
            navPane.popOnePage()
        }
    }
    ButtonRaised {
        text: "Push 3"
        onClicked: {
            navPane.pushOnePage(pageThree)
        }
    }
    ButtonRaised {
        text: "GoTo 5"
        onClicked: {
            navPane.goToPage(5)
        }
    }
} // button row

How the FAB is customized I have described as part of one-page-x app blog article.

ApplicationWindow (main.qml)

main.qml imports, ApplicationWindow and header are similar to one-page-x app – but header is using another ToolBar from Sources/common/StackTextTitle:

header: StackTextTitle {
    id: titleBar
    text: navPane.currentItem? navPane.currentItem.title : qsTr("A simple Stacked - Pages APP")
}

StackTextTitle’s text is bound to currentItem from StackView and text changes if another Page becomes the topmost page – the ‘currentItem’. There’s also a new ToolButton added at left side in ToolBar:

ToolButton {
    enabled: navPane.depth > 1
    focusPolicy: Qt.NoFocus
    Image {
        id: backImageImage
        visible: navPane.depth > 1
        anchors.centerIn: parent
        source: "qrc:/images/"+iconOnPrimaryFolder+"/arrow_back.png"
    }
    onClicked: {
        navPane.popOnePage()
    }
}

You can see that the visibility of this ToolButton is bound to ‘depth’ property from StackView: only if depth > 1 the Button is visible.

Here’s the TitleBar for the first page (depth == 1) and for the second page (depth == 2):

android_page_01_02_titlebar

The options menu (three dots at right side from ToolBar) looks different to the previous app. Now you can:

  • switch between dark and light theme
  • select primary color
  • select accent color

main.qml contains the Popup to select the colors – already described in one-page-x app.

Two other controls we’ll find in main.qml:

  • StackView
  • Floating Action Button (FAB)

ApplicationWindow –> FAB

From screenshots above you have seen that the FAB always stays on top on a fixed position. At first I tried to use the ‘footer’ from ApplicationWindow, but this doesn’t work, because the footer occupies the complete space besides the FAB, where I only want to have the FAB on top without hiding more. Thanks to @jpnurmi I learned that I can place the FAB without using the footer by simply defining the FAB in main.qml besides StackView.

Coming from BlackBerry10 Cascades ? This is more flexible in Qt 5.7. In Cascades there’s only one tree with a Pane as root. In Qt 5.7 you can place different controls onto same space and ‘z’ order decides which will overlap.

FAB (z:1) stays on top of StackView (z:0). PopupPalette is only visible when opened. This is the ApplicationWindow structure:

ApplicationWindow {
	// ...
    FloatingActionButton {
        property string imageName: navPane.depth < 5? "/directions.png" : "/home.png"
        z: 1
        anchors.margins: 16
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        imageSource: "qrc:/images/"+iconOnPrimaryDarkFolder+imageName
        backgroundColor: primaryDarkColor
        onClicked: {
            if(navPane.depth < 5) {
                navPane.pushNextPage()
            } else {
                navPane.goToPage(1)
            }
        }
    } // FAB
	StackView {}
	PopupPalette {}
} // ApplicationWindow

Notice: the Image and onClick behavior are different: On Pages 1 to 4 FAB displays ‘directions.png’ and pushes the next page, where on Page 5 FAB displays ‘home.png’ and provides a short path to jump back to the root (Home):

FABs

ApplicationWindow –> StackView

StackView is one of the new Qt Quick Controls 2 to enable comfortable navigation through mobile apps. Our previous app was a simple one-page app, where all informations was displayed in one page. There are sometimes use-cases where you only need one page (plus Popups), but normaly there are more pages.

per example:

  • List of Orders
  • Tap on a Order to see the Details
  • From Details tap on a Part to see Article Details
  • From Article take a look at Inventory

So you’re opening a page on top of another one or go back to previous level: a typical stack.

Starting the app you want to display first informations, so normaly you’ll show the root object (List of Orders in example above). This first object is the initialItem. To place a page on top, you must push() another one. To go back you have to pop() the topmost Page.

Want to know how many Pages are already on your stack ? depth will let you know this, where a depth of 1 means, there’s only the root Page.

What kind of ‘Pages’ can you place on a StackView ? Item, Component or url.

stacked-pages-app always uses Components. Components are only definitions, no object will be instatiated before you create it and at the end of lifecycle you have to destroy a Component. Components are great for performance and memory – I always try to instatiate objects lazy: only when they’re needed.

StackView manages Components really cool: you don’t have to think about create() or destroy() – all is done by-magic from StackView itself.

By default push(component) will take the Component, create() the object and put it on top of your stack. Going back and pop() will automatically destroy the object and display the underlying page.

Take a look at StackView inside stacked-pages-app:

    StackView {
        id: navPane
        focus: true
        anchors.fill: parent
        initialItem: pageOne
		// ....
	} // navPane
    Component {
        id: pageOne
        PageOne {
        }
    } // pageTwo

    Component {
        id: pageTwo
        PageTwo {
        }
    } // pageTwo
	// ...

StackView will fill the complete space of parent (ApplicationWindow).

‘initialItem’ (pageOne) is a Component defined in Sources/pages/PageOne.qml and will be created automatically at startup.

Doing a push(pageTwo) will create the page from Sources/pages/PageTwo.qml, put it on top and will display that page.

Push() and pop() are done using default Transitions working well in Material styled apps, but you can customize Transitions.

Coming from BlackBerry10 Cascades ? StackView is like Cascades NavigationPane, where you also can push() and pop() pages. But if you’re using Cascades ComponentDefinition you must create() the objects by yourself and also destroy() as soon as pop() was done.

StackView has more power as push() / pop() and is very flexible, so I’m sure it’ll fit with your use-cases. You can push() an array of pages, which by default will mean all components will be put on the stack as placeholders and only the top most page will be created() and displayed.  Setting behavior as StackView.ForceLoad all components inside the array will be instantiated immediately. So it’s up to you what’s the best.

Also pop() can be used in different ways: pop() pops the topmost page, pop(null) goes back to the root page, pop(myPage) does a pop() until myPage will be reached. If pop() reaches a ‘placeholder’ placed by an array-push before, the object will be created automatically and then displayed.

One of my BlackBerry10 Cascades apps had the requirement to jump between some stacks with deep depth and to position to a specific one – having the possibility to push() an array of pages without creating all would have been very helpful.

From time to time you’ll need access to a specific page in your stack, you can get() an Item from  a given index, where the root page has index 0.

Attention: if get(index) points to a non-loaded page you’ll get ‘null’ or you must do a get(index, StackView.ForceLoad) to be sure that the object will be instatiated if not already done.

Pushing pages on top, all Items underneath stay alive. But – if you wish so – there are some tricks to get them unloaded wile push() something on top:

unload_item

You see: you have all the freedom what should be loaded or not and when to unload.

StackView – UI – Business Logic

We have seen that it’s easy to push() or pop() pages and StackView does creation and destruction. But in business apps in many cases there’s more to do if a page will be displayed or closed.

To demonstrate how you can do such kind of stuff, I implemented init() function to be called directly after push() and cleanup() function called when pop() is done for a page.

per ex Sources/pages/PageOne.qml:

Flickable {
    property string name: "PageOne"
    property string title: qsTr("Root Page in Stack of max 5")
	// ...
    Pane {
	    // ... content ...
	} // pane
    ScrollIndicator.vertical: ScrollIndicator { }
	//
    // called immediately after push()
    function init() {
        console.log(qsTr("Init done from One"))
    }
    // called immediately after pop()
    function cleanup() {
        console.log(qsTr("Cleanup done from One"))
    }
} // Flickable

But from where should these functions be called ? How to be sure that init() is called if StackView lazy creates a page from a placeholder (Component)  while doing a pop() ?

Coming from BlackBerry10 Cascades ? NavigationPane gives you a onPopTransitionEnded(page) signal to know that a pop() was called for the page. In Qt 5.7 there’s a Transition for pop(), but no signal when it’s done. Fortunately pop() and push() are giving you the page as return value.

This enables you to do something like this for push()

function pushOnePage(pageComponent) {
    var page = push(pageComponent)
    page.init()
}

Doing a pop() you have cleanup() that page and for the new currentItem you must check if it already was initialized or not:

function popOnePage() {
    if(navPane.depth == 1) {
        return
    }
    // check if target page already is on the stack
    var targetIsUninitialized = false
    if(!navPane.get(navPane.depth-2)) {
        targetIsUninitialized = true
    }
    var page = pop()
    if(targetIsUninitialized) {
        navPane.currentItem.init()
    }
    // do cleanup from previous page
    page.cleanup()
} // popOnePage

Take a look at the other functions I implement to solve jumping back some levels deeper or pushing arrays of pages.

From code above you can see that my pages have two properties added:

  • property string name
  • property string title

‘name’ is used to demonstrate find() and ‘title’ is bound to ToolBar as already described above.

StackView – Navigation Back and Forward

You can go different ways to navigate through the stack:

  • click on a (Raised or Flat) Button to push() or go back – pop()
  • click on ‘Back’ from ToolBar to go one level back – pop()
  • click on Android System ‘Back’ key to go one level back
  • click on FAB to go to the next page – push() – or back to the root

ToolBar, Buttons and FAB we have discussed before – what about the Android System ‘Back’ key ?

android_back

This is easy to implement:

    StackView {
        id: navPane
        focus: true
        anchors.fill: parent
        initialItem: pageOne
        // support of BACK key
        Keys.onBackPressed: {
            event.accepted = navPane.depth > 1
            popOnePage()
            if(navPane.depth == 1) {
                // perhaps ask user if app should really quit
                var page = navPane.get(0)
                page.cleanup()
            }
        }
		// ...

StackView gets focus: true and is listening for Keys.onBackPressed. If there are pages on top of root page, pressing the ‘Back’ key does a pop() to go one level back. If root page is reached, pressing Back again the app will be closed on Android. Could be a good idea to ask the user using a Popup if app really should be closed now.

Attention: Tapping on the options menu (three dots) from ToolBar will move the focus and StackView won’t catch the Back key anymore. To solve this, watch for onAboutToHide() from Menu() and reset the focus:

Menu (ToolButton top-right on ToolBar clicked):

Menu {
    id: optionsMenu
    x: parent.width - width
    transformOrigin: Menu.TopRight
    MenuItem {
        text: isDarkTheme? qsTr("Light Theme") : qsTr("Dark Theme")
        onTriggered: {
            themePalette = myApp.swapThemePalette()
        }
    }
    MenuItem {
        text: qsTr("Select Primary Color")
        onTriggered: {
            popup.selectAccentColor = false
            popup.open()
        }
    }
    MenuItem {
        text: qsTr("Select Accent Color")
        onTriggered: {
            popup.selectAccentColor = true
            popup.open()
        }
    }
    onAboutToHide: {
        appWindow.resetFocus()
    }
} // end optionsMenu

ApplicationWindow – resetFocus():

    // we can loose the focus if Menu or Popup is opened
    function resetFocus() {
        navPane.focus = true
    }

Now StackView (navPane) gets the focus back.

StackView – Shortcuts BlackBerry PRIV

There are even more ways to navigate if your device has a physical keyboard connected:

  • BlackBerry PRIV (Android Slider) with hardware keyboard
  • Bluetooth Keyboard attached to Android or iOS device

Here’s the BlackBerry PRIV keyboard:

priv_shortcuts

Attention: with Qt 5.7 RC I had some trouble using Space and Shift+Space – sometimes also the on-clicked from a visible Button was executed. Will do a Bugreport. So I changed to ‘n’ for NEXT and ‘p’ for PREVIOUS

You can use ‘Shortcut‘:

  • Typing 1…5 or equivalent ‘w’ ‘e’ ‘r’ ‘s’ ‘d’ goes to Page 1…5 back or forward
  • Typing ‘SPACE’ or ‘n’ goes to next page (push) until page 5 reached
  • Typing Shift + Spacebar or ‘p’ goes to previous page (pop) until root page reached

Using Shortcut is easy done – here are the Shortcuts for GoTo Page 4 and GoTo Next Page as example:

StackView {
        // ...
        Shortcut {
            sequence: "s"
            onActivated: navPane.goToPage(4)
        }
        Shortcut {
            sequence: "Alt+s" // keyboard sequence for '4'
            onActivated: navPane.goToPage(4)
        }
		// ...
        Shortcut {
            sequence: " "
            onActivated: navPane.pushNextPage()
        }
		// ...

Shortcuts are defined at StackView and because all pages pushed on the stack are owned by StackView, the Shortcuts are always recognized.

For Business apps it’s important to provide smooth workflow to users and using Shortcuts can improve UX much.

StackView – Workflow

The implemented push() – pop() workflow from stacked-pages-app is something like this snippet:

workflow

Feel free to take a look at the Button’s onClicked() functionality to see what happens under the hood.

Also each page has a short description, per ex. here’s page 5:

android_page_05

Summary

This app is only using some of the powerful features StackView provides you for navigation through a stack of pages. Remember: this is not a production-ready app.

StackV iew is not the only way to navigate – another way is using a SwipeView.


← Back (One Page App)

→ Next Article (Swiped Pages App)

⇐ Home (Overview / Topics)

 

No Comments

Be the first to start the conversation!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s