Desktop System UI Example

Illustrates a minimal Desktop System UI in pure QML.

Screenshot

Introduction

This example showcases the application manager API in a simple way, as a classic desktop with server-side window decorations. The example focuses more on the concepts, but less on elegance or completeness. For example, there's no error checking done. Some features in this minimal Desktop System only print debug messages.

The following features are supported:

  • Start applications by clicking on an icon in the top left
  • Stop an application by clicking on the icon in the top left again
  • Close application windows by clicking on the top left window decoration rectangle
  • Bring applications forward by clicking on the decoration
  • Drag windows by pressing on the window decoration and moving them
  • The System UI sends a 'propA' change when an app starts
  • The System UI and App2 react to window property changes with a debug message
  • Stop or restart App1 animations with a click
  • App1 sends rotation angle as a window property to System UI on stop
  • App1 shows a pop up on the System UI when it is paused
  • App2 logs the document URL that started it
  • App2 triggers a notification in the System UI, when the bulb icon is clicked
  • Show Wayland client windows that originate from processes outside of appman

Note: This example can be run in single-process or multi-process mode. In the walkthrough below, we use multi-process and its corresponding terminology. The terms client and application; server and System UI are used interchangeably.

To start the example, navigate to the minidesk folder, and run the following command:

<path-to-appman-binary> -c am-config.yaml

The appman binary (executable file) is usually located in the Qt installation bin folder.

Walkthrough

System UI Window
import QtQuick 2.11
import QtQuick.Window 2.11
import QtApplicationManager.SystemUI 2.0

Window {
    title: "Minidesk - QtApplicationManager Example"
    width: 1024
    height: 640
    color: "whitesmoke"

    Readme {}

    Text {
        anchors.bottom: parent.bottom
        text: (ApplicationManager.singleProcess ? "Single" : "Multi") + "-Process Mode"
    }
    ...

The QtApplicationManager.SystemUI module has to be imported to access the application manager APIs. The System UI window has a fixed size and "whitesmoke" background color. Instead of a Window, the root element could also be a regular item, like a Rectangle. The application manager would wrap it in a window for you. On top of the background, we display a Readme element with information on the features available. At the bottom left there is a textual indication for whether the application manager runs in single-process or multi-process mode.

Launcher
    // Application launcher panel
    Column {
        Repeater {
            model: ApplicationManager

            Image {
                source: icon
                opacity: isRunning ? 0.3 : 1.0

                MouseArea {
                    anchors.fill: parent
                    onClicked: isRunning ? application.stop() : application.start("documentUrl");
                }
            }
        }
    }

A Repeater provides the application icons arranged in a Column on the top left corner of the System UI; the ApplicationManager element is the model. Among others, the ApplicationManager provides the icon role which is used as the Image source URL. The icon URL is defined in the application's info.yaml file. To indicate that an application has launched, the corresponding application icon's opacity is decreased by binding it to the isRunning role.

Clicking on an application icon launches the corresponding application through a call to ApplicationObject.start(). This function is accessible through the application role in the ApplicationManager model. Both applications start with the (optional) document URL, documentUrl. If the application is already running, ApplicationObject.stop() is called instead.

Application Windows in the System UI
    // System UI chrome for applications
    Repeater {
        model: ListModel { id: topLevelWindowsModel }

        delegate: Image {
            source: "chrome-bg.png"
            z: model.index

            Text {
                anchors.horizontalCenter: parent.horizontalCenter
                text: "Decoration: " + (model.window.application ? model.window.application.name("en")
                                                                 : 'External Application')
            }

            MouseArea {
                anchors.fill: parent
                drag.target: parent
                onPressed: topLevelWindowsModel.move(model.index, topLevelWindowsModel.count - 1, 1);
            }

            Rectangle {
                width: 25; height: 25
                color: "chocolate"

                MouseArea {
                    anchors.fill: parent
                    onClicked: model.window.close();
                }
            }

            WindowItem {
                anchors.fill: parent
                anchors.margins: 3
                anchors.topMargin: 25
                window: model.window

                Connections {
                    target: window
                    function onContentStateChanged() {
                        if (window.contentState === WindowObject.NoSurface)
                            topLevelWindowsModel.remove(model.index, 1);
                    }
                }
            }

            Component.onCompleted: {
                x = 300 + model.index * 50;
                y =  10 + model.index * 30;
            }
        }
    }

This second Repeater provides the window chrome for the application windows in its delegate. The model is a plain ListModel fed with window objects as they are created by the WindowManager. The code that populates the window role of this ListModel is shown below. For now let's focus on what this Repeater's delegate consists of:

  • A mostly transparent background Image. The location depends on the model.index, hence each application window has a different initial location.
  • The name of the application that created that window, prefixed with "Decoration" on top. This name is from the related ApplicationObject, defined in the application's info.yaml file.
  • A MouseArea for dragging and raising the window. The MouseArea fills the entire window. The WindowItem containing the application's window is placed on top of it and hence it will not handle dragging.
  • A small chocolate-colored Rectangle on the top left corner for closing the window (see WindowObject.close()). Since our sample applications only have one top-level window, closing it causes the corresponding application to quit.
  • The centerpiece: a WindowItem to render the WindowObject in the System UI; similar to the relationship between image files and QML's Image component.
  • And finally code to remove a row from the ListModel once its window has been destroyed from the application (client) side - either because it was closed, made invisible, or the application itself quit or crashed. Any of these cases results in the WindowObject losing its surface. A more sophisticated System UI could animate the disappearance of a window, as illustrated in the Animated Windows System UI Example.
Pop-ups

Two approaches are implemented to display pop-ups in the System UI:

  • Through a window rendered by the client application
  • Through the notification API provided by the application manager

This is the corresponding System UI code:

    // System UI for a pop-up
    WindowItem {
        id: popUpContainer
        z: 9998
        width: 200; height: 60
        anchors.centerIn: parent

        Connections {
            target: popUpContainer.window
            function onContentStateChanged() {
                if (popUpContainer.window.contentState === WindowObject.NoSurface) {
                    popUpContainer.window = null;
                }
            }
        }
    }

    // System UI for a notification
    Text {
        z: 9999
        font.pixelSize: 46
        anchors.centerIn: parent
        text: NotificationManager.count > 0 ? NotificationManager.get(0).summary : ""
    }
Client Application Rendering

App1 instantiates another ApplicationManagerWindow for the pop-up within its ApplicationManagerWindow root element, as shown here:

    ApplicationManagerWindow {
        id: popUp
        visible: false
        color: "orangered"

        Text {
            anchors.centerIn: parent
            text: "App1 paused!"
        }

        Component.onCompleted: setWindowProperty("type", "pop-up");
    }

The ApplicationManagerWindow.setWindowProperty() method is used to set a freely selectable shared property. Here we chose type: "pop-up" to indicate that the window is supposed to be shown as a pop-up.

In the WindowManager::onWindowAdded() signal handler below, the System UI checks this property and handles the window appropriately as a pop-up.

A pop-up window will be set as the content window of the popUpContainer WindowItem in the System UI code above. For demonstration purposes, the implementation supports only one pop-up at a time. This is sufficient, since only App1 will display a single pop-up when its animation is paused. It is essential to understand, that there has to be an agreement between the System UI and applications, in terms of how windows are mapped. In contrast to regular application windows that are freely draggable and have title bars and borders, the pop-up window is just centered and has no decoration at all. Note also how the WindowObject.contentStateChanged signal is handled in the popUpContainer: the window is released when it has no surface associated any more. This is important to free any resources that the window object is using. Note that this is done implicitly when the WindowManager model is used directly. This approach is recommended as it's more convenient.

Notification API Usage

An alternative to the window property approach is to use the application manager's Notification API on the application (client) side and the NotificationManager API on the System UI (server) side. The following code is invoked when you click on the bulb icon in App2:

                var notification = ApplicationInterface.createNotification();
                notification.summary = "Let there be light!"
                notification.show();

App2 creates a new Notification element, sets its summary property and calls show() on it. This call increases the NotificationManager.count on the System UI side, and subsequently the Text element's text property will be set to the summary string of the first notification. For brevity, the example only presents the first notification.

WindowManager Signal Handler
    // Handler for WindowManager signals
    Connections {
        target: WindowManager
        function onWindowAdded(window) {
            if (window.windowProperty("type") === "pop-up") {
                popUpContainer.window = window;
            } else {
                topLevelWindowsModel.append({"window": window});
                window.setWindowProperty("propA", 42);
            }
        }

        function onWindowPropertyChanged(window, name, value) {
            console.log("SystemUI: OnWindowPropertyChanged [" + window + "] - " + name + ": " + value);
        }
    }

This is the vital part of the System UI, where the window (surfaces) of the applications are mapped to WindowItems in the System UI. When a new application window is available (becomes visible), the onWindowAdded handler is invoked.

Only App1's "pop-up" ApplicationManagerWindow has the user-defined type property set. Such a window is placed in the popUpContainer WindowItem. All other windows don't have a type property; they are added to the topLevelWindowsModel. This model is used in the System UI chrome Repeater above. Consequently, the window object passed as an argument to onWindowAdded is set as the window property of the WindowItem (within the Repeater's delegate).

Incidentally, any Wayland client window from a process started outside of the application manager will also be displayed since in the configuration file, "flags/noSecurity: yes" is set, for instance in KDE's Calculator:

$ QT_WAYLAND_DISABLE_WINDOWDECORATION=1 WAYLAND_DISPLAY=qtam-wayland-0 kcalc -platform wayland
Application Termination

When an application is stopped from the System UI through ApplicationManager.stopApplication(), it is sent the ApplicationInterface.quit() signal. Then, the application can do some clean-up and it must subsequently confirm with ApplicationInterface.acknowledgeQuit(), like App2 does:

    Connections {
        target: ApplicationInterface
        function onOpenDocument(documentUrl, mimeType) {
            console.log("App2: onOpenDocument - " + documentUrl);
        }
        function onQuit() {
            target.acknowledgeQuit();
        }
    }

Note that App1 is not well-behaved: it does not acknowledge the quit signal and will hence simply be terminated by the application manager.

Example project @ code.qt.io

© 2021 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.