Desktop System-UI Example
Minimal Desktop System-UI in pure QML.
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 an application by clicking the icon on the top left again
- Close application windows 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 them
- 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 angle as a window 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
- Wayland client windows from processes started outside of appman will be shown
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.SystemUI 2.0 Rectangle { width: 1024 height: 640 color: "linen" Readme {} Text { anchors.bottom: parent.bottom text: (ApplicationManager.singleProcess ? "Single" : "Multi") + "-Process Mode" } ...
The QtApplicationManager.SystemUI 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 { 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. 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 ApplicationObject.start() that is accessible through the application
role in the ApplicationManager model. Both applications will be started with the (optional) document URL "documentUrl". If the application is already running, ApplicationObject.stop() will be called instead.
Application Windows in the System-UI
// System-UI chrome for applications Repeater { model: ListModel { id: topLevelWindowsModel } delegate: Rectangle { width: 400; height: 320 z: model.index color: "tan" 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 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 get created by the WindowManager. The code that populates the window role of this ListModel will be shown below. For now let's focus on what this Repeater's delegate consists of:
- A fixed size window container Rectangle with a "tan" color. 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 the top horizontal center. The name is provided by the ApplicationObject related to that window and is 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 will also cause the corresponding application to quit.
- The centerpiece: a WindowItem to render the
WindowObject
in the SystemUI. That's 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 client (application) 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. For that please check 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 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: "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 chose type: "pop-up"
to indicate that the window is supposed to be shown as a pop-up. In the WindowManager::onWindowAdded() 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 SystemUI 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. In general it is essential to understand, that there has to be an agreement between the SystemUI 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 more convenient and should be preferred.
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 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 onWindowAdded: { if (window.windowProperty("type") === "pop-up") { popUpContainer.window = window; } else { topLevelWindowsModel.append({"window": window}); window.setWindowProperty("propA", 42); } } onWindowPropertyChanged: 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. The onWindowAdded handler is invoked when a new application window is available (becomes visible).
Only the "pop-up" ApplicationManagerWindow of App1 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. Those windows are added to the topLevelWindowsModel
. This model is used in the SystemUI 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).
By the way, any wayland client window from a process started outside of the application manager will also be displayed since "flags/noSecurity
: yes"
is set in the configuration file, for instance (KDE's Calculator):
$ QT_WAYLAND_DISABLE_WINDOWDECORATION=1 QT_WAYLAND_SHELL_INTEGRATION=xdg-shell-v5 kcalc -platform wayland
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 { 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(this, "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:
- applicationmanager/minidesk/am-config.yaml
- applicationmanager/minidesk/apps/tld.minidesk.app1/app1.qml
- applicationmanager/minidesk/apps/tld.minidesk.app1/info.yaml
- applicationmanager/minidesk/apps/tld.minidesk.app2/app2.qml
- applicationmanager/minidesk/apps/tld.minidesk.app2/info.yaml
- applicationmanager/minidesk/minidesk.pro
- applicationmanager/minidesk/minidesk.qmlproject
- applicationmanager/minidesk/system-ui/Readme.qml
- applicationmanager/minidesk/system-ui/main.qml
Images:
© 2019 Luxoft Sweden AB. 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.