Difference between revisions of "Creating TWin decorations"
(Added text with some basic formatting. (TODO: insert images, more formatting)) |
m (Added to category Tutorials) |
||
Line 1: | Line 1: | ||
+ | |||
+ | [[Category:Tutorials]] |
||
This is a HOWTO tutorial which was originally written for KWin3 by David Johnson |
This is a HOWTO tutorial which was originally written for KWin3 by David Johnson |
Revision as of 14:39, 28 March 2021
This is a HOWTO tutorial which was originally written for KWin3 by David Johnson
[1].
General notes
The default window manager for KDE is KWin. KWin has a very flexible theming mechanism, because all decorations are ultimately plugins to the window manager. This allows you to change both the look (appearance) and feel (behavior) of the window manager.
This tutorial covers creating a native KWin decoration using C++ code, as opposed to creating a decoration for "pixmap engine", such as the IceWM plugin. The material presented here may be of use in creating a new pixmap engine however.
The sample code for this tutorial, along with a copy of this documentation, can be found here[2]. The code snippets shown in this documentation are not documented, but the source code itself is.
This tutorial is assuming that you have some experience with C++ and the Qt toolkit. Specifically, it assumes that you understand C++ inheritance, the Qt signal/slot paradigm, and are familiar with the common Qt classes. If you have never done any Qt programming, please stop and take some time to familiarize yourself with this excellent toolkit. It comes with some of the best documentation for a development library that I have seen.
Note: This tutorial and accompanying example code are based on KDE 3.2. It is not applicable to earlier versions of KDE.
Step 1: Proper planning
Before you start working on a new decoration, you should have some idea as to what the finished decoration will look like. All usable window managers will have a title bar containing buttons and a frame which surround the application. There are two classic layouts for window manager decorations, with some minor variations. For lack of standard names, I'll call them the "motif" and "next" layouts.
The "motif" layout is the most popular. The title bar and window are surrounded by a thick frame. Sometimes the frame only wraps the left, right and bottom of the window, with the title bar being the top part of the frame.
The "next" layout was first seen, to the best of my knowledge, in the NeXT desktop. It is used by the Windowmaker and Blackbox window managers, as well as several KDE decorations. There is only a titlebar on top and a "handle" on the bottom.
The example decoration will use the motif layout because it is traditional. Since this is an example, I'm going to make everything look quite plain with simple lines delineating the parts of the decoration. Think of the example as a template from which to create your own, more aesthetic, decorations.
Drawing out the decoration in a paint program like GIMP is very helpful. Sometimes what you think will look good won't, once you actually see it.
Education is also a part of planning. Read the kdecoration.h and kdecorationfactory.h header files. They are located in the $KDEBASE/include directory, and are very well documented. These should be your primary documentation for the KWin API. Read the source code for other KDE decorations.
If you are porting an earlier KWin decoration to the new KWin API for KDE 3.2, please use the PORTING document created for this purpose. It is located in the KDE source tree at kdebase/kwin/clients/PORTING. It will help you find the places in your decoration that need changing.
Usability
Always keep the usability of the decoration in mind. No matter how gorgeous the look, no one will use it if it keeps getting in their way. Some simple rules will keep your decoration usable by most people. If you must break any of the following rules, please have a good reason to do so.
Honor Standard Behavior: There are certain standard behaviors that users will expect. Users do not want to change the way they interact with windows every time they change the decoration. The title bar should be on the top. The maximize button should maximize the window. The frame or handle should be large enough to easily resize the window.
Honor User Preferences: If users do not like the default behaviors, they will change them. Do not presume to know what is best for the user. For example, people who use custom button positions do so for a reason, and it will only annoy them to disregard their preference. Those that don't will still expect to see the standard button positions.
Step 2: Infrastructure
To start out your project, you'll need a build infrastructure. This will be similar to most KDE applications. Surprisingly, it's an area a lot of people skimp on. The default infrastructure is based on GNU's automake/autoconf framework. There should be very little you need to change from the example code provided.
There should be an admin directory that contains the standard KDE admin files. A lot of work has been done to make your life as a KDE developer easy, and most of it is in this directory. I have not included this directory in the example package, but you can obtain this from any KDE source package. The best place is to copy it from a current kdesdk package.
Please include a README and INSTALL file. Describe what this decoration is, and how to install it. And by all means, please state what license your code is under! There's nothing worse than wanting to borrow a piece of code, and not knowing if you're legally entitled to. The preferred location for this information is the COPYING file. The GPL is the most popular for KDE programs, but many decorations are under the MIT license, since that's what KWin itself is under.
If you're using the example package as a template, you will need to modify the configure.in.in, client/Makefile.am, client/config/Makefile.am, and example.desktop files. Simply replace all occurances of "example" with the name of your decoration. If you add additional source code files, you'll need to change these files as appropriate. Please see the KDE Developer FAQ for more information on the standard KDE build infrastructure.
Step 3: Configuration
If there are any user configurable aspects to your decoration, you should create a configuration dialog. The configuration dialog is actually a plugin to Control Center. Common configuration options for KWin decorations include title alignment, title bar height, and displaying an additional handle. For the example, we will be using only one option to specify the title alignment.
I have chosen to use Qt Designer to handle the layout of the dialog. This makes it very easy to arrange the dialog. Add three QRadio buttons in a QButtonGroup box to let the user choose the alignment of the title text. Add some "What's This" help text. Designer will make all of the widgets public so that you can easily access them from your configuration plugin.
The exampleconfig.h header file is quite short, and has only two member variables, one for the configuration, and one for the actual dialog:
...
private:
KConfig *config_;
ConfigDialog *dialog_;
The exampleconfig.cc is almost as simple. Since most of the GUI work is being done by Qt Designer in our ui file, all we need to do is worry about is the configuration. The constructor creates a new configuration object and dialog, and connects with the dialog.
ExampleConfig::ExampleConfig(KConfig* config, QWidget* parent)
: QObject(parent), config_(0), dialog_(0)
{
config_ = new KConfig("kwinexamplerc");
KGlobal::locale()->insertCatalogue("kwin_example_config");
dialog_ = new ConfigDialog(parent);
dialog_->show();
load(config_);
connect(dialog_->titlealign, SIGNAL(clicked(int)),
this, SLOT(selectionChanged(int)));
}
Our work consists of loading and saving the configuration, and setting some sensible defaults. We simply set the dialog widgets to the existing configuration, and write them out again when they change.
void ExampleConfig::load(KConfig*)
{
config_->setGroup("General");
QString value = config_->readEntry("TitleAlignment", "AlignHCenter");
QRadioButton *button = (QRadioButton*)dialog_->titlealign->child(value);
if (button) button->setChecked(true);
}
void ExampleConfig::save(KConfig*)
{
config_->setGroup("General");
QRadioButton *button = (QRadioButton*)dialog_->titlealign->selected();
if (button) config_->writeEntry("TitleAlignment", QString(button->name()));
config_->sync();
}
Step 4: The Factory
Each window is known as a "client", and is represented by the ExampleClient class. If you have five windows up on the screen, you will have five instances of ExampleClient. Initializing and managing the decoration for multiple clients is the job of the factory. Every decoration will need a factory class, since this is what KWin uses to create the decoration.
The decoration factory is often used to store global data needed by the clients. We'll use the ExampleFactory to take care of storing the configuration data. It could also be used to create and store the pixmaps used by the clients.
ExampleFactory::ExampleFactory()
{
readConfig();
initialized_ = true;
}
KDecoration* ExampleFactory::createDecoration(KDecorationBridge* b)
{
return new ExampleClient(b, this);
}
The factory constructor reads in the configuration and performs any other global initialization for the decoration. KWin will call the createDecoration() method to create the decoration for each client window.
bool ExampleFactory::reset(unsigned long changed)
{
initialized_ = false;
bool confchange = readConfig();
initialized_ = true;
if (confchange ||
(changed & (SettingDecoration | SettingButtons | SettingBorder))) {
return true;
} else {
resetDecorations(changed);
return false;
}
}
We need to reset all the clients whenever the configuration or settings have changed. We first read in our decoration configuration, noting if anything has changed. Then we used the changed parameter to find out if any KWin settings have changed. changed is a bit field that specifies values for SettingDecoration, SettingColors, SettingFont, SettingButtons, SettingTooltips, or SettingBorder.
If the reset() method returns true if all of the decorations need to be remade, or false if only a repaint is necessary. We do not remake the decorations if only the color, font or tooltip status has changed.
bool ExampleFactory::readConfig()
{
KConfig config("kwinexamplerc");
config.setGroup("General");
Qt::AlignmentFlags oldalign = titlealign_;
QString value = config.readEntry("TitleAlignment", "AlignHCenter");
if (value == "AlignLeft") titlealign_ = Qt::AlignLeft;
else if (value == "AlignHCenter") titlealign_ = Qt::AlignHCenter;
else if (value == "AlignRight") titlealign_ = Qt::AlignRight;
if (oldalign == titlealign_) return false;
else return true;
}
Reading in the configuration is very similar to how we read it in the configuration dialog. By assigning the titlealign_ member to the title alignment, clients can determine their title alignment without having to load the configuration. They will do this with ExampleFactory's titleAlign() method.
Note that we used the oldalign variable to detect any configuration changes. We do not want to unnecessarily remake the decorations if the configuration has not changed.
Step 5: The Buttons
The buttons on the titlebar are derived from the QButton class, but we still need to determine their look and behavior.
ExampleButton::ExampleButton(ExampleClient *parent, const char *name,
const QString& tip, ButtonType type,
const unsigned char *bitmap)
: QButton(parent->widget(), name), client_(parent), type_(type),
deco_(0), lastmouse_(NoButton)
{
setBackgroundMode(NoBackground);
setFixedSize(BUTTONSIZE, BUTTONSIZE);
setCursor(arrowCursor);
if (bitmap) setBitmap(bitmap);
QToolTip::add(this, tip);
}
The constructor sets the size of the button, the cursor that will be shown when the mouse is over the button, and the bitmap decoration. The decoration is what visually distinguishes the buttons from each other. The close button will have an "x" decoration, while the minimize button will have a "v" decoration.
void ExampleButton::setBitmap(const unsigned char *bitmap)
{
if (!bitmap) return;
if (deco_) delete deco_;
deco_ = new QBitmap(DECOSIZE, DECOSIZE, bitmap, true);
deco_->setMask(*deco_);
repaint(false);
}
A QBitmap has only two colors, foreground and background. If we wanted more colors for our decorations we could have used QPixmap instead. We repaint the button every time the bitmap is changed.
For the menu button we do use a QPixmap. In this case we will use the application icon. We will access this pixmap from the ExampleClient when we draw the button.
void ExampleButton::enterEvent(QEvent *e)
{
QButton::enterEvent(e);
}
void ExampleButton::leaveEvent(QEvent *e)
{
QButton::leaveEvent(e);
}
We don't do anything with the enter and leave events except pass them on to the base class. If we wanted to implement "mouse over" highlighting of the buttons, however, this is where we would start.
void ExampleButton::mousePressEvent(QMouseEvent* e)
{
lastmouse_ = e->button();
int button = LeftButton;
if ((type_ != ButtonMax) && (e->button() != LeftButton)) {
button = NoButton;
}
QMouseEvent me(e->type(), e->pos(), e->globalPos(),
button, e->state());
QButton::mousePressEvent(&me);
}
void ExampleButton::mouseReleaseEvent(QMouseEvent* e)
{
lastmouse_ = e->button();
int button = LeftButton;
if ((type_ != ButtonMax) && (e->button() != LeftButton)) {
button = NoButton;
}
QMouseEvent me(e->type(), e->pos(), e->globalPos(),
button, e->state());
QButton::mouseReleaseEvent(&me);
}
These two events tell us about mouse clicks. These are just the events, and not the signals, so we don't perform any actual windowing behaviors, such as minimize or close. But we do want to remember which mouse button did the clicking. That way if the maximize button was pressed, we will know whether to maximize horizontally, vertically or full.
Unless it is the maximize button, it is a good idea for the buttons to react to left mouse presses only. Only respond to middle or right clicks if your button makes use of them. In our example, we translate middle and right clicks to "NoButton" if the button is not the maximize button, and pass it on to the base class.
Notice that we translate all other clicks to "LeftButton". This is because the base QButton class only responds to left mouse button clicks. We're saving the actual button pressed since our maximize button will have different behaviors depending on which button was pressed.
void ExampleButton::drawButton(QPainter *painter)
{
if (!ExampleFactory::initialized()) return;
QColorGroup group;
int dx, dy;
...
The last method to ExampleButton is to draw the button. We return if the factory has not been initialized. This should never happen, but it's better to be safe than to have several hundred bug reports concerning mysterious crashes.
...
group = KDecoration::options()->colorGroup(KDecoration::ColorButtonBg, client_->isActive());
painter->fillRect(rect(), group.button());
painter->setPen(group.dark());
painter->drawRect(rect());
...
There are an infinite number of ways to draw the button. For the purposes of this example, we merely draw a blank button with a dark border around it.
Notice the call to the KDecoration::options object. KWin keeps track of several configuration items which we can access through the the KDecoration::options class. One of these items is a QColorGroup generated from the user defined button color.
...
if (type_ == ButtonMenu) {
dx = (width() - 16) / 2;
dy = (height() - 16) / 2;
if (isDown()) { dx++; dy++; }
painter->drawPixmap(dx, dy,
client_->icon().pixmap(QIconSet::Small, QIconSet::Normal));
} else if (deco_) {
dx = (width() - DECOSIZE) / 2;
dy = (height() - DECOSIZE) / 2;
if (isDown()) { dx++; dy++; }
painter->setPen(group.dark());
painter->drawPixmap(dx, dy, *deco_);
}
}
Finally we paint the button decoration. We do some minor calculations to center the decoration in the button. If this is the menu button, we draw the application icon. Notice that we get this pixmap from the client. If it's any other button, we draw the bitmap decoration.
A common effect for buttons is to have make it look "pressed" when the mouse clicks on it, and it's in the "down" state. We do this by shifting the position of the decoration slightly.
Step 6: The Client
The ExampleClient class is the heart of the decoration. Is provides most of the theming, and contains the application window and the title bar buttons.
ExampleClient::ExampleClient(KDecorationBridge *b, KDecorationFactory *f)
: KDecoration(b, f) { ; }
The constructor doesn't do anything. All of the initialization is done in the init() method, which is called by KWin.
void ExampleClient::init()
{
createMainWidget(WResizeNoErase | WRepaintNoErase);
widget()->installEventFilter(this);
widget()->setBackgroundMode(NoBackground);
...
In the initializer we create the main widget of the decoration, passing in a couple of flags to help eliminate some flicker when resizing and repainting windows. This should be the first method called in init(). We also set a NoBackground mode for the same reason. An event filter is set so that we receive important events. More information on this event filter is given below.
...
QGridLayout *mainlayout = new QGridLayout(widget(), 4, 3); // 4x3 grid
QHBoxLayout *titlelayout = new QHBoxLayout();
titlebar_ = new QSpacerItem(1, TITLESIZE, QSizePolicy::Expanding,
QSizePolicy::Fixed);
mainlayout->setResizeMode(QLayout::FreeResize);
mainlayout->addRowSpacing(0, FRAMESIZE);
mainlayout->addRowSpacing(3, FRAMESIZE*2);
mainlayout->addColSpacing(0, FRAMESIZE);
mainlayout->addColSpacing(2, FRAMESIZE);
mainlayout->addLayout(titlelayout, 1, 1);
...
Qt's layout classes are very flexible and powerful, so we use them to layout our decoration. We create a four row by three column grid. See the "motif" diagram above for a visual explanation.
The only objects we actually add to the main layout is the titlelayout and window. The outer rows and columns around them are empty spacing. We will later use this spacing to draw the window frame.
if (isPreview()) {
mainlayout->addWidget(
new QLabel(i18n("<b><center>Example preview</center></b>"),
widget()), 2, 1);
} else {
mainlayout->addItem(new QSpacerItem(0, 0), 2, 1);
}
Normally the center of the layout is a spacer item. The actual client widnow is drawn by KWin on top of the spacer. But when the decoration is previewed in the Control Center, we need to set our own widget for display purposes. In this case we simply make it a label with the name of the decoration.
...
mainlayout->setRowStretch(2, 10);
mainlayout->setColStretch(1, 10);
...
It is important to ensure that only the central window changes size when the window is resized. We do this by setting the stretch for the central row and column. Try removing these two lines to see what happens without it. It's not pretty.
for (int n=0; n<ButtonTypeCount; n++) button[n] = 0;
addButtons(titlelayout, options()->titleButtonsLeft());
titlelayout->addItem(titlebar_);
addButtons(titlelayout, options()->titleButtonsRight());
}
To finish up initialization we layout the titlebar. The options object will tell us the button layout that the user has chosen. This options object is the same as the KDecoration::options object we used earlier in the button class. Both the local and the global static objects are identical. Between the left and the right buttons is the titlebar_ spacer we created. This spacer will stretch horizontally as needed, but keep a fixed vertical height.
Adding Buttons
I have created an addButton() method to ease the creation and layout of the title bar buttons. Most decorations that honor custom button layouts use a similar method. The method is lengthy, but the concept is simple.
void ExampleClient::addButtons(QBoxLayout *layout, const QString& s)
{
unsigned char *bitmap;
QString tip;
if (s.length() > 0) {
for (unsigned n=0; n < s.length(); n++) {
switch (s[n]) {
case 'M': // Menu button
if (!button[ButtonMenu]) {
button[ButtonMenu] =
new ExampleButton(this, "menu", i18n("Menu"),
ButtonMenu, 0);
connect(button[ButtonMenu], SIGNAL(pressed()),
this, SLOT(menuButtonPressed()));
layout->addWidget(button[ButtonMenu]);
}
break;
...
A string is passed in representing the button positions, and the layout the buttons are to be added to. We go through the string one character at a time. For each character we construct a button. In the first case shown above, the character 'M' constructs a menu button, connects the pressed signal to the menuButtonPressed() method, and adds it to the layout.
Notice that we are keeping our buttons points in an array. This makes it easy to access all buttons at once, as we did when we initialized all the pointers to null in the initializer. An enum is used to conveniently access them.
...
case 'S': // Sticky button
if (!button[ButtonSticky]) {
if (isOnAllDesktops()) {
bitmap = stickydown_bits;
tip = i18n("Un-Sticky");
} else {
bitmap = sticky_bits;
tip = i18n("Sticky");
}
button[ButtonSticky] =
new ExampleButton(this, "sticky", tip,
ButtonSticky, bitmap);
connect(button[ButtonSticky], SIGNAL(clicked()),
this, SLOT(toggleOnAllDesktops()));
layout->addWidget(button[ButtonSticky]);
}
break;
...
The sticky button is similar. The difference is that we must check the state of the window. We set the appropriate bitmap and tooltip text depending on the "sticky" state of the window. A similar state check also needs to be made when creating the maximize button. See the example source code for the details.
...
case '_': // Spacer item
layout->addSpacing(FRAMESIZE);
}
}
}
}
A spacing in the button layout is a special case. In the example decoration we merely add a small bit of spacing to the layout.
The finished title bar layout will look like this:
Painting the Window
void ExampleClient::paintEvent(QPaintEvent*)
{
if (!ExampleFactory::initialized()) return;
QColorGroup group;
QPainter painter(widget());
...
Painting the the window is simply a matter of determining the coordinates of the various parts, then using the standard QPainter methods to draw. The only parts we need to worry about are the titlebar and the outside frame. The buttons and the window will draw themselves.
...
QRect title(titlebar_->geometry());
group = options()->colorGroup(KDecoration::ColorTitleBar, isActive());
painter.fillRect(title, group.background());
painter.setPen(group.dark());
painter.drawRect(title);
...
To draw the titlebar we get its coordinates, set the color group, fill it with the background color, then outline it with a dark color.
For the example this is good enough. Other possibilities are to draw a bevel effect around the title, using a gradient or custom pixmap, or even fancier techniques.
...
painter.setFont(options()->font(isActive(), false));
painter.setPen(options()->color(KDecoration::ColorFont, isActive()));
painter.drawText(title.x() + FRAMESIZE, title.y(),
title.width() - FRAMESIZE * 2, title.height(),
ExampleFactory::titleAlign() | AlignVCenter,
caption());
...
The options object also gives us the font and font color. The false parameter when retrieving the font indicates that we want the normal font. If we had set it to true we would have gotten a smaller font suitable for use in tool window titlebars.
A small space is left on the left and right sides, so that the title text doesn't run up against the buttons.
...
group = options()->colorGroup(KDecoration::ColorFrame, isActive());
QRect frame(0, 0, width(), FRAMESIZE);
painter.fillRect(frame, group.background());
frame.setRect(0, 0, FRAMESIZE, height());
painter.fillRect(frame, group.background());
frame.setRect(0, height() - FRAMESIZE*2, width(), FRAMESIZE*2);
painter.fillRect(frame, group.background());
frame.setRect(width()-FRAMESIZE, 0, FRAMESIZE, height());
painter.fillRect(frame, group.background());
// outline the frame
painter.setPen(group.dark());
frame = widget()->rect();
painter.drawRect(frame);
frame.setRect(frame.x() + FRAMESIZE-1, frame.y() + FRAMESIZE-1,
frame.width() - FRAMESIZE*2 +2,
frame.height() - FRAMESIZE*3 +2);
painter.drawRect(frame);
}
Drawing the outer frame involves a more complex geometry. We simplify this by separating it into four parts. The corners of the frame will each be drawn twice, due to the overlap, but for simple fills, this does not result in any performance loss. Notice that the frame geometry matches the spacing we put in the main layout.
Event Handling
We've finished all of our drawing code. But we're still far from done with the client class. There are events we have to take care of. As you may have noticed, our decoration is not a widget. Instead it has a QWidget as a member. This widget is placed behind the application window when it is drawn. The event filter is so that our decoration class can handle the events meant for the widget member.
bool ExampleClient::eventFilter(QObject *obj, QEvent *e)
{
if (obj != widget()) return false;
switch (e->type()) {
case QEvent::MouseButtonDblClick: {
mouseDoubleClickEvent(static_cast<QMouseEvent *>(e));
return true;
}
case QEvent::MouseButtonPress: {
processMousePressEvent(static_cast<QMouseEvent *>(e));
return true;
}
case QEvent::Paint: {
paintEvent(static_cast<QPaintEvent *>(e));
return true;
}
case QEvent::Resize: {
resizeEvent(static_cast<QResizeEvent *>(e));
return true;
}
case QEvent::Show: {
showEvent(static_cast<QShowEvent *>(e));
return true;
}
default: {
return false;
}
}
return false;
}
Our event filter is simple. We merely pass on the events to the appropriate method. If we do not handle an event, we return false so that it will be handled elsewhere.
void ExampleClient::mouseDoubleClickEvent(QMouseEvent *e)
{
if (titlebar_->geometry().contains(e->pos()))
titlebarDblClickOperation();
}
When the user double clicks in our window, we need to decide what to do. The only place where a double click does anything is in the title bar, so we first check to see if that's where the event happened. If so, we tell KWin to perform the titlebarDblClickOperation() action. Usually this shades or unshades the window, but the user can redefine this behavior.
Other events that our decoration must handle are resizeEvent() and showEvent(). These are not shown here, but are included in the example code. We already covered the paintEvent() method earlier.
State Changes and Information
There are a lot of state changes that occur in a KWin decoration. When a change occurs, KWin will call the appropriate method in our class. We also need methods to provide KWin with information about our decoration.
void ExampleClient::activeChange()
{
for (int n=0; n<ButtonTypeCount; n++)
if (button[n]) button[n]->reset(false);
widget()->repaint(false);
}
A window is only active when it has the focus. The typical KWin behavior is to use a different color scheme for active and inactive windows. When the focus state changes, we need to redraw the window because it will have a different color scheme. This is a simple matter of redrawing all the buttons, and then the client. If you wish to know if the window is active or inactive, use the isActive() method.
void ExampleClient::maximizeChange()
{
bool m = (maximizeMode() == MaximizeFull);
if (button[ButtonMax]) {
button[ButtonMax]->setBitmap(m ? minmax_bits : max_bits);
QToolTip::remove(button[ButtonMax]);
QToolTip::add(button[ButtonMax], m ? i18n("Restore") : i18n("Maximize"));
}
}
When the window has changed its maximize state, we need to change the button decoration for the maximize button. We also need to change the "tooltip" text
void ExampleClient::borders(int &l, int &r, int &t, int &b) const
{
l = r = FRAMESIZE;
t = TITLESIZE + FRAMESIZE;
b = FRAMESIZE * 2;
}
The borders() method returns the sizes of the decoration borders. These sizes might change according to the window maximize of shaded state. They could also change if border width is a user configurable parameter. For the example decoration we will keep the border sizes the same for all states.
It is important that this method return the real size of the borders. KWin doesn't pay attention to the spacing we added to the main widget above. All it cares about are the values we return here. If they do not match the "reality", the the decorations will not be drawn correctly.
KDecoration::Position ExampleClient::mousePosition(const QPoint &point) const
{
const int corner = 24;
Position pos;
if (point.y() <= FRAMESIZE) { // inside top frame
if (point.x() <= corner) pos = PositionTopLeft;
else if (point.x() >= (width()-corner)) pos = PositionTopRight;
else pos = PositionTop;
} else if (point.y() >= (height()-FRAMESIZE*2)) { // inside handle
if (point.x() <= corner) pos = PositionBottomLeft;
else if (point.x() >= (width()-corner)) pos = PositionBottomRight;
else pos = PositionBottom;
} else if (point.x() <= FRAMESIZE) { // on left frame
if (point.y() <= corner) pos = PositionTopLeft;
else if (point.y() >= (height()-corner)) pos = PositionBottomLeft;
else pos = PositionLeft;
} else if (point.x() >= width()-FRAMESIZE) { // on right frame
if (point.y() <= corner) pos = PositionTopRight;
else if (point.y() >= (height()-corner)) pos = PositionBottomRight;
else pos = PositionRight;
} else { // inside the frame
pos = PositionCenter;
}
return pos;
}
KWin also needs to know where the mouse cursor is logically oriented in the window. We use the mouse position to compute the logical positions. These will be used to change the mouse cursor and direct any resizing actions.
Actions
Finally we need to handle user actions such as mouse clicks. These will be the slots for our earlier decoration buttons.
void ExampleClient::maxButtonPressed()
{
if (button[ButtonMax]) {
switch (button[ButtonMax]->lastMousePress()) {
case MidButton:
maximize(maximizeMode() ^ MaximizeVertical);
break;
case RightButton:
maximize(maximizeMode() ^ MaximizeHorizontal);
break;
default:
(maximizeMode() == MaximizeFull) ? maximize(MaximizeRestore)
: maximize(MaximizeFull);
}
}
}
The expected behavior for KWin maximize buttons is to maximize horizontally if the right button is pressed, maximize vertically if the middle button is pressed, and maximize fully if the left button was pressed. We know which mouse button was pressed because we had the button save that information for us earlier.
The maximize mode of a window can be MaximizeRestore, MaximizeVertical, or MaximizeHorizontal. There is also a MaximizeFull mode, which is identical to (MaximizeVertical | MaximizeHorizontal). We can use this information to easily change the maximization state of the window.
void ExampleClient::menuButtonPressed()
{
if (button[ButtonMenu]) {
QPoint p(button[ButtonMenu]->rect().bottomLeft().x(),
button[ButtonMenu]->rect().bottomLeft().y());
KDecorationFactory* f = factory();
showWindowMenu(button[ButtonMenu]->mapToGlobal(p));
if (!f->exists(this)) return; // 'this' was destroyed
button[ButtonMenu]->setDown(false);
}
}
Clicking in the menu button brings up the window menu. We want this menu positioned just below our button, so we pass this point to KWin's showWindowMenu() method.
For some decorations, a double click in the menu button will close the window. There is a small controversy over whether this is desirable behavior. I have decided not to implement "double-click to close" functionality for the example.
Notice the call the exists(). If the user selects the "close" option from the menu, then the window decoration will be destroyed. This will invalidate the current decoration object, "this".
Wrapping Things Up
I haven't shown all of the example source code here, but it's all included in the example-0.8.tar.gz package. Besides reading though the example, also look at the kdecoration.h header file located in $KDEDIR/include/kwin/lib. This header file is well documented. Also look at other KWin decorations.
The finished product isn't pretty, but it's fully functional. Think of it as your starting point for your own KWin decoration.
Tips and Tricks
Developing a new KWin decoration can be sometimes frustrating. It seems that if you want to test a minor code change you must kill the existing KWin window manager and then restart it. If you know some tricks, however, it isn't difficult.
If all you're interested in seeing are changes to the look of the decoration, you can see them in the KControl window decoration module preview. If you're interested in seeing the behavior of the decoration, then you must restart KWin. The simplest and safest way to do this is to use the --replace option to KWin. This "replaces" the current window manager with KWin, even if the current manager is already KWin.
kwin --replace &
Also remember that if you're porting an earlier KWin decoration to the new API, use the PORTING document to help you out.
Excercises
Here's a list of simple exercises to get you some hands on knowledge of KWin decorations
- When we shade a window, we still draw the handle and borders. Fix this so that when a window is shaded only the titlebar is drawn.
- If the custom button layout includes spacing between the buttons, we add a spacer item to the title layout. But we don't draw those spacers. This results in an ugly transparency. Draw those spacers in the title bar color.
- The simple dark lines around the window parts is very plain. It's almost, but not quite, a presentable decoration. Fix up the little details to make something you might want to actually use on your desktop. With just a little bit of work, you could make this a clone of the CDE or KDE1 decorations.