Development for SailfishOS: menu

  • Tutorial
Hello! Another continuation of a series of articles on development for the mobile platform SaifishOS. This time I want to talk about how to implement a different kind of menu in an application. This topic deserves a separate article, since the menus in SailfishOS themselves look quite interesting and do not look like menus on other mobile platforms.

Actually, the main view of the menu in Sailfish is the menu, which is shown at the top of the screen. Since the platform itself places great emphasis on gestures and on quick control with their help, a gesture is used to show this menu - swipe with your finger from top to bottom. It looks like this: if a menu is available on the screen in the application, a light bar appears at the top of the screen:

Full size screenshot


Even in a full-size screenshot, this strip is poorly distinguishable, but on a real device it is noticeable. For comparison, below is a screenshot of the same screen, but without the menu and, accordingly, with the missing bar.

Full size screenshot


If you pull down on the screen, the menu will appear:



An interesting feature of interacting with the menu in SailfishOS is that you can select any menu item in two ways. You can simply swipe down until the menu appears completely (as in the screenshot above). Then it will remain on the screen and you can simply poke into the desired menu item. And you can not pull it down to the end and then, as the menu appears, its items will be highlighted, as in the screenshot below:



If you remove your finger from the screen at that moment, the highlighted menu item will be selected.

Pulldownmenu


Implementing such a menu in Sailfish is quite simple. For this, the PullDownMenu component is present in Sailfish Silica . However, this component has a number of features that you need to know before you start using it.

Firstly, since the menu itself is called using a swipe gesture, PullDownMenu can only be used inside containers that allow this gesture. In Sailfish Silica, these containers are:

  • SilicaFlickable is the most basic container that allows you to scroll the screen if the content does not fit completely into the frame of the screen. This component inherits the standard Flickable QML component and should be used when you need a menu on the application page, but none of the following containers are suitable.

  • SilicaListView - a component that displays a list of elements that inherits the standard QML component of ListView .

  • SilicaGridView - like the previous component, is used to display a list of elements, but not in the form of a vertical list, but in the form of a grid. Inherits the standard QML component of the GridView .

  • SilicaWebView - component for displaying web content, inherits the standard QML component of WebView .

Secondly, the contents of PullDownMenu should be components like MenuItem or MenuLabel (actually not, see the text below). The first is an interactive menu item and has a number of the following properties:

  • text - directly the text of the menu item.
  • color - the color of the text defined in the previous property.
  • horizontalAlignment - horizontal alignment of menu item text. It can be one of the following values: Text.AlignLeft , Text.AlignRight , Text.AlignHCenter (used by default) or Text.AlignJustify .
  • down - the value of this property is true when the menu item was selected.

In addition to these properties, MenuItem also contains a fairly large number of font- type properties . to set the font for the menu item (you can read about them in the documentation ) and the onClicked () signal handler , which defines the actions that must be performed when this menu item is selected.

MenuLabel is a static menu item that simply displays some text and cannot be pressed. Such items are used, for example, as a menu title or as separators between interactive menu items. Naturally, MenuLabel contains fewer properties than PullDownMenu :

  • text - the text of the menu item.
  • color - the color of the text defined in the previous property.
  • verticalOffset - vertical indentation.

UPD: Of course, the contents of PullDownMenu , like the other menus discussed in this article, can be any component, not just MenuItem or MenuLabel . However, in the case of using other components, the developer will have to implement the entire interaction logic himself. And such a menu will no longer look native and, therefore, it is worth using such opportunities only in rare cases. In standard situations, MenuItem and MenuLabel are enough , so the use of other components inside the menu will not be considered in this article.

A minimal example of a page with PullDownMenu would look like this:

Page {
    id: page
    SilicaFlickable {
        anchors.fill: parent
        contentHeight: column.height
        PullDownMenu {
            MenuItem {
                text: qsTr("Пункт меню 3")
                onClicked: console.log("Нажат третий пункт меню")
            }
            MenuLabel {
                text: qsTr("Подраздел")
            }
            MenuItem {
                text: qsTr("Пункт меню 2")
                onClicked: console.log("Нажат второй пункт меню")
            }
            MenuItem {
                text: qsTr("Пункт меню 1")
                onClicked: console.log("Нажат первый пункт меню")
            }
            MenuLabel {
                text: qsTr("Меню приложения")
            }
        }
        Column {
            id: column
            width: page.width
            spacing: Theme.paddingLarge
            PageHeader {
                title: qsTr("Моё приложение")
            }
            Label {
                text: "Привет, Хабр!"
                width: page.width
                horizontalAlignment: Text.AlignHCenter
                font.pixelSize: Theme.fontSizeExtraLarge
            }
        }
    }
}

The menu itself will look like this:



Here it is worth noting that the menu items inside PullDownMenu are determined not in the order they appear on the screen with a swipe gesture, but in the order of their presence in the menu itself, from top to bottom. This feature may seem unusual at first. In addition, it is worth noting that the example above is for informational purposes only, but in real applications, do not use too many menu items and especially MenuLabel components.

PullDownMenu Properties


PullDownMenu contains a number of properties that allow you to customize its appearance and behavior. For example, component padding can be set using the following properties:

  • spacing - the distance between the bottom edge of the menu and the top border of the page content. By default, this value is zero.
  • topMargin - the distance between the top edge of the menu (the same as the top screen tap) and the top edge of the topmost menu item. By default, this value is Theme.itemSizeSmall .
  • bottomMargin - the distance between the lower edge of the lowest menu item and the lower edge of the menu itself.

An interesting feature of the last property ( bottomMargin ) is that its default value changes depending on the contents of the menu. If the lowest menu item is MenuLabel , then the property value is 0. Otherwise, the property value is equal to the height of the MenuLabel component . You can see the difference in the examples below:


As can be seen in the screenshots, this feature allows you to always have the same menu height, regardless of the presence or absence of a title in it, which in turn allows you to save the user experience when using different types of menus.

In addition to dimensions, you can also change other appearance parameters of PullDownMenu using the following properties:

  • background - allows you to describe the component that will be used as the background of the menu.
  • backgroundColor - background color of the menu. It is noteworthy that the system itself applies a gradient to the specified color.
  • highlightColor - the highlight color of the selected menu item, as well as the menu indicator displayed on top of the screen when the menu itself is closed.
  • menuIndicator - allows you to describe the component that will be used as an indicator of the menu displayed at the top of the screen when the menu itself is closed.

You can change the colors in the menu from the example above to the following:

backgroundColor: "red"
highlightColor: "green"

Then you get a menu of the following form:



It looks, of course, scary, but it demonstrates how these properties work. And with the help of background and menuIndicator you can set, for example, pictures as a background and a menu indicator.

The above properties can be useful if you want to make the menu, like the entire application, executed, for example, in corporate colors. In addition to these properties, the PullDownMenu component also contains properties that allow you to get the status of a menu or easily change its behavior:

  • active - a property of a logical type whose value is true if the menu is fully or partially present on the screen.
  • busy is also a property of a boolean type. If its value is set to true , then the menu indicator at the top of the screen will begin to "pulsate." In this case, the menu itself will still be available. This property is convenient to use if you want to show the user that some process is still in progress.
  • flickable - using this property you can specify the flickable component with which the menu will be activated. Those. instead of putting PullDownMenu inside any container, you can simply specify this container with the value for this property.
  • quickSelect - a property of a logical type that allows you to enable the quick selection function for the menu. This function only works on a menu with just one item. If this function is activated, then at any scrolling of the menu (including to the end) its only item will be automatically selected.

Finally, PullDownMenu contains two methods. After selecting a menu item, this menu is closed and the closing animation is played. This animation can be canceled using the cancelBounceBack () method , for example, if you call it in the onClicked () handler of the desired menu item. This can be useful in rare cases when closing the menu may interfere with the execution of the action assigned to the menu item.

The close () method allows you to manually close the menu. At the same time, you can specify true as an argument to this method, and then the menu will close instantly, without animation. For example, in the code below, when you select the menu item "Menu item 2", the menu closes without animation:

PullDownMenu {
    id: menu
    MenuItem {
        text: qsTr("Пункт меню 2")
        onClicked: menu.close(true)
    }
    MenuItem {
        text: qsTr("Пункт меню 1")
        onClicked: console.log("Нажат первый пункт меню")
    }
}

Pushupenu


Another type of menu - PushUpMenu - is the same PullDownMenu , with the only difference being that this menu appears not from the top of the screen, but from the bottom and, accordingly, the swipe is activated with a gesture from the bottom up. PushUpMenu looks the same as PullDownMenu and all its properties and methods are similar to those of PullDownMenu , so they do not need a separate mention in this article.

However, it is worth noting that, since PushUpMenu (as well as PullDownMenu) should be inside the container that allows swipe gestures, then the activation of such a menu occurs only after all the contents of the container have been scrolled to the end. In other words, if you put PushUpMenu on the list page, then this menu will be activated only after the user has scrolled the entire list to the end.

In the code, such an example would look like this:

Page {
    id: page
    SilicaListView {
        PushUpMenu {
            MenuItem {
                text: qsTr("Пункт меню 3")
                onClicked: console.log("Нажат третий пункт меню")
            }
            MenuItem {
                text: qsTr("Пункт меню 2")
                onClicked: console.log("Нажат второй пункт меню")
            }
            MenuItem {
                text: qsTr("Пункт меню 1")
                onClicked: console.log("Нажат первый пункт меню")
            }
            MenuLabel {
                text: qsTr("Меню приложения")
            }
        }
        id: listView
        model: 20
        anchors.fill: parent
        header: PageHeader {
            title: "Простой список"
        }
        delegate: BackgroundItem {
            id: delegate
            Label {
                x: Theme.paddingLarge
                text: "Элемент #" + index
                anchors.verticalCenter: parent.verticalCenter
                color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
            }
        }
        VerticalScrollDecorator {}
    }
}

As a result, you can get to the menu only if you scroll the entire list to the end:

ContextMenu


The last type of menu in SailfishOS to be discussed in this article is the context menu. It is implemented using the ContextMenu component and is a pop-up menu that can be associated with any user interface element. The contents of such a menu are described in the same way as for PushUpMenu and PullDownMenu , using the MenuItem and MenuLabel components .

Most often, such menus are used to implement a context menu of list items. For this, the ListItem component , which is used to describe list delegates, has a special menu property. So you can add a context menu to the list items from the last example. To do this, you will have to slightly change the delegate so that it is implemented through ListItem , and add the menu itself to it:

delegate: ListItem {
    id: delegate
    Label {
        id: label
        x: Theme.paddingLarge
        text: "Элемент #" + index
        anchors.verticalCenter: parent.verticalCenter
        color: delegate.highlighted ? Theme.highlightColor : Theme.primaryColor
    }
    menu: ContextMenu {
        MenuLabel {
            text: "Контекстное меню"
        }
        MenuItem {
            text: "Выделить жирным"
            onClicked: label.font.bold = !label.font.bold
        }
        MenuItem {
            text: "Выделить курсивом"
            onClicked: label.font.italic = !label.font.italic
        }
    }
}

Now, with a long tap on the list item, a context menu will appear under it:



When you select items from this menu, the text style of the list item will change:

The context menu will close if you select one of the menu items or simply tap outside this menu. However, the ListItem component also contains the hideMenu () and showMenu () methods , which allow you to manually hide or show the context menu. The last method can be passed as a parameter a list of properties of the ContextMenu component that will be applied to the menu (the properties of the ContextMenu component will be discussed a bit later). In addition, the default behavior for the context menu item in the list can be changed by setting a property showMenuOnPressAndHold component ListItem value false. In this case, the context menu will not appear with a long tap on an element. Finally, know the context menu is shown or not, you can use the properties menuOpen component of the ListItem .

The context menu can also be displayed outside the list by associating them with regular interface elements. For this, the ContextMenu component has a show () method , to which an element is passed as an argument, relative to which the menu should be displayed. In this case, the menu will be attached to the lower border of this element, and will be displayed upward when displayed. A minimal example with such a menu might be:

Page {
    id: page
    SilicaFlickable {
        id: flickab
        anchors.fill: parent
        contentHeight: column.height
        Column {
            id: column
            width: page.width
            spacing: Theme.paddingLarge
            PageHeader {
                title: qsTr("Моё приложение")
            }
            Button {
                id: button
                text: "Нажми меня"
                width: page.width
                onClicked: contextMenu.show(label)
            }
            Label {
                id: label
                height: page.height / 2
                text: "Просто текст"
                verticalAlignment: Text.AlignBottom
            }
        }
        ContextMenu {
            id: contextMenu
                MenuLabel {
                    text: "Контекстное меню"
                }
                MenuItem {
                    text: qsTr("Пункт меню 1")
                    onClicked: console.log("Нажат первый пункт меню")
                }
        }
    }
}

Such a page looks like this:



And when you click on the button, the menu leaves the bottom edge of the inscription:



Here it should be noted that the height of the inscription was not accidentally made so large (half the page). The fact is that the context menu is shown exactly inside the component specified in the show () method , and if the menu turns out to be larger than this element, then it will simply be cut off. To demonstrate this feature, in the example above, you can make the height at the inscription the usual height, and place a white rectangle between the button and the inscription:

Button {
    id: button
    text: "Нажми меня"
    width: page.width
    onClicked: contextMenu.show(label)
}
Rectangle {
    color: "white"
    height: page.height / 2
    width: parent.width
}
Label {
    id: label
    text: "Просто текст"
}

Then the page will look like this:



And when you open the menu, it will be cut off:



UPD: As several people have noticed, the examples above were not chosen very well. In real projects, in cases where the context menu is larger than the element to which it refers, it is worth changing the sizes of the elements depending on whether the menu is open or not. The code example above, in this case, should be changed by making the height of the Label component dependent on the menu:

Label {
    id: label
    text: "Просто текст"
    height: contentHeight + (contextMenu.visible ? contextMenu.height : 0)
}

Then the page itself will look like this:

And when you click on the menu button, it will visually go to the bottom of the inscription:


You can close the context menu using the hide () method , and whether it is open or not using the active property . In addition, the ContextMenu component also contains the closeOnActivation property , with which you can set whether the menu should be closed when selecting any of its items. And the hasContent property will help to find out if the menu has any content. This property is used by the system itself: if the hasContent value is false , then the menu will not be shown, even when the show () method is called.

Finally, ContextMenu contains an onActivated () signal handler , which is called every time a menu item has been selected. The handler argument is the index of the selected menu item.

That's all. In this article I described 3 main standard types of menus in SailfishOS, told how they can be implemented, as well as what features each type has.

Posted by Denis Laure

UPD: The text of the article has been updated, all comments received have been taken into account. Thanks to everyone who sent comments and comments.

Also popular now: