User:Mgraesslin/Effects

From KDE TechBase
Warning
Most of the API presented in this tutorial is not available any more in 4.7 or later. Please refer to the API documentation


This tutorial illustrates how to write a small KWin effect. It is based on KWin's resize effect, which highlights the changed geometry while resizing a window. The effect support both backends: XRender and OpenGL.

Getting Started

Each effect has an own directory in kwin/effects so we create a new directory resize. There we create the following files:

  • CMakeLists.txt
  • resize.h
  • resize.cpp
  • resize.desktop

We have to include this directory into the build, so we edit the CMakeLists.txt in the effects directory. We just add the following line to the section marked as "Common effects": include( resize/CMakeLists.txt ) If it were an OpenGL only effect we would place this line in the section marked as "OpenGL-specific effects". So at this point we are finished with the preparation. So let's start looking at the files. First the desktop file:

[Desktop Entry]
Name=Resize Window
Icon=preferences-system-windows-effect-resize
Comment=Effect to outline geometry while resizing a window

Type=Service
X-KDE-ServiceTypes=KWin/Effect
X-KDE-PluginInfo-Author=Martin Gräßlin
[email protected]
X-KDE-PluginInfo-Name=kwin4_effect_resize
X-KDE-PluginInfo-Version=0.1.0
X-KDE-PluginInfo-Category=Window Management
X-KDE-PluginInfo-Depends=
X-KDE-PluginInfo-License=GPL
X-KDE-PluginInfo-EnabledByDefault=false
X-KDE-Library=kwin4_effect_builtins
X-KDE-Ordering=60

Most of it is self explaining and just needed for the "All effects" tab in the compositing kcm. The most important value is the "X-KDE-PluginInfo-Name". This is the name used to load the effect and has to start with "kwin4_effect_" followed by your custom effect name. This last part will be needed in the source code. Each effect is a subclass of class "Effect" defined in kwineffects.h and implements some of the virtual methods provided by Effect. There are methods for almost everything the window manager does. So by implementing those methods you can react on change of desktop or on opened/closed windows. In this effect we are interested in resize events so we have to implement method "windowUserMovedResized( EffectWindow *w, bool first, bool last )". This method is called whenever a user moves or resizes the given window. The two boolean values indicate if it is the first, last or an intermediate resize event. But there are more methods we have to implement. The effect should paint the changed geometry while resizing. So we have to implement the methods required for custom painting. KWin's painting pass consists of three stages:

  • pre paint
  • paint
  • post paint

These stages are executed once for the complete screen and once for every window. All effects are chained and each effect calls the stage for the next effect. How this works we will see when looking at the implementation. You can find a good documentation in the comments of scene.cpp Now it's time to have a look at the header file:

  1. ifndef KWIN_RESIZE_H
  2. define KWIN_RESIZE_H
  1. include <kwineffects.h>

namespace KWin {

class ResizeEffect

   : public Effect
   {
   public:
       ResizeEffect();
       ~ResizeEffect();
       virtual void prePaintScreen( ScreenPrePaintData& data, int time );
       virtual void paintWindow( EffectWindow* w, int mask, QRegion region, WindowPaintData& data );
       virtual void windowUserMovedResized( EffectWindow *w, bool first, bool last );
   private:
       bool m_active;
       EffectWindow* m_resizeWindow;
       QRegion m_originalWindowRect;
   };

}

  1. endif

We see that there are three member variables. The boolean is used to indicate if there is a window being resized, that is if we have to do some painting. The EffectWindow is a pointer on the window being resized and the QRegion stores the windows's geometry before the start of resizing. So now we can have a look at the implementation. I will split the code in small parts and explain the code. So first let's look at the includes: 

  1. include "resize.h"
  1. ifdef KWIN_HAVE_OPENGL_COMPOSITING
  2. include <GL/gl.h>
  3. endif
  4. ifdef KWIN_HAVE_XRENDER_COMPOSITING
  5. include <X11/Xlib.h>
  6. include <X11/extensions/Xrender.h>
  7. endif
  1. include <KColorScheme>

As our effect should support both XRender and OpenGL we have to include the headers for both. As it is possible that the effect is compiled on a system which does not support one of both we use ifdef. We can be sure that at least one of both is available or the effects wouldn't be compiled at all. If you write an OpenGL only effect you do not have to bother about such things. Also if you only use KWin's high level API you don't need to include those headers. But we want to paint on the screen using OpenGL or XRender directly. So let's have a look at the next part: 

namespace KWin {

KWIN_EFFECT( resize, ResizeEffect )

ResizeEffect::ResizeEffect()

   : m_active( false )                                                                                                                                    
   , m_resizeWindow( 0 )                                                                                                                                         
   {                                                                                                                           
   reconfigure( ReconfigureAll );                             
   }                                                                                                                           

ResizeEffect::~ResizeEffect()

   {                                                                                         
   }

Here we see the use of a macro. This has to be included or your effect will not load (it took me ten minutes to notice I forgot to add this line). The first value is the second part of X-KDE-PluginInfo-Name - I told you we will need it again. The second value is the class name. Following is constructor and deconstructor. So let's look at the pre paint screen stage: 

void ResizeEffect::prePaintScreen( ScreenPrePaintData& data, int time )

   {                                                                                                                           
   if( m_active )                                             
       {                                                                                                                       
       data.mask |= PAINT_SCREEN_WITH_TRANSFORMED_WINDOWS;           
       }                                                                                                                       
   effects->prePaintScreen( data, time );                                                                                                                     
   }

Here we extend the mask to say that we paint the screen with transformed windows when the effect is active. That's not completely true - we don't transform a window. But this flag indicates that the complete screen will be repainted, so we eliminate the risk of artefacts. We could also track the parts which have to be repainted manually but this would probably be more work for the CPU than the complete repaint for the GPU. At this point we see the chaining for the first time. The effects->prePaintScreen( data, time ); will call the next effect in the chain. effects is a pointer on the EffectsHandler and a very useful helper. So now we start looking at the heart of the effect: 

void ResizeEffect::paintWindow( EffectWindow* w, int mask, QRegion region, WindowPaintData& data )

   {                                                                                                                           
   effects->paintWindow( w, mask, region, data );                                                   
   if( m_active && w == m_resizeWindow )                                                                                                                 
       {                                                                                                                       
       QRegion intersection = m_originalWindowRect.intersected( w->geometry() );                                                                                                                
       QRegion paintRegion = m_originalWindowRect.united( w->geometry() ).subtracted( intersection );                                                                                                                                                          
       float alpha = 0.8f;                                                                                                                                              
       QColor color = KColorScheme( QPalette::Normal, KColorScheme::Selection ).background().color();

We first continue the paint window effect chain - this will paint the window on the screen. Now we check if we are in resizing mode (m_active) and if the currently painted window is the window which is repainted. In that case we calculate the region which has to be painted. We just subtract the intersection of current geometry with saved geometry from the union of those two. The next two lines are for the color definition. We use the background color of a selection with 80 % opacity. Now we have to do a little bit OpenGL. In most effects where you just transform windows you don't have to write OpenGL at all. There is a nice high level API which allows you to translate, scale and rotate windows or the complete screen. Also transforming single quads can be completely done without knowing anything about OpenGL. 

  1. ifdef KWIN_HAVE_OPENGL_COMPOSITING
       if( effects->compositingType() == OpenGLCompositing)                                  
           {                                                                                                                   
           glPushAttrib( GL_CURRENT_BIT | GL_ENABLE_BIT );                                                                                                                                    
           glEnable( GL_BLEND );                              
           glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );                                                                                                                               
           glColor4f( color.red() / 255.0f, color.green() / 255.0f, color.blue() / 255.0f, alpha );                                                                                                                      
           glBegin( GL_QUADS );                               
           foreach( const QRect &r, paintRegion.rects() )                                                                                                  
               {                                                                                                               
               glVertex2i( r.x(), r.y() );                                                                                                                                                  
               glVertex2i( r.x() + r.width(), r.y() );           
               glVertex2i( r.x() + r.width(), r.y() + r.height() );                                
               glVertex2i( r.x(), r.y() + r.height() );          
               }                                                                                                               
           glEnd();                                                                        
           glPopAttrib();                                                                  
           }                                                                                                                   
  1. endif

We check if KWin uses OpenGL as a backend. We enable blending in the OpenGL state machine (needed to have translucent colors) and set the color for our rects. OpenGL clamps colors in the range [0,1] that's why we can't use the values from QColor directly. Last but not least we just paint one quads for each rect of our regin. Now just the XRender part is missing.

  1. ifdef KWIN_HAVE_XRENDER_COMPOSITING
       if( effects->compositingType() == XRenderCompositing)                                 
           {                                                                                                                   
           XRenderColor col;                                                                      
           col.alpha = int( alpha * 0xffff );                                                                 
           col.red = int( alpha * 0xffff * color.red() / 255 );                       
           col.green = int( alpha * 0xffff * color.green() / 255 );                   
           col.blue= int( alpha * 0xffff * color.blue() / 255 );                      
           foreach( const QRect &r, paintRegion.rects() )                                                                                                  
               XRenderFillRectangle( display(), PictOpOver, effects->xrenderBufferPicture(),                                                                                                                          
                   &col, r.x(), r.y(), r.width(), r.height());
           }
  1. endif
       }
   }

This does the same as the OpenGL part just with XRender. Last but not least we have to track the window resizing: 

void ResizeEffect::windowUserMovedResized( EffectWindow* w, bool first, bool last )

   {
   if( first && last )
       {
       // not interested in maximized
       return;
       }
   if( first && w->isUserResize() && !w->isUserMove() )
       {
       m_active = true;
       m_resizeWindow = w;
       m_originalWindowRect = w->geometry();
       w->addRepaintFull();
       }
   if( m_active && w == m_resizeWindow && last )
       {
       m_active = false;
       m_resizeWindow = NULL;
       effects->addRepaintFull();
       }
   }

} // namespace

So and that's all. When a resize event is started we activate the effect and trigger a repaint of the window (probably not needed, but doesn't hurt). And when the resizing is finished we deactivate the effect and trigger another repaint of the complete screen just to make sure that there are no artefacts left. The CMakeLists.txt could just be taken from any other effect and be adjusted. So here's the example: 

#######################################
# Effect

# Source files
set( kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources}
    resize/resize.cpp
    )

# .desktop files
install( FILES
    resize/resize.desktop
    DESTINATION ${SERVICES_INSTALL_DIR}/kwin )

#######################################
# Config

Now you can compile and try your new effect.