Desktop System-UI Example

Minimal Desktop System-UI in pure QML.

Screenshot

Introduction

The Desktop System-UI Example showcases the application-manager API in a simple fashion. The focus is on the concepts, less on elegance or completeness, for instance no error checking is done. Some features will only print debug messages without further functionality. A classic desktop with server-side window decorations is emulated.

The following features are supported:

  • Start applications by clicking an icon on the top left
  • Stop applications by clicking on the top/left window decoration rectangle
  • Raise applications by clicking on the decoration
  • Drag windows by pressing on the window decoration and moving
  • System-UI sends 'propA' change when an app is started
  • System-UI and App2 react on window property changes with a debug message
  • App1 animation can be stopped and restarted by a click
  • App1 sends 'rotation' property to System-UI on stop
  • App1 shows a pop-up on the System-UI while it is paused
  • App2 will make use of an IPC extension when it is started
  • App2 logs the document URL it has been started with
  • App2 triggers a notification in the System-UI when the bulb icon is clicked
  • A separate ("wayland") process started outside of appman will be shown as App1

Note: The example can be run in single- and multi-process mode. In the following, multi-process is assumed and corresponding terminology is used. The terms client and application, respectively server and System-UI are used interchangeably. The System-UI comprises compositing and generic inter-process communication (IPC).

Invocation

The example can be started from within the minidesk folder with:

path/to/bin/appman -c am-config.yaml -r

Note: The application-manager attempts to register a freedesktop.org compliant notification server. DBus errors might occur if it conflicts with the server running on the host desktop environment. In this case, a private session bus needs to be started by adding the --start-session-dbus option:

path/to/bin/appman -c am-config.yaml -r --start-session-dbus

Walkthrough

System-UI Window

import QtQuick 2.4
import QtApplicationManager 1.0

Rectangle {
    property int zorder: 1

    width: 1024
    height: 640
    color: "linen"

    Readme {}

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

The QtApplicationManager module has to be imported to be able to access the application-manager APIs. The System-UI window has a fixed size and linen background color. Instead of a Rectangle, the root element could as well be a Window. On top of the background the Readme element is shown that displays a usage how-to. At the bottom left there is a textual indication for whether the application-manager runs in single- or multi-process mode.

Launcher

    // Application launcher panel
    Column {
        id: launcher
        Repeater {
            id: menuItems
            model: ApplicationManager

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

                MouseArea {
                    anchors.fill: parent
                    onClicked: ApplicationManager.startApplication(applicationId, "documentUrl");
                }
            }
        }
    }

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

Clicking on an application icon will launch the corresponding application through a call to ApplicationManager.startApplication(). The first argument, applicationId, is provided by the ApplicationManager model (e.g. "tld.minidesk.app1" for the first application). Both applications will be started with the (optional) document URL "documentUrl". Subsequent clicks on an already launched icon will still call startApplication(), but will be silently ignored. The content of the appContainter will be set below in the onWindowReady handler. The content is provided by the two applications in an ApplicationManagerWindow root element.

Application Windows in the System-UI

    // System-UI chrome for applications
    Repeater {
        id: windows
        model: menuItems.model

        Rectangle {
            id: winChrome

            property alias appContainer: appContainer

            width: 400; height: 320
            x: 300 + model.index * 50; y: 10 + model.index * 30
            color: "tan"
            visible: false

            Text {
                anchors.horizontalCenter: parent.horizontalCenter
                text: "Decoration: " + name
            }

            MouseArea {
                anchors.fill: parent
                drag.target: parent
                onPressed: parent.z = zorder++;   // for demo purposes only
            }

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

                MouseArea {
                    anchors.fill: parent
                    onClicked: ApplicationManager.stopApplication(applicationId, false);
                }
            }

            Item {
                id: appContainer
                anchors.fill: parent
                anchors.margins: 3
                anchors.topMargin: 25
            }
        }
    }

This second Repeater provides the window chrome for the actual applications in its delegate. The model is the same as for the first Repeater, essentially the ApplicationManager element. The chrome consists of:

  • A fixed size window Rectangle with a "tan" color. The default location depends on the model.index, hence each application has a different location.
  • The name of the application, prefixed with "Decoration" on the top horizontal center. The name is also provided by the ApplicationManager model and read from the application's info.yaml file.
  • A MouseArea for dragging the window and setting the z-order. The MouseArea fills the entire window, though the application container is placed on top of it and hence it will not handle dragging. Simply increasing the z-order on each click is only done to keep the code to a minimum.
  • A small chocolate-colored Rectangle on the top left corner for closing the window chrome and stopping the actual application (see stopApplication()).
  • A container Item where the actual application will be placed in. The appContainer is exposed, so it can later be referenced to place the actual application window in it.

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 and notification
    Item {
        id: popUpContainer
        z: 30000
        width: 200; height: 60
        anchors.centerIn: parent
    }

    Text {
        z: 30001
        font.pixelSize: 46
        anchors.centerIn: parent
        text: NotificationManager.count > 0 ? NotificationManager.get(0).summary : ""
    }

Client Application Rendering

The popUpContainer Item provides a System-UI recipient for the pop-up rendered by App1. App1 instantiates another ApplicationManagerWindow for the pop-up within its ApplicationManagerWindow root element, as shown here:

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

        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 choose type: "pop-up" to indicate that the window is supposed to be shown as a pop-up. In the onWindowReady handler below the System-UI checks for this property and handles the window appropriately as a pop-up.

Notification API Usage

An alternative to the above 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 the bulb icon of App2 is clicked:

                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 will increase 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. Presenting the first notification only is a simplification to keep the code short.

WindowManager Signal Handler

    // Handler for WindowManager signals
    Connections {
        target: WindowManager
        onWindowReady:  {
            var appIndex = ApplicationManager.indexOfApplication(WindowManager.get(index).applicationId);
            var type = WindowManager.windowProperty(window, "type");
            console.log("SystemUI: onWindowReady [" + window + "] - index: "
                         + index + ", appIndex: " + appIndex + ", type: " + type);

            if (type !== "pop-up") {
                if (appIndex === -1) {
                    console.log("Allowing a single app started outside of appman instead of App1 ...");
                    appIndex = 0;
                }
                var chrome = windows.itemAt(appIndex);
                window.parent = chrome.appContainer;
                window.anchors.fill = chrome.appContainer;
                chrome.visible = true;
                WindowManager.setWindowProperty(window, "propA", 42)
            } else {
                window.parent = popUpContainer;
                window.anchors.fill = popUpContainer;
            }
        }

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

        onWindowClosing: {
            console.log("SystemUI: onWindowClosing [" + window + "] - index: " + index);
            if (WindowManager.windowProperty(window, "type") !== "pop-up") {
                var appIndex = ApplicationManager.indexOfApplication(WindowManager.get(index).applicationId);
                if (appIndex === -1)
                    appIndex = 0;
                windows.itemAt(appIndex).visible = false;
            }
        }

        onWindowLost: {
            console.log("SystemUI: onWindowLost [" + window + "] - index: " + index);
            WindowManager.releaseWindow(window);
        }
    }

This is the vital part of the System-UI, where the window surfaces of the applications are mapped to items in the System-UI. The onWindowReady handler is invoked when a new application window is shown (visibility set to true). The index parameter references into the WindowManager model, which holds the window surfaces. The first line translates this index to the application index of the ApplicationManager model and assigns it to appIndex.

Only the "pop-up" ApplicationManagerWindow of App1 has the user-defined type property set. All other windows don't have the type property. In the latter case, the application's window passed to onWindowReady is re-parented to the System-UI's winChrome. Also the size of the window is set to fill the entire appContainer.

There is a special treatment for an invalid appIndex for demonstration purposes: it is assumed that the window surface is from an external application that follows the Wayland protocol. For example a Qt application could be started on the command line as follows:

QT_WAYLAND_DISABLE_WINDOWDECORATION=1 ./qtapp -platform wayland

This application will then be shown inside the App1 container. QT_WAYLAND_DISABLE_WINDOWDECORATION is set to disable client side window decorations, since they are provided by the System-UI. Note, that an application started in this way, can just as well be closed only from the command line.

Pop-ups (windows with type "pop-up") are re-parented to their respective popUpContainer.

The onWindowClosing handler determines if the window is a top-level application window. If so, the applications's winChrome is set to invisible and the corresponding application icon is set to fully opaque to indicate that the application is not running. The onWindowLost handler calls WindowManager.releaseWindow() to free all resources associated with the window.

IPC Extension

The following snippet demonstrates how the ApplicationIPCInterface can be used to define an IPC extension. The IPC interface has to be defined in the System-UI, for instance:

    // IPC extension
    ApplicationIPCInterface {
        id: extension

        property double pi
        signal computed(string what)
        readonly property var _decltype_circumference: { "double": [ "double", "string" ] }
        function circumference(radius, thing) {
            console.log("SystemUI: circumference(" + radius + ", \"" + thing + "\") has been called");
            pi = 3.14;
            var circ = 2 * pi * radius;
            computed("circumference for " + thing);
            return circ;
        }

        Component.onCompleted: ApplicationIPCManager.registerInterface(extension, "tld.minidesk.interface", {});
    }

Here, a property pi is defined, as well as a signal computed and a function circumference. After registering this interface with ApplicationIPCManager.registerInterface(), it can be used from the application processes.

On the application side, the ApplicationInterfaceExtension type has to be used. Here is how App2 makes use of this interface extension:

    ApplicationInterfaceExtension {
        id: extension
        name: "tld.minidesk.interface"

        onReadyChanged: console.log("App2: circumference function returned: "
                              + object.circumference(2.0, "plate") + ", it used pi = " + object.pi);
    }

    Connections {
        target: extension.object
        onComputed: console.log("App2: " + what + " has been computed");
        onPiChanged: console.log("App2: pi changed: " + target.pi);
    }

The interface is used here immediately when it becomes ready. It can, of course, be accessed from elsewhere, too. The ApplicationInterfaceExtension.name has to match the name it was registered with in ApplicationIPCManager.registerInterface().

Application Termination

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

    Connections {
        target: ApplicationInterface
        onOpenDocument: console.log("App2: onOpenDocument - " + documentUrl);
        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.

Files:

Images:

© 2018 Pelagicore AG. 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.