kwin Library API Documentation

layers.cpp

00001 /*****************************************************************
00002  KWin - the KDE window manager
00003  This file is part of the KDE project.
00004 
00005 Copyright (C) 1999, 2000 Matthias Ettrich <ettrich@kde.org>
00006 Copyright (C) 2003 Lubos Lunak <l.lunak@kde.org>
00007 
00008 You can Freely distribute this program under the GNU General Public
00009 License. See the file "COPYING" for the exact licensing terms.
00010 ******************************************************************/
00011 
00012 // SELI zmenit doc
00013 
00014 /*
00015 
00016  This file contains things relevant to stacking order and layers.
00017 
00018  Design:
00019 
00020  Normal unconstrained stacking order, as requested by the user (by clicking
00021  on windows to raise them, etc.), is in Workspace::unconstrained_stacking_order.
00022  That list shouldn't be used at all, except for building
00023  Workspace::stacking_order. The building is done
00024  in Workspace::constrainedStackingOrder(). Only Workspace::stackingOrder() should
00025  be used to get the stacking order, because it also checks the stacking order
00026  is up to date.
00027  All clients are also stored in Workspace::clients (except for isDesktop() clients,
00028  as those are very special, and are stored in Workspace::desktops), in the order
00029  the clients were created.
00030 
00031  Every window has one layer assigned in which it is. There are 6 layers,
00032  from bottom : DesktopLayer, BelowLayer, NormalLayer, DockLayer, AboveLayer
00033  and ActiveLayer (see also NETWM sect.7.10.). The layer a window is in depends
00034  on the window type, and on other things like whether the window is active.
00035 
00036  NET::Splash clients belong to the Normal layer. NET::TopMenu clients
00037  belong to Dock layer. Clients that are both NET::Dock and NET::KeepBelow
00038  are in the Normal layer in order to keep the 'allow window to cover
00039  the panel' Kicker setting to work as intended (this may look like a slight
00040  spec violation, but a) I have no better idea, b) the spec allows adjusting
00041  the stacking order if the WM thinks it's a good idea . We put all
00042  NET::KeepAbove above all Docks too, even though the spec suggests putting
00043  them in the same layer.
00044 
00045  Most transients are in the same layer as their mainwindow,
00046  see Workspace::constrainedStackingOrder(), they may also be in higher layers, but
00047  they should never be below their mainwindow.
00048 
00049  When some client attribute changes (above/below flag, transiency...),
00050  Workspace::updateClientLayer() should be called in order to make
00051  sure it's moved to the appropriate layer ClientList if needed.
00052 
00053  Currently the things that affect client in which layer a client
00054  belongs: KeepAbove/Keep Below flags, window type, fullscreen
00055  state and whether the client is active, mainclient (transiency).
00056 
00057  Make sure updateStackingOrder() is called in order to make
00058  Workspace::stackingOrder() up to date and propagated to the world.
00059  Using Workspace::blockStackingUpdates() (or the StackingUpdatesBlocker
00060  helper class) it's possible to temporarily disable updates
00061  and the stacking order will be updated once after it's allowed again.
00062 
00063 */
00064 
00065 #include <assert.h>
00066 
00067 #include "utils.h"
00068 #include "client.h"
00069 #include "workspace.h"
00070 #include "tabbox.h"
00071 #include "popupinfo.h"
00072 #include "group.h"
00073 #include <kdebug.h>
00074 
00075 namespace KWinInternal
00076 {
00077 
00078 //*******************************
00079 // Workspace
00080 //*******************************
00081 
00082 void Workspace::updateClientLayer( Client* c )
00083     {
00084     if( c == NULL )
00085         return;
00086     if( c->layer() == c->belongsToLayer())
00087         return;
00088     StackingUpdatesBlocker blocker( this );
00089     c->invalidateLayer(); // invalidate, will be updated when doing restacking
00090     for( ClientList::ConstIterator it = c->transients().begin();
00091          it != c->transients().end();
00092          ++it )
00093         updateClientLayer( *it );
00094     }
00095 
00096 void Workspace::updateStackingOrder( bool propagate_new_clients )
00097     {
00098     if( block_stacking_updates > 0 )
00099         {
00100         blocked_propagating_new_clients |= propagate_new_clients;
00101         return;
00102         }
00103     ClientList new_stacking_order = constrainedStackingOrder();
00104     bool changed = ( new_stacking_order != stacking_order );
00105     stacking_order = new_stacking_order;
00106 #if 0
00107     kdDebug() << "stacking:" << changed << endl;
00108     if( changed || propagate_new_clients )
00109         {
00110         for( ClientList::ConstIterator it = stacking_order.begin();
00111              it != stacking_order.end();
00112              ++it )
00113             kdDebug() << (void*)(*it) << *it << endl;
00114         }
00115 #endif
00116     if( changed || propagate_new_clients )
00117         propagateClients( propagate_new_clients );
00118     }
00119 
00124 void Workspace::propagateClients( bool propagate_new_clients )
00125     {
00126     Window *cl; // MW we should not assume WId and Window to be compatible
00127                                 // when passig pointers around.
00128 
00129     // restack the windows according to the stacking order
00130     Window* new_stack = new Window[ stacking_order.count() + 2 ];
00131     int pos = 0;
00132     // Stack all windows under the support window. The support window is
00133     // not used for anything (besides the NETWM property), and it's not shown,
00134     // but it was lowered after kwin startup. Stacking all clients below
00135     // it ensures that no client will be ever shown above override-redirect
00136     // windows (e.g. popups).
00137     new_stack[ pos++ ] = supportWindow->winId();
00138     int topmenu_space_pos = 1; // not 0, that's supportWindow !!!
00139     for( ClientList::ConstIterator it = stacking_order.fromLast();
00140          it != stacking_order.end();
00141          --it )
00142         {
00143         new_stack[ pos++ ] = (*it)->frameId();
00144         if( (*it)->isTopMenu())
00145             topmenu_space_pos = pos;
00146         }
00147     if( topmenu_space != NULL )
00148         { // make sure the topmenu space is below all topmenus, if there are any
00149         for( int i = pos;
00150              i > topmenu_space_pos;
00151              --i )
00152             new_stack[ i ] = new_stack[ i - 1 ];
00153         new_stack[ topmenu_space_pos ] = topmenu_space->winId();
00154         ++pos;
00155         }
00156     // TODO isn't it too inefficient to restart always all clients?
00157     // TODO don't restack not visible windows?
00158     assert( new_stack[ 0 ] = supportWindow->winId());
00159     XRestackWindows(qt_xdisplay(), new_stack, pos);
00160     delete [] new_stack;
00161 
00162     if ( propagate_new_clients )
00163         {
00164         cl = new Window[ desktops.count() + clients.count()];
00165         pos = 0;
00166     // TODO this is still not completely in the map order
00167         for ( ClientList::ConstIterator it = desktops.begin(); it != desktops.end(); ++it )
00168             cl[pos++] =  (*it)->window();
00169         for ( ClientList::ConstIterator it = clients.begin(); it != clients.end(); ++it )
00170             cl[pos++] =  (*it)->window();
00171         rootInfo->setClientList( cl, pos );
00172         delete [] cl;
00173         }
00174 
00175     cl = new Window[ stacking_order.count()];
00176     pos = 0;
00177     for ( ClientList::ConstIterator it = stacking_order.begin(); it != stacking_order.end(); ++it)
00178         cl[pos++] =  (*it)->window();
00179     rootInfo->setClientListStacking( cl, pos );
00180     delete [] cl;
00181 
00182 #if 0 // not necessary anymore?
00183     if ( tab_box->isVisible() )
00184         tab_box->raise();
00185 
00186     if ( popupinfo->isVisible() )
00187         popupinfo->raise();
00188 
00189     raiseElectricBorders();
00190 #endif
00191     }
00192 
00193 
00199 // TODO misleading name for this method
00200 Client* Workspace::topClientOnDesktop( int desktop, bool unconstrained ) const
00201     {
00202 // TODO    Q_ASSERT( block_stacking_updates == 0 );
00203     ClientList::ConstIterator begin, end;
00204     if( !unconstrained )
00205         {
00206         begin = stacking_order.fromLast();
00207         end = stacking_order.end();
00208         }
00209     else
00210         {
00211         begin = unconstrained_stacking_order.fromLast();
00212         end = unconstrained_stacking_order.end();
00213         }
00214     for( ClientList::ConstIterator it = begin;
00215         it != end;
00216         --it )
00217         {
00218         if ( (*it)->isOnDesktop( desktop ) && !(*it)->isSpecialWindow()
00219             && (*it)->isShown( false ) && (*it)->wantsTabFocus())
00220             return *it;
00221         }
00222     return 0;
00223     }
00224 
00225 Client* Workspace::findDesktop( bool topmost, int desktop ) const
00226     {
00227 // TODO    Q_ASSERT( block_stacking_updates == 0 );
00228     if( topmost )
00229         {
00230         for ( ClientList::ConstIterator it = stacking_order.fromLast(); it != stacking_order.end(); --it)
00231             {
00232             if ( (*it)->isOnDesktop( desktop ) && (*it)->isDesktop()
00233                 && (*it)->isShown( true ))
00234                 return *it;
00235             }
00236         }
00237     else // bottom-most
00238         {
00239         for ( ClientList::ConstIterator it = stacking_order.begin(); it != stacking_order.end(); ++it)
00240             {
00241             if ( (*it)->isOnDesktop( desktop ) && (*it)->isDesktop()
00242                 && (*it)->isShown( true ))
00243                 return *it;
00244             }
00245         }
00246     return NULL;
00247     }
00248 
00249 void Workspace::raiseOrLowerClient( Client *c)
00250     {
00251     if (!c) return;
00252     Client* topmost = NULL;
00253 // TODO    Q_ASSERT( block_stacking_updates == 0 );
00254     if ( most_recently_raised && stacking_order.contains( most_recently_raised ) &&
00255          most_recently_raised->isShown( true ) && c->isOnCurrentDesktop())
00256         topmost = most_recently_raised;
00257     else
00258         topmost = topClientOnDesktop( c->isOnAllDesktops() ? currentDesktop() : c->desktop());
00259 
00260     if( c == topmost)
00261         lowerClient(c);
00262     else
00263         raiseClient(c);
00264     }
00265 
00266 
00267 void Workspace::lowerClient( Client* c )
00268     {
00269     if ( !c )
00270         return;
00271     if( c->isTopMenu())
00272         return;
00273 
00274     c->cancelAutoRaise();
00275 
00276     StackingUpdatesBlocker blocker( this );
00277 
00278     unconstrained_stacking_order.remove( c );
00279     unconstrained_stacking_order.prepend( c );
00280     if( c->isTransient())
00281         {
00282         // lower also mainclients, in their reversed stacking order
00283         ClientList mainclients = ensureStackingOrder( c->mainClients());
00284         for( ClientList::ConstIterator it = mainclients.fromLast();
00285              it != mainclients.end();
00286              ++it )
00287             lowerClient( *it );
00288         }
00289 
00290     if ( c == most_recently_raised )
00291         most_recently_raised = 0;
00292     }
00293 
00294 void Workspace::lowerClientWithinApplication( Client* c )
00295     {
00296     if ( !c )
00297         return;
00298     if( c->isTopMenu())
00299         return;
00300 
00301     c->cancelAutoRaise();
00302 
00303     StackingUpdatesBlocker blocker( this );
00304 
00305     unconstrained_stacking_order.remove( c );
00306     bool lowered = false;
00307     // first try to put it below the bottom-most window of the application
00308     for( ClientList::Iterator it = unconstrained_stacking_order.begin();
00309          it != unconstrained_stacking_order.end();
00310          ++it )
00311         if( Client::belongToSameApplication( *it, c ))
00312             {
00313             unconstrained_stacking_order.insert( it, c );
00314             lowered = true;
00315             break;
00316             }
00317     if( !lowered )
00318         unconstrained_stacking_order.prepend( c );
00319     // ignore mainwindows
00320     }
00321 
00322 void Workspace::raiseClient( Client* c )
00323     {
00324     if ( !c )
00325         return;
00326     if( c->isTopMenu())
00327         return;
00328 
00329     c->cancelAutoRaise();
00330 
00331     StackingUpdatesBlocker blocker( this );
00332 
00333     if( c->isTransient())
00334         {
00335         ClientList mainclients = ensureStackingOrder( c->mainClients());
00336         for( ClientList::ConstIterator it = mainclients.begin();
00337              it != mainclients.end();
00338              ++it )
00339             raiseClient( *it );
00340         }
00341 
00342     unconstrained_stacking_order.remove( c );
00343     unconstrained_stacking_order.append( c );
00344 
00345     if( !c->isSpecialWindow())
00346         most_recently_raised = c;
00347     }
00348 
00349 void Workspace::raiseClientWithinApplication( Client* c )
00350     {
00351     if ( !c )
00352         return;
00353     if( c->isTopMenu())
00354         return;
00355 
00356     c->cancelAutoRaise();
00357 
00358     StackingUpdatesBlocker blocker( this );
00359     // ignore mainwindows
00360     
00361     // first try to put it above the top-most window of the application
00362     for( ClientList::Iterator it = unconstrained_stacking_order.fromLast();
00363          it != unconstrained_stacking_order.end();
00364          --it )
00365         {
00366         if( *it == c ) // don't lower it just because it asked to be raised
00367             return;
00368         if( Client::belongToSameApplication( *it, c ))
00369             {
00370             unconstrained_stacking_order.remove( c );
00371             ++it; // insert after the found one
00372             unconstrained_stacking_order.insert( it, c );
00373             return;
00374             }
00375         }
00376     }
00377 
00378 void Workspace::raiseClientRequest( Client* c )
00379     {
00380     if( allowFullClientRaising( c ))
00381         raiseClient( c );
00382     else
00383         {
00384         raiseClientWithinApplication( c );
00385         c->demandAttention();
00386         }
00387     }
00388 
00389 void Workspace::lowerClientRequest( Client* c )
00390     {
00391     // If the client has support for all this focus stealing prevention stuff,
00392     // do only lowering within the application, as that's the more logical
00393     // variant of lowering when application requests it.
00394     // No demanding of attention here of course.
00395     if( c->hasUserTimeSupport())
00396         lowerClientWithinApplication( c );
00397     else
00398         lowerClient( c );
00399     }
00400 
00401 void Workspace::restackClientUnderActive( Client* c )
00402     {
00403     if( c->isTopMenu())
00404         return;
00405     if( !active_client || active_client == c )
00406         {
00407         raiseClient( c );
00408         return;
00409         }
00410 
00411     // put in the stacking order below _all_ windows belonging to the active application
00412     assert( unconstrained_stacking_order.contains( active_client ));
00413     for( ClientList::Iterator it = unconstrained_stacking_order.begin();
00414          it != unconstrained_stacking_order.end();
00415          ++it )
00416         { // TODO ignore topmenus?
00417         if( Client::belongToSameApplication( active_client, *it ))
00418             {
00419             if( *it != c )
00420                 {
00421                 unconstrained_stacking_order.remove( c );
00422                 unconstrained_stacking_order.insert( it, c );
00423                 }
00424             break;
00425             }
00426         }
00427     assert( unconstrained_stacking_order.contains( c ));
00428     if( c->wantsTabFocus() && focus_chain.contains( active_client ))
00429         {
00430         // also put in focus_chain after all windows belonging to the active application
00431         focus_chain.remove( c );
00432         for( ClientList::Iterator it = focus_chain.fromLast();
00433              it != focus_chain.end();
00434              --it )
00435             {
00436             if( Client::belongToSameApplication( active_client, *it ))
00437                 {
00438                 focus_chain.insert( it, c );
00439                 break;
00440                 }
00441             }
00442         }
00443     updateStackingOrder();
00444     }
00445 
00446 void Workspace::circulateDesktopApplications()
00447     {
00448     if ( desktops.count() > 1 )
00449         {
00450         bool change_active = activeClient()->isDesktop();
00451         raiseClient( findDesktop( false, currentDesktop()));
00452         if( change_active ) // if the previously topmost Desktop was active, activate this new one
00453             activateClient( findDesktop( true, currentDesktop()));
00454         }
00455     // if there's no active client, make desktop the active one
00456     if( desktops.count() > 0 && activeClient() == NULL && should_get_focus.count() == 0 )
00457         activateClient( findDesktop( true, currentDesktop()));
00458     }
00459 
00460 
00464 ClientList Workspace::constrainedStackingOrder()
00465     {
00466     ClientList layer[ NumLayers ];
00467 
00468 #if 0
00469     kdDebug() << "stacking1:" << endl;
00470 #endif
00471     // build the order from layers
00472     for( ClientList::ConstIterator it = unconstrained_stacking_order.begin();
00473          it != unconstrained_stacking_order.end();
00474          ++it )
00475         {
00476 #if 0
00477         kdDebug() << (void*)(*it) << *it << endl;
00478 #endif
00479         layer[ (*it)->layer() ].append( *it );
00480         }
00481     ClientList stacking;    
00482     for( Layer lay = FirstLayer;
00483          lay < NumLayers;
00484          ++lay )    
00485         stacking += layer[ lay ];
00486 #if 0
00487     kdDebug() << "stacking2:" << endl;
00488     for( ClientList::ConstIterator it = stacking.begin();
00489          it != stacking.end();
00490          ++it )
00491         kdDebug() << (void*)(*it) << *it << endl;
00492 #endif
00493     // now keep transients above their mainwindows
00494     // TODO this could(?) use some optimization
00495     for( ClientList::Iterator it = stacking.fromLast();
00496          it != stacking.end();
00497          )
00498         {
00499         if( !(*it)->isTransient())
00500             {
00501             --it;
00502             continue;
00503             }
00504         ClientList::Iterator it2 = stacking.end();
00505         if( (*it)->groupTransient())
00506             {
00507             if( (*it)->group()->members().count() > 0 )
00508                 { // find topmost client this one is transient for
00509                 for( it2 = stacking.fromLast();
00510                      it2 != stacking.end();
00511                      --it2 )
00512                     {
00513                     if( *it2 == *it )
00514                         {
00515                         it2 = stacking.end(); // don't reorder
00516                         break;
00517                         }
00518                     if( (*it2)->hasTransient( *it, true ) && keepTransientAbove( *it2, *it ))
00519                         break;
00520                     }
00521                 } // else it2 remains pointing at stacking.end()
00522             }
00523         else
00524             {
00525             for( it2 = stacking.fromLast();
00526                  it2 != stacking.end();
00527                  --it2 )
00528                 {
00529                 if( *it2 == *it )
00530                     {
00531                     it2 = stacking.end(); // don't reorder
00532                     break;
00533                     }
00534                 if( *it2 == (*it)->transientFor() && keepTransientAbove( *it2, *it ))
00535                     break;
00536                 }
00537             }
00538 //        kdDebug() << "STACK:" << (*it) << ":" << ( it2 == stacking.end() ? ((Client*)0) : (*it2)) << endl;
00539         if( it2 == stacking.end())
00540             {
00541             --it;
00542             continue;
00543             }
00544         Client* current = *it;
00545         ClientList::Iterator remove_it = it;
00546         --it;
00547         stacking.remove( remove_it );
00548         if( !current->transients().isEmpty())  // this one now can be possibly above its transients,
00549             it = it2; // so go again higher in the stack order and possibly move those transients again
00550         ++it2; // insert after the mainwindow, it's ok if it2 is now stacking.end()
00551         stacking.insert( it2, current );
00552         }
00553 #if 0
00554     kdDebug() << "stacking3:" << endl;
00555     for( ClientList::ConstIterator it = stacking.begin();
00556          it != stacking.end();
00557          ++it )
00558         kdDebug() << (void*)(*it) << *it << endl;
00559     kdDebug() << "\n\n" << endl;
00560 #endif
00561     return stacking;
00562     }
00563 
00564 void Workspace::blockStackingUpdates( bool block )
00565     {
00566     if( block )
00567         {
00568         if( block_stacking_updates == 0 )
00569             blocked_propagating_new_clients = false;
00570         ++block_stacking_updates;
00571         }
00572     else // !block
00573         if( --block_stacking_updates == 0 )
00574             updateStackingOrder( blocked_propagating_new_clients );
00575     }
00576 
00577 // Ensure list is in stacking order
00578 ClientList Workspace::ensureStackingOrder( const ClientList& list ) const
00579     {
00580 // TODO    Q_ASSERT( block_stacking_updates == 0 );
00581     if( list.count() < 2 )
00582         return list;
00583     // TODO is this worth optimizing?
00584     ClientList result = list;
00585     for( ClientList::ConstIterator it = stacking_order.begin();
00586          it != stacking_order.end();
00587          ++it )
00588         if( result.remove( *it ) != 0 )
00589             result.append( *it );
00590     return result;
00591     }
00592 
00593 // check whether a transient should be actually kept above its mainwindow
00594 // there may be some special cases where this rule shouldn't be enfored
00595 bool Workspace::keepTransientAbove( const Client* mainwindow, const Client* transient )
00596     {
00597     // When topmenu's mainwindow becomes active, topmenu is raised and shown.
00598     // They also belong to the Dock layer. This makes them to be very high.
00599     // Therefore don't keep group transients above them, otherwise this would move
00600     // group transients way too high.
00601     if( mainwindow->isTopMenu() && transient->groupTransient())
00602         return false;
00603     return true;
00604     // #63223 - don't keep transients above docks, because the dock is kept high,
00605     // and e.g. dialogs for them would be too high too
00606     // TODO this doesn't really work - the transient should be raised after clicking
00607     // on the dock, but docks don't become active after clicking them
00608     if( mainwindow->isDock() && !mainwindow->keepBelow()
00609         && !mainwindow->isActive() && !transient->isActive()) // TODO !w->group()->isActive() ???
00610         return false;
00611     return true;
00612     }
00613 
00614 //*******************************
00615 // Client
00616 //*******************************
00617 
00618 void Client::restackWindow( Window /*above TODO */, int detail, NET::RequestSource source, bool send_event )
00619     {
00620     switch ( detail )
00621         {
00622         case Above:
00623         case TopIf:
00624             if( source == NET::FromTool )
00625                 workspace()->raiseClient( this );
00626             else
00627                 workspace()->raiseClientRequest( this );
00628             break;
00629         case Below:
00630         case BottomIf:
00631             if( source == NET::FromTool )
00632                 workspace()->lowerClient( this );
00633             else
00634                 workspace()->lowerClientRequest( this );
00635             break;
00636         case Opposite:
00637         default:
00638             break;
00639         }
00640     if( send_event )
00641         sendSyntheticConfigureNotify();
00642     }
00643     
00644 void Client::setKeepAbove( bool b )
00645     {
00646     if ( b == keepAbove() )
00647         return;
00648     setKeepBelow( false );
00649     keep_above = b;
00650     info->setState( b ? NET::KeepAbove : 0, NET::KeepAbove );
00651     // TODO emit a signal about the change to the style plugin
00652     workspace()->updateClientLayer( this );
00653     }
00654 
00655 void Client::setKeepBelow( bool b )
00656     {
00657     if ( b == keepBelow() )
00658         return;
00659     setKeepAbove( false );
00660     keep_below = b;
00661     info->setState( b ? NET::KeepBelow : 0, NET::KeepBelow );
00662     workspace()->updateClientLayer( this );
00663     }
00664 
00665 Layer Client::layer() const
00666     {
00667     if( in_layer == UnknownLayer )
00668         const_cast< Client* >( this )->in_layer = belongsToLayer();
00669     return in_layer;
00670     }
00671 
00672 Layer Client::belongsToLayer() const
00673     {
00674     if( isDesktop())
00675         return DesktopLayer;
00676     if( isSplash())         // no damn annoying splashscreens
00677         return NormalLayer; // getting in the way of everything else
00678     if( isDock() && keepBelow())
00679         // slight hack for the 'allow window to cover panel' Kicker setting
00680         // don't move keepbelow docks below normal window, but only to the same
00681         // layer, so that both may be raised to cover the other
00682         return NormalLayer;
00683     if( keepBelow())
00684         return BelowLayer;
00685     if( isDock() && !keepBelow())
00686         return DockLayer;
00687     if( isTopMenu())
00688         return DockLayer;
00689     // only raise fullscreen above docks if it's the topmost window in unconstrained stacking order,
00690     // i.e. the window set to be topmost by the user
00691     bool raise_special_active_windows = ( workspace()->topClientOnDesktop( desktop(), true ) == this );
00692     if( keepAbove())
00693         return AboveLayer;
00694     if( isFullScreen() && workspace()->activeClient() != NULL
00695         && ( workspace()->activeClient() == this || this->hasTransient( workspace()->activeClient(), true ))
00696         && raise_special_active_windows )
00697         return ActiveLayer;
00698     return NormalLayer;
00699     }
00700 
00701 } // namespace
KDE Logo
This file is part of the documentation for kwin Library Version 3.2.3.
Documentation copyright © 1996-2004 the KDE developers.
Generated on Mon Aug 16 14:44:22 2004 by doxygen 1.3.6 written by Dimitri van Heesch, © 1997-2003