Sailfish Application Features

When writing Sailfish applications, there are features and implementation patterns to be aware of to improve your app's presentation and user experience. The Sailfish Silica module provides ways to access these features and standardize your application UI so that it looks and feels like other Sailfish applications.

The application page stack

Every Sailfish application has a page stack that contains the screens of content to be shown in the application window. This allows the user to move between different screens, or pages, within the app; only one page on the stack is visible at any time. To show a new page, add it to the top. To close the current page and return to the one that was previously displayed, remove the page from the stack.

Pages are created with the Page type, and the application's page stack is accessible as a PageStack instance through the window's pageStack property. The first page in the stack, shown when the application starts, is set by the window's initialPage.

To add or push a page to the stack, call the stack's push() function with a QML file URL, or a Component with a top-level Page. To remove the page at the top of the stack (that is, the currently visible page), call the stack's pop() function. To get a reference to this top-most page, use currentPage, and to get the current number of pages in the stack, use depth.

Here's an example of the page stack in action. This allows pages to be continually added with push() or removed with pop():

 import QtQuick 2.2
 import Sailfish.Silica 1.0

 ApplicationWindow {
     initialPage: pageComponent

     Component {
         id: pageComponent

         Page {
             PageHeader {
                 title: "Page count: " + pageStack.depth
             }

             Column {
                 width: parent.width
                 anchors.centerIn: parent
                 spacing: Theme.paddingLarge

                 Button {
                     text: "Push a page"
                     anchors.horizontalCenter: parent.horizontalCenter
                     onClicked: {
                         pageStack.push(pageComponent)
                     }
                 }

                 Button {
                     text: "Pop this page"
                     anchors.horizontalCenter: parent.horizontalCenter
                     onClicked: {
                         pageStack.pop()
                     }
                 }
             }
         }
     }
 }

(When the user swipes to the right or tap the top-left page indicator to move back to the previous page, this pops the page off the stack in the same way as pop().)

While you can pass a Component to the push() function to add the page to the stack, if the page is defined in a separate QML file, it's preferable to push the page using the URL instead:

 Button {
     text: "Add a page"
     onClicked: pageStack.push(Qt.resolvedUrl("MyPage.qml"))
 }

This postpones the page compilation and construction until push() is called, which can create a noticable difference if the page is complex and takes some time to compile.

There are a number of other operations you can do with a PageStack. These include:

  • Call push() with a set of initial properties for the page, or pass PageStackAction.Immediate to push the page immediately without the sliding animation to the new page
  • Call pop() and pass a page that is lower in the stack, so that all pages above it are popped
  • Instead of push(), use replace() to replace the current page, rather than adding a new one
  • Instead of push(), use pushAttached() to add a new page without displaying it, allowing the user to swipe forward to it instead

See the PageStack documentation for the full details.

Using dialogs for user input screens

Dialog are special types of pages that present content that needs to be confirmed or canceled by the user, or request user input while providing the ability to confirm or cancel the input operation. A Dialog can be swiped forwards as an action of confirmation by the user, or swiped backwards (as with an ordinary Page) as an act of cancelation. Additionally, dialogs use DialogHeader (rather than PageHeader) in order to display "Accept" and "Cancel" buttons at the top of the page.

Below is a page with a button that shows a dialog with a set of options when clicked:

 import QtQuick 2.2
 import Sailfish.Silica 1.0

 ApplicationWindow {
     initialPage: firstPage

     Component {
         id: firstPage
         Page {
             PageHeader { id: header }

             Button {
                 text: "Show dialog"
                 anchors.centerIn: parent
                 onClicked: {
                     pageStack.push(dialogComponent)
                 }
             }
         }
     }

     Component {
         id: dialogComponent
         Dialog {
             property string selectedOption: options.currentItem.text

             DialogHeader {
                 id: header
                 title: "Choose an option"
             }

             ComboBox {
                 id: options
                 label: "Options:"
                 anchors.top: header.bottom
                 width: parent.width

                 menu: ContextMenu {
                     MenuItem { text: "Option 1" }
                     MenuItem { text: "Option 2" }
                     MenuItem { text: "Option 3" }
                 }
             }
         }
     }
 }

Dialog has accepted and rejected signals that are emitted when the dialog is accepted (closed by pressing "Accept" or swiping forwards) or rejected (closed by pressing "Cancel" or swiping backwards). To respond to the closing of the dialog and give some feedback about whether the selected option was confirmed by the user, modify the first page to connect to the accepted() signal to show the result:

 Component {
     id: firstPage
     Page {
         PageHeader { id: header }

         Button {
             text: "Show dialog"
             anchors.centerIn: parent
             onClicked: {
                 var dialog = pageStack.push(dialogComponent)
                 dialog.accepted.connect(function() {
                     header.title = "Last selection: " + dialog.selectedOption
                 })
             }
         }
     }
 }

Notice that PageStack::push() returns an instance of the page that was created when pushed onto the stack; in this case, it is an instance of the dialogComponent. This allows the connection to the accepted() signal.

Since the page header's title is only updated when the dialog is accepted, if the user changes the selected option but presses "Cancel" rather than "Accept", the title will not be updated. This allows, for example, an app to only save a set of user-entered data to disk if a dialog was accepted, and not rejected.

Managing UI layout and orientation

Using Theme for consistent and scalable layouts

The Theme object is configured to scale its sizes and margins according to the platform screen size and resolution, so it should always be used where possible in preference to hard-coded sizes and margins. Using Theme also ensures the application UI uses sizes and margins consistent with other Sailfish applications. For example:

See the Theme documentation for more details.

Dynamic layout updates for orientation changes

Sailfish applications can use a number of orientation-related properties to respond to changes in the device orientation to update the UI accordingly when the device is physically rotated. This allows the application UI to take full advantage of the available screen space in different orientations.

To do this:

For example, consider an app that shows an image and its basic metadata (filename, width and height). In portrait orientation, the UI show a horizontally-centered Image at the top, and a column of DetailItem objects below with the metadata:

 import QtQuick 2.2
 import Sailfish.Silica 1.0

 ApplicationWindow {
     initialPage: Component {
         Page {
             id: page

             PageHeader {
                 id: header
                 title: "Image details"
             }

             Image {
                 id: image
                 anchors {
                     top: header.bottom
                     horizontalCenter: parent.horizontalCenter
                 }
                 sourceSize.width: page.width - (Theme.horizontalPageMargin * 2)
                 fillMode: Image.PreserveAspectFit
                 source: "/home/nemo/Pictures/img_0001.jpg"
             }

             Column {
                 anchors {
                     top: image.bottom
                     topMargin: Theme.paddingLarge

                     left: parent.left
                     leftMargin: Theme.horizontalPageMargin

                     right: parent.right
                     rightMargin: Theme.horizontalPageMargin
                 }

                 DetailItem { label: "Name of file"; value: image.source }
                 DetailItem { label: "Width"; value: image.width }
                 DetailItem { label: "Height"; value: image.height }
             }
         }
     }
 }

To allow the page to respond to orientation changes, set the page's allowedOrientations to the orientations that should be supported by your UI. The value may be Orientation.Portrait, Orientation.PortraitInverted, Orientation.Landscape, Orientation.LandscapeInverted, or a mask of any combination of these. In addition there is Orientation.All to support all of the possible orientations, which is what we shall use here:

 Page {
     allowedOrientations: Orientation.All

     // rest of page code...
 }

Now when the device orientation changes, the application will update this page's orientation value to the current device orientation.

However, when the app is run with this new change, we can see that the current UI layout is not ideal in landscape orientations: if the image is large, the details below may go off-screen and will no longer be visible. Instead of adding a SilicaFlickable to allow the user to scroll down to view the image metadata, the layout could instead dynamically change when in landscape orientations to show the image on the left and the description on the right instead. To determine when the page is in portrait or landscape orientation, check the orientation value, or as a convenience, check isPortrait or isLandscape.

To make the required layout changes to the example:

  • Clear the image's horizontalCenter anchor in landscape orientations so that the image automatically anchors itself to the left instead
  • Resize the image: in landscape orientation, it should fill half of the page width, and the right-edge margin is not necessary as it no longer sizes to the right edge of the screen

Here is the relevant example code with the changes applied:

 Image {
     id: image

     anchors {
         top: header.bottom
         horizontalCenter: page.isPortrait ? parent.horizontalCenter : undefined
     }

     sourceSize.width: {
         var maxImageWidth = page.width
         var leftMargin = Theme.horizontalPageMargin
         var rightMargin = page.isPortrait ? Theme.horizontalPageMargin : 0
         return maxImageWidth - leftMargin - rightMargin
     }

     // rest of image code...
 }

Then, adjust the anchors of the column of metadata depending on the orientation:

 Column {
     anchors {
         // in landscape, anchor the top to the bottom of the header instead of the image
         top: page.isPortrait ? image.bottom : header.bottom
         topMargin: page.isPortrait ? Theme.paddingLarge : 0

         // in landscape, anchor the column's left side to the right of the image with a
         // margin of Theme.paddingLarge between them
         left: page.isPortrait ? parent.left : image.right
         leftMargin: page.isPortrait ? Theme.horizontalPageMargin : Theme.paddingLarge

         right: parent.right
         rightMargin: Theme.horizontalPageMargin
     }

     // rest of column code...
 }

It's also possible to customize the animation that is run when a page transitions between orientations, by changing orientationTransitions. This could be used, for example, to animate the positions of the items on the page as they move within the layout.

There are also additional properties in ApplicationWindow for handling device orientation changes; see its reference documentation for more details.

Application lifecycle

Application states

Sailfish OS is a true multiprocessing system. Applications can be moved into the background by the user. To support these different modes, applications have two states:

  • Active: the app consumes all available screen real estate
  • Background: the app is represented by its cover, displayed in the home screen

An application can determine its state using the Qt.application.state property. This property is Qt.ApplicationActive when the application is running in the foreground (Active) and Qt.ApplicationInactive when the application is running in the background.

An application running in the background must minimize resource usage. All animations must be paused and, if possible, the app should release unused resources:

 Label {
     // spinning label
     text: "Hello world!"
     anchors.centerIn: parent
     RotationAnimation on rotation {
         from: 0
         to: 360
         duration: 2000
         loops: Animation.Infinite
         running: Qt.application.state == Qt.ApplicationActive // but only when active
     }
 }

Application covers

An application's cover is a representation of the application that is displayed in the home screen when the app is backgrounded. It is created using the Cover type and is specified with the ApplicationWindow cover property. Covers allow the user to view significant information from your app or interact with a limited subset of its features while it is backgrounded.

For example, here is an app that shows a ColorPicker. If a color is clicked, then when the app is backgrounded, the selected color is shown in the cover:

 import QtQuick 2.2
 import Sailfish.Silica 1.0

 ApplicationWindow {
     id: appWindow

     property color selectedColor

     initialPage: Component {
         Page {
             ColorPicker {
                 onColorClicked: appWindow.selectedColor = color
             }
         }
     }

     cover: Component {
         Cover {
             Rectangle {
                 anchors.fill: parent
                 color: appWindow.selectedColor
             }
         }
     }
 }

Note that as the cover is active while the app is backgrounded, any animations or resource-intensive processes in the cover should only be run while the cover status is Cover.Active.

(Note that while the above example sets the ApplicationWindow::cover property as a Component, it is preferable to place the cover in a separate QML file and set the cover property using the file URL to avoid the cost of compiling the QML component on application start-up.)

Cover actions

Cover actions provide instant access to common actions from the application cover. For instance, a music player app might provide controls for the user to pause or go to the next song from the application cover.

Cover actions are added by creating a CoverActionList with CoverAction children within a Cover. Each CoverAction sets an iconSource specifying the action button's icon, and an onTriggered signal handler that executes the action. For the color picker in the previous example, this could be used to allow the color to be reset to white from the app cover:

 ApplicationWindow {
     cover: Component {
         Cover {
             id: appCover

             CoverActionList {
                 CoverAction {
                     iconSource: "icon.png"
                     onTriggered: appWindow.selectedColor = "white"
                 }
             }

             // rest of cover code...
         }
     }
 }

Application shutdown

The user may close the application at any time. Cleanup may be performed in the Component.onDestruction handler of ApplicationWindow.

We use cookies to improve your user experience and to help us to develop our services. By continuing to browse the site, you approve of our use of cookies.