Development/Tutorials/KDE2/Extending the KDE Panel

From KDE TechBase
Revision as of 11:09, 15 April 2007 by Annma (talk | contribs)
Warning
This page needs to be migrated. You can find the original page and its subpages at http://developer.kde.org/documentation/tutorials/dot/panel-applets.html. Please make use of subpages to structure the wiki, e.g. Policies/Packaging Policy. Read Help:Contents for further details. If in doubt, join #kde-www on irc.kde.org.

Introduction

The KDE panel (below referred to as "kicker") has been rewritten from scratch for KDE 2.0 and one of the main goals of the rewrite was to increase extensibility. For this reason with KDE 2.0 an API for panel applets has been introduced followed by an API for panel extension introduced with the KDE 2.1 release. Panel applets and extensions are simply referred to as "plugins" in the following paragraphs.

Panel applets are small applications living inside the panel. Available applets range from desktop pagers and task bars to little games or other toys. In contrast to applets the panel extension API is an interface for extensions living outside of the panel, in the window manager dock area (as defined in the freedesktop.org window manager specifications and are managed by the panel. Examples for panel extensions are the external task bar and the dock application bar which adds support for Window Maker applets and other applications using the standard X11 docking mechanism to kicker.

Starting with a short technology overview this tutorial discusses the implementation of a simple panel applet. Both the applet and extension APIs are simple. This qualifies writing a panel applet as a suitable task for an introduction to KDE programming. This tutorial presumes that the reader has some basic C++ and Qt knowledge. You can find a basic Qt tutorial at: http://doc.trolltech.com/tutorial.html

Overview

Panel plugins are implemented as DSO s (Dynamic Shared Objects). The panel locates available applets by searching for applet description files in (ALL_KDEDIRS)/share/apps/kicker/applets Every plugin must install a description file to be recognized by the panel. Available panel extensions are located by searching (A_KDEDIR)/share/apps/kicker/extensions for similar description files.

Implementing a panel applet is as easy as inheriting from the base class KPanelApplet (declared in kdelibs/kdeui/kpanelapplet.h), providing a factory function and installing the description file mentioned above. For extensions the respective base class is KPanelExtension , declared in kdelibs/kdeui/kpanelextension.h.

While plugins are implemented as shared libraries and thus loaded into the panel's address space, for security and stability reasons kicker also implements loading of plugins via external proxy processes called appletproxy and extensionproxy. The loading behavior is user configurable via kcontrol, with the obvious benefit that users may configure their panel to load untrusted third party plugins via the proxies to avoid buggy plugins taking the whole panel down with them.

While as an plugin author you don't have to be concerned about these implementation details it is good to know what happens behind the scenes. Therefore I might mention here that the panel communicates with the proxies via the DCOP protocol (see kdelibs/dcop) and makes use of QXEmbed (see kdelibs/kdeui/qxembed.h) to embed external (loaded via one of the two proxies) plugins into the panel.

A hello world panel applet

We start with a hello world panel applet that can be implemented in only a few lines of code and is a good base upon which to build a more complex applet. As already mentioned in the overview section all that needs to be done is inheriting from KPanelApplet , providing a factory function and a .desktop description file.

Our little project consists of four files: helloworldapplet.h containing the class declaration, helloworldapplet.cpp containing the class implementation and the factory function, helloworldapplet.desktop the description file and Makefile.am as input for the automake/autoconf KDE build system.

HelloWorldApplet class declaration

class HelloWorldApplet : public KPanelApplet {

 Q_OBJECT

public:

 HelloWorldApplet( const QString& configFile, Type t = Stretch, 
                   int actions = 0, QWidget *parent = 0, 
                   const char *name = 0 );
 int widthForHeight( int height ) const;
 int heightForWidth( int width ) const;

};

Having a look at kpanelapplet.h see that all we do is re-implement the constant virtual functions int widthForHeight(int) and heightForWidth(int) Playing around with kicker you may notice that you can arrange the panel either horizontally or vertically at the desktop borders. You will also notice that there is a fixed set of panel sizes (representing panel height for horizontal panels and width for vertical panels) to choose from.

The concept of panel applets is that one size component is dictated by the panel while the other one is free for the applet to choose. On horizontal panels the applet's height is fixed according to the panel height while the applet is free to choose its width. Respectively on vertical panels the applet's height is fixed while it is free to choose its height.

Every panel applet should be prepared to be used both on horizontal and vertical panels. The panel uses the two functions we are re-implementing for the HelloWorldApplet to query the applet's preferred size. A horizontal panel will call widthForHeight() while a vertical panel will call heightForWidth() to query the free size component of the applet. Applets are guaranteed to be resized according to the size they request.

HelloWorldApplet class implementation

HelloWorldApplet::HelloWorldApplet( const QString& configFile,

                                 Type type, int actions,
                                 QWidget *parent, const char *name )
 : KPanelApplet( configFile, type, actions, parent, name )

{

 setBackgroundColor( blue );
 setFrameStyle( StyledPanel | Sunken );

}

int HelloWorldApplet::widthForHeight( int h ) const {

 return h; // we want to be quadratic

}

int HelloWorldApplet::heightForWidth( int w ) const {

 return w; // we want to be quadratic

} Our constructor simply passes its default arguments to the KPanelApplet constructor, sets the frame style to StyledPanel Sunken (see qframe.h) and sets the background color of the applet to blue so we recognize it later running in the panel. Both widthForHeight() and heightForHeight() are implemented to choose a quadratic geometry for the applet. Thus we expect our applet to show up as a sunken blue square on the panel when run.

Screenshot of 'Hello, World!' applet

The factory function

extern "C" {

 KPanelApplet* init( QWidget *parent, const QString& configFile )
 {
     KGlobal::locale()->insertCatalogue( "helloworldapplet");
     return new HelloWorldApplet( configFile, KPanelApplet::Normal,
                                  0, parent, "helloworldapplet");
 }

}

The factory function is mostly copy&paste work where you would replace "helloworldapplet" with "myapplet" to adjust it to your custom applet.

Note
Don't change the factory function's signature or the panel will fail to load your applet. After adding the factory function, we are done with the code part and only the description file and the Makefile.am remain to be written.


The description file: helloworldapplet.desktop

[Desktop Entry] Name = Hello World Comment = Hello World Applet X-KDE-Library = libhelloworldapplet X-KDE-UniqueApplet = true

Besides standard .desktop file keys like "Name", "Comment" and "Icon" there are two panel applet specific keys:

X-KDE-Library
is used by the panel to locate the applet DSO (Dynamic Shared Object)

Example: X-KDE-Library=libexampleapplet

X-KDE-UniqueApplet
Similar to KApplication and KUniqueApplication there are two types of panel applets. Use unique applets when it makes no sense to run more than one instance of an applet in the panel. A good example for unique applets is the taskbar applet. Use normal applets when you need instance specific configuration. An example is the koolclock applet where you might want to run two instances in your panel, one configured as analog clock, the other one as digital clock. X-KDE-UniqueApplet is a boolean key which defaults to "false".

Example: X-KDE-UniqueApplet=true

The following conventions are used for the applet

DSOs:
Name
libappletnameapplet.la
LDFLAGS
-module -no-undefined

The automake input file: Makefile.am

INCLUDES = $(all_includes)

lib_LTLIBRARIES = libhelloworldapplet.la

libhelloworldapplet_la_SOURCES = helloworldapplet.cpp

METASOURCES = AUTO

noinst_HEADERS = helloworldapplet.h

lnkdir = $(kde_datadir)/kicker/applets

lnk_DATA = helloworldapplet.desktop

EXTRA_DIST = $(lnk_DATA)

libhelloworldapplet_la_LDFLAGS = $(all_libraries) -version-info 1:0:0 -module \

                              -no-undefined

libhelloworldapplet_la_LIBADD = $(LIB_KDEUI)

messages:

     $(XGETTEXT) *.cpp *.h -o $(podir)/helloworldapplet.pot

Explaining the details of this particular automake input file and the KDE build system in general is not the goal of this tutorial. So to adjust it to your applet project simply replace all occurrences of "helloworldapplet" with "myapplet".

Building and installing

You can download a complete helloworldapplet.tar.gz tarball here: khelloworldapplet.tar.gz (308 kB), which in addition to the four files mentioned above contains the automake/autoconf magic of the KDE build system. It's convenient to base your own panel applets on as the build system magic is already in place. Untar it, change into the untarred directory and build and install it with: /configure -prefix=<your-kde-dir> make su -c 'make install'

Hello world!

Now that your applet is installed and the description file in place, a "Hello World" entry will show up in the Add-Applet sub menu of the panel menu. Select the "Hello World" entry and you will see our blue sunken quadratic panel applet show up in the panel. You will also notice a small grey handle on the left side of the applet. Use the right mouse button context menu of the applet handle to move or remove the applet. You can also move applets by dragging the handle.

Fifteen pieces panel applet

Searching for a panel applet suitable for this tutorial I stumbled over a screenshot of an applet implementation of the old fifteen pieces game for another desktop environment. While it's more a toy than of any real use it is simple and fast to implement and thus perfect for a tutorial. I'm sure you know the fifteen pieces game with the goal to put fifteen sliding pieces into numerical oder. The idea of the game is that on a 4x4 cell quadratic game board fifteen numbered (1 to 15) quadratic pieces must be put into numerical order while pieces can only be moved horizontally or vertically and with only one free cell to perform move operations on.

We are going to use a customized QTableView widget for the game board. Similar to the hello world applet our little project will consist of three files, fiftenapplet.cpp , fifteenapplet.h , fifteenapplet.desktop and the Makefile.am file.

fifteenapplet.h contains the declaration of FifteenApplet , the class inheriting from KPanelApplet : class FifteenApplet : public KPanelApplet {

 Q_OBJECT

public:

 FifteenApplet(const QString& configFile, Type t = Stretch,
               int actions = 0,
               QWidget *parent = 0, const char *name = 0);
 int widthForHeight(int height) const;
 int heightForWidth(int width) const;
 void about();

private:

 PiecesTable *_table;
 KAboutData  *_aboutData;

}; While it is very similar to the declaration of HelloWorldApplet , we have added a reimplementation of void about() (as defined in kpanelapplet.h ) and private pointers for our game board class and a KAboutData object (see kdelibs/kdecore/kaboutdata.h ) used to build an about dialog from.

Searching for void about() in the KPanelApplet class declaration you will find two similar protected functions void help() and void preferences() In the right mouse button context menu of some applets you find in addition to the already mentioned "Move" and "Remove" entries, entries called "About", "Help" and "Preferences". The three protected functions are action handlers called when a user selects "About", "Help" or "Preferences" from the applets context menu. You have to reimplement them in your applet class to handle them. Not every applet implements for example the "Preferences" action because it might not have any preferences to configure. To avoid unused menu actions in the applet's context menu, kicker will only display those that you configure it to do by passing the according actions you reimplement or'ed together as the third parameter of the applet's class constructor.

The second class declared in fifteenapplet.h is our game board inheriting from QTableView : class PiecesTable : public QTableView {

 Q_OBJECT

public:

 PiecesTable(QWidget* parent = 0, const char* name = 0);

protected:

 void resizeEvent(QResizeEvent*);
 void mousePressEvent(QMouseEvent*);
 void mouseMoveEvent(QMouseEvent*);
 void paintCell(QPainter *, int row, int col);
 void initMap();
 void initColors();
 void randomizeMap();
 void checkwin();

private:

 QArray<int>     _map;
 QArray<QColor>  _colors;
 QPopupMenu     *_menu;
 int             _activeRow, _activeCol;
 bool            _randomized;
 enum MenuOp { mRandomize = 1, mReset = 2 };

}; I'm going to explain the details of this class below by means of the class implementation.

The factory function is very similar to the one of the helloworld applet with all occurrences of "helloworldapplet" replaced with "fifteenapplet". The second difference you might notice is that we pass KPanelApplet::About as the third parameter of the FifteenApplet constructor to make sure the "About" context menu entry will be there: extern "C" {

 KPanelApplet* init(QWidget *parent, const QString& configFile)
 {
     KGlobal::locale()->insertCatalogue("kfifteenapplet");
     return new FifteenApplet(configFile, KPanelApplet::Normal,
                              KPanelApplet::About, parent, "kfifteenapplet");
 }

} The implementation of the FifteenApplet class is very short as the game board class will handle all the drawing and game logic. FifteenApplet::FifteenApplet(const QString& configFile, Type type, int actions,

                          QWidget *parent, const char *name)
 : KPanelApplet(configFile, type, actions, parent, name), _aboutData(0)

{

 // setup table
 _table = new PiecesTable(this);
 // setup layout
 QHBoxLayout *_layout = new QHBoxLayout(this);
 _layout->add(_table);
 srand(time(0));

} The constructor creates an instance of our game board class and places it into a simple layout object to resize it to the full applet size every time the applet itself is resized. srandom is used to initialize the random number generator with the current time in seconds (see the man pages of srandom and random) as seed. int FifteenApplet::widthForHeight(int h) const {

 return h; // we want to be quadratic

}

int FifteenApplet::heightForWidth(int w) const {

 return w; // we want to be quadratic

} Similar to the hello world applet the fifteen pieces applet will have a quadratic shape. void FifteenApplet::about() {

 if(!_aboutData) {
     _aboutData = new KAboutData("kfifteenapplet", I18N_NOOP("KFifteenApplet"), 
             "1.0", I18N_NOOP("Fifteen pieces applet.\n\n"
             "The goal is to put the sliding pieces into numerical order.\n"
             "Select \"Randomize Pieces\" from the RMB menu to start a game."),
             KAboutData::License_BSD, "(c) 2001, Matthias Elter");
     _aboutData->addAuthor("Matthias Elter", 0, "[email protected]");
 }
 KAboutApplication dialog(_aboutData);
 dialog.show();

}