Development/Tutorials/Plasma4/ShellDesign

From KDE TechBase

Introduction

Creating a full featured Plasma shell involves pulling together a number of standard components from libplasma into a single interface. There are also several optional components that can be used to improve the overall user experience.

Whether you are creating a full screen interface such as Plasma Desktop or Plasma Netbook, or an application helper like the Plasma KPart or Plasma Media Center, this tutorial, which documents each of these components and how they fit together, can get you started in the right direction.

An Overview Of A Plasma Shell

Plasma is a model/view approach to user interfaces, much as the QGraphicsView framework it is based on. Nearly all of the user interface is kept on a canvas which can be thought of as an infinite Cartesian plane. This plane is called the "Corona", and is a QGraphicsScene subclass.

The Corona serves as a top-level manager for Containments, which are QGraphicsWidgets that manage collections of Applets. Containments are not nested, and Applet nesting is also discouraged due to being error-prone and nearly never actually needed. This creates a nice hierarchy of Corona->Containments->Applets within the scene. Note that "plain" QGraphicsWidgets may exist between Containments and Applets in the scene, and that they are transparent in terms of the Plasma hierarchy.

Within the scene, items are placed in the four quadrants of the plane according to their functions: full window / screen Containments are kept in quadrant III, panel Containments in quadrants II and IV (to accomodate their most natural direction of growth, e.g horizontal panel Containments grow horizontally more often that vertically so appear in quadrant II) and other items such as contents for popup windows in quadrant I (often referred to as "offscreen widgets").

To present this to the user, Views are created which show sections of the Corona. Views usually are set to display the geometry of a Containment or an offscreen widget. There is no correlation between the location of a View in the user interface or on the computer screen and where the contents it is viewing are on the screen. It is also possible to have more than one View showing the same area on the scene at once.

So the primary job of a Plasma shell is to provide a collection of Views on the Corona scene arranged in a way that makes sense for the application. The default layout of the items on the scene is also managed by the Plasma shell, but storing and loading once set up is handled by libplasma.

In addition to Views and the use of stock Containments and Applets, shells may want to provide customized Containments and/or Applets. These custom additions can themselves also be plugins (which can be shared with other Plasma shells or not) or built-in to the shell itself using the PluginLoader class.

There are also a host of optional components that a shell can provide that allow a more customized presentation, such as the AccessManager which allows the shell to provide a custom user interface for remote access requests, the DialogManager which forwards requests for dialogs such as configuration interfaces to be shown, etc.

Each of these components is covered in detail below.

Core Presentation

The Base Canvas: Corona

Corona is a subclass of QGraphicsScene, however it provides several additional facilities:

  • There is one and only one Corona instance per application
  • It manages Containments, including saving and restoring the whole layout into KConfig. In fact, only Containments and off-screen widgets are added as top-level items to the Corona.
  • It manages the association between Containments and screens: the mapped ones will be displayed by a Plasma::View
  • It manages the concept of available space for screen: reimplementations of screenGeometry() and availableScreenRegion() can take into account position and size of panels or the size of the window that contains the Corona if it isn't a full-screen application, for instance
  • QActions can be assigned to the Corona: they can be visualized by the Containments ToolBox, but unlike the Containments Actions, those are global and shared by all Containments
  • It has the concept of Immutability: part of the user interaction can be blocked, to prevent accidental data loss or for kiosk environments. When the Corona is immutable, all of the Containments and Applets are as well.
  • Corona provides access to the animation mapping facility (see the Animation Customization section below)

Most Plasma shells will want to create a subclass of Corona, if only to reimplement the loadDefaultLayout() method which is called when there is no existing layout configuration to load (e.g. on first launch of the application).

This is also where most Plasma shells implement things such as adding new Containments in response to shell-specific features or arranging Containments on the scene if necessary by reimplementing the layoutContainments() slot.

Most Plasma shell implementations find most of the default implementations of the virtual methods in Corona satisfactory, but there are valid use cases for each of the virtual methods. Examples of how these are implemented in different shells can be seen quite well in Plasma Desktop, Plasma Netbook, Plasma Mobile and the Plasma KPart.

Views

Plasma::View is a subclass of QGraphicsView. It has some peculiarities over its base class:

  • Its scene will be always the global unique scene: the Corona
  • It will always have an 1:1 association with a Containment
  • Its sceneRect() will always be synchronized with its Containment geometry()
  • It has a screen and desktop property: a view will be associated to a physical screen and a virtual desktop, and will be synchronized with the Containment. Not all reimplementations need to actually associate physical screens: for instance in an application dashboard there won't be this physical->logic association of screens
  • It has its own configuration, of all values that don't depend on scene items, but from actual window geometry/properties (such as panel position, or showing the view on all virtual desktops)

Custom Containments

Containment implementations can have several types:

  • DesktopContainment: default desktop containment, will have standard applet handles and the Corona will assign a ToolBox to it.
  • PanelContainment : default panel containment, won't have applet handles and the Corona will assign a ToolBox to it.
  • CustomContainment: won't have applet handles and the Corona won't assign a ToolBox by default, however it is possible to assign one to the containment implementation.
  • CustomPanelContainment: for custom panels: won't have applet handles and the Corona won't assign a ToolBox by default, however it is possible to assign one to the containment implementation.

A Containment implementation manages the way the applets are laid out inside it, so it will have to react to new applets being added or removed into the containment, by connecting to the appletAdded(Plasma::Applet *applet) and appletRemoved(Plasma::Applet *applet) signals.

If the applets in the containment can grow in any direction they want, the containment will have a 'Plasma::Planar FormFactor, otherwise will be Plasma::Horizontal if they can grow only horizontally and Plasma::Vertical if they can grow only vertically.

A typical use case is to set a Planar FormFactor when the Location is Plasma::Floating, Plasma::Desktop or Plasma::FullScreen. Which will be Horizontal for a BottomEdge or TopEdge Location and Vertical for a LeftEdge or RightEdge Location.

PluginLoader

Usually Applets, DataEngines, Services and Runners are loaded from plugins on disk (either .so libraries if written in C++, or Plasma Packages if scripted). In those cases, the plugins exist external to the shell itself and are loosely coupled. It is possible to create application-specific plugins by including the X-KDE-ParentApp= entry in their .desktop files, but sometimes this isn't enough. In some cases, it is most natural to create some plugins right from within the shell instead of relying on the standard plugin loading mechanisms.

This is most often the case with users of the Plasma KPart when they need to offer DataEngines or even Applets that are tightly coupled to internal data structures or classes.

For Plasma shells that do not need any special, non-standard plugin loading tied to internals, PluginLoader can be ignored as an implementation detail. For shells that do need to augment the standard plugin loading, Plasma::PluginLoader allows applications to do so with shell-specific internal processes in a way that is completely transparent to libplasma and all other Plasma plugins (e.g. DataEngines loaded that are used by Applets).

To take advantage of this facility, the shell must instantiate a subclass of Plasma::PluginLoader and pass it into Plasma::PluginLoader::setPluginLoader(PluginLoader* loader) prior to any calls to Corona, DataEngineManager, RunnerManager, etc. that result in plugin loading. As soon as the first plugin is loaded, if there is no PluginLoader registered the default PluginLoader will be created for the shell. Thus it is important to perform this process before anything else.

PluginLoader provides a standard pattern for each of the plugin types which follows:

  • KPluginInfo::List internal<PluginType>Info(): this returns the list of internal plugins for use in things such as "add widget" dialogs. There is a corresponding standardInternal<PluginType>Info() method that relies on the application installing .desktop files to $APPDATA/plasma/internal/<plugintype/. If an application follows that pattern, then the internal info listing method becomes a one-liner which calls the standard internal method. This is not required, however; the shell may implement any method of returning a KPluginInfo::List as it sees fit.
  • T *internalLoad<PluginType>(..): this method is called whenever a plugin of PluginType (e.g. Applet or DataEngine) is requested before the standard Plasma plugin loading code is run. If a valid pointer to an object is returned, then libplasma does not attempt to load the object using the plugin system.

By implementing these two methods for each plugin type of interest, shells can provide access to internal objects very easily. An example for DataEngines might look like this:

KPluginInfo::List MyPluginLoader::internalDataEngineInfo() const
{
    return standardInternalDataEngineInfo();
}

Plasma::DataEngine *MyPluginLoader::internalLoadDataEngine(const QString &name)
{
    if (name == "org.kde.myapp.SomeInternalEngine") {
        return new InternalDataEngine();
    }

    return 0;
}

Note that the "org.kde.myapp" prefix is now the recommended way of naming plugins, this way it is not possible for any conflictions to come about.

Bringing It All Together

The typical approach to bringing together the Corona, Views and Containments generally follows this flow:

  • setup the Corona; after creating an instance of Corona (usually a shell-specific subclass of it):
    • connect to the containmentAdded signals to be notified of when Containments are added; Containment removal is tracked by connecting to individual Containment destroyed signals
    • connect to the screenOwnerChanged signal if appropriate to the shell
  • call loadLayout() on the Corona
    • when Containments are added as a result, create and/or assign Views to the Containments as appropriate

When the shell is quitting, then:

  • call saveLayout() on the Corona, this ensures that the layout is saved for next time and will be reloaded properly when loadLayout() is called on next start
  • clean up any existing Views (which allows avoiding dealing with the case of Containments being deleted when the Corona is deleted on shell shut down)
  • delete the Corona object

It is critical that this shutdown procedure happens before the QApplication destructor is entered as some cleanup routines may rely on a fully setup application. This is why many Plasma shells do this work in a slot connected to the QApplication::aboutToQuit() signal.

Window Dressings

Containment Tool Boxes

Each containment shown by the Plasma Shell, can have a central place to show the main actions that can be performed.

A default set of toolbox actions is present in the base Containment implementation. Containment subclasses, as well as the Corona implementation of the Plasma shell can add their own.

The method Containment::AddToolBoxAction(QAction *a) adds the action a to the toolbox.

The action can contain metadata about its category, to help the toolbox implementations to visually group together similar actions, to do that call

QAction::setData(AbstractToolBox::ToolType)

A ToolBox is a subclass of Plasma::AbstractToolBox and is usually loaded as a plugin, whose desktop file will have the entry X-KDE-ServiceTypes=Plasma/ToolBox

A Containment implementation can force its own ToolBox implementation, but this is discouraged, especially in containments of type DesktopContainment or PanelContainment.

By default, containments will load the ToolBox plugin the Corona tells them. To set a default ToolBox plugin, use the function

Corona::setDefaultToolBoxPlugin(const QString &PluginName, Plasma::ContainmentType type)

in the constructor of your Corona implementation.

Applet Handles

Containment Actions

Containments (and Applets) can have a set of QAction associated. There are some already present created by default, such as the "add widgets" action.

Reimplementations can add their own actions. The relevant methods are:

  • void addAction(const QString &name, QAction *action): add a new action to the applet/containment.
  • QAction *action(const QString &name) const: retrieve the action associated with that name, it returns 0 if not found
  • QList<QAction *> actions() const: returns the list of all associated actions.

If some Containment actions should appear in the ToolBox, the method

Containment::addToolBoxAction(QAction *action) should be invoked.

It is advised (but not required) all the toolbox actions managed by the Containment are also part of the containment action set described above.

If you would like to remove any actions, you would have to know their name, call delete on QAction* action(QString). Examples of names are "zoom in", "zoom out", "add widgets", "configure" (for the settings page), "unlock widgets", "unlock desktop" (the latter is a bit of a misnomer, and refers to the "Leave..."/shutdown action.

You can just grep for some occurrences of these to lead you to where you want, if you need more.

Add Widgets Interface

All Plasma shells must provide a widget explorer interface.

Containments have an action called "add widgets". Corona implementations should connect to this action and show the widget explorer when this action is triggered.

The Plasma shells distributed with workspace use the library plasmagenericshell, that provides a widget explorer.

There are no particular guidelines to follow for implementing correctly a widget explorer. What is necessary is to show a list of the available applets, obtained with the static method

static KPluginInfo::List Applet::listAppletInfo(const QString &category, const QString &application)

The parameter category restricts the list to a specific category (that could be date and time, system information, eduction and so on) if it's empty all categories will be shown.

The parameter application will restrict the list to applets that are defined as specific for a certain parent application, with the entry X-Plasma-ParentApp in the Applet desktop file.

To list all categories it's used the method

static QStringList listCategories(const QString &parentApp = QString(), bool visibleOnly = true);

User Interface Management

Containment and View Configuration

Both the Containment and the associated View may want to offer a configuration UI to the user. This might includes options such as an interface to choose the Wallpaper to use with the Containment or the position of a Panel view.

It is often natural to include shell-specific features alongside the Containment configuration in one user interface for configuration. Additionally, different parts of a shell may want to present configuration differently, as Plasma Desktop does with the panel controller compared to the configuration dialog used for desktop Containments. Since only the shell can know about these design specifics, it is left up to the shell to manage configuration, unlike with Applets where configuration is handled inside libplasma.

Containments have a configureRequested(Plasma::Containment *containment) signal that can be connected to in order to trigger the creation and showing of the configuration UI.

Containment::createConfigurationInterface can be called to add Containment-specific pages to a configuration dialog, just as Wallpaper::createConfigurationInterface does for wallpaper plugins. Together with access to ContainmentActions related to the Containment, all of the plugin-specific configuration can be retrieved from the Containment and its active Wallpaper plugin (if any) and combined with shell-specific pages into one interface.

Remote Access Management

The remote widgets feature adds some new public API to libplasma, mostly aimed at plasma shells. They allow a shell to:

  • Access remote widgets. (AccessManager/AccessAppletJob)
  • Obtain a list of remote widgets that are announced on the network, and receive notifications when widgets appear/disappear. (AccessManager)
  • Publish widgets on the network. (Applet::publish(), Applet::unpublish(), Applet::isPublished())
  • Set one of the sensible default security policy on incoming connections... (AuthorizationManager)
  • ... or provide an own implementation of a security policy by implementing an AuthorizationInterface and setting it as customAuthorizationInterface in AuthorizationManager.

The plasma shell can control the security aspects of remote widgets, combined with a system wide config file (/etc/plasma-remotewidgets.conf) containing rules that can allow/disallow certain machines access to certain published widgets/services/engines. As a plasma shell you've got the following options:

  • Use one of the sensible presets in which case you don't have to care about security. The only thing you'll need to do is set the desired behavior using AuthorizationManager::self()->setAuthorizationPolicy(). This will set the AuthorizationInterface implementation to one of the ones built in into libplasma. Do note that even when you whish to use the default policy (DenyAll), you should still call this function, since that automatically locks this value so a potential malicious plasma plugin can't change this to serve it's own evil desires.
  • Supply your own behavior by implementing AuthorizationInterface, set authorizationPolicy to custom, and use AuthorizationManager::self()->setAuthorizationInterface() to your own implementation.

Dialog Positioning

If an Applet wants to show a QGraphicsWidget into a standalone window, it can use a Plasma::Dialog. It is a QWidget subclass and must be created without a parent, in order to be a top level window. A QGraphicsWidget (or a subclass) can be associated to a Dialog with Dialog::setGraphicsWidget(widget). The widget will be automatically positioned in an offscreen area of the Corona, with Corona::addOffScreenWidget(widget), that is not necessary to call it manually.

Animation Customization

Corona contains a set of protected methods which can be used to map animation types. This allows shells to customize what, for instance, the "AppearAnimation" actually means. Since different shells often have different needs when it comes to stock animations provided to Plasma components via Plasma::Animator, this is a way to accomplish that. So, for instance, a shell might do:

mapAnimation(Plasma::Animator::AppearAnimation, Plasma::Animator::ZoomAnimation);
mapAnimation(Plasma::Animator::DisappearAnimation, Plasma::Animator::ZoomAnimation);

Additionally, shells may install Javascript animations which are then accessible to any users of Plasma::Animator. These animations may also be mapped to standard animations such as AppearAnimation, allowing for a high degree of customization and even the addition of entirely new animation possibilities.

Configuration and Automation

Configuration Files

A Plasma shell uses 3 configuration files:

  • plasmarc: common to all Plasma shells, at the moment it contains the configuration about the used theme. This is completely transparent to the shell and can be ignored as an implementation detail.
  • <appname>rc: configuration specifics of the application containing global shell configuration values such as information about active views. This is also the standard configuration file used by all KDE applications as their default configuration storage location.
  • <appname>-appletsrc: this is the saved layout of all the Containments and the Applets inside each containment, with all of their individual configuration.

The files are created and managed by KDE and Plasma libraries and do not need to be referenced directly. For instance, when Corona::saveLayout() is called, the -appletsrc file is used automatically.

Scripting

Deployers and users of Plasma shells often find it more convenient, powerful and useful to interact with the shell using scripting. The recommended mechanism for doing so is to use Javascript via QtScript to offer a well defined set of shell-specific interaction API. This approach can be used in Corona::loadDefaultLayout() to create highly dynamic layouts without having to modify source code or manage complex default configuration files.

A comprehensive example of this is the Plasma Shell Scripting shared by Plasma Desktop and Netbook, or the scripting provided as part of the Plasma KPart shell.