File indexing completed on 2024-04-21 15:07:54

0001 /*
0002  * Copyright 1999 by Martin R. Jones <mjones@kde.org>
0003  * Copyright 2010 by Stefan Böhmann <kde@hilefoks.org>
0004  *
0005  * This program is free software; you can redistribute it and/or modify
0006  * it under the terms of the GNU General Public License as published by
0007  * the Free Software Foundation; either version 2 of the License, or
0008  * (at your option) any later version.
0009  *
0010  * This program is distributed in the hope that it will be useful,
0011  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0012  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013  * GNU General Public License for more details.
0014  *
0015  * You should have received a copy of the GNU General Public License
0016  * along with this program; if not, write to the Free Software
0017  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
0018  */
0019 #include "amor.h"
0020 #include "amorpixmapmanager.h"
0021 #include "amorbubble.h"
0022 #include "amorwidget.h"
0023 #include "amordialog.h"
0024 #include "version.h"
0025 #include "amorthememanager.h"
0026 #include "amoradaptor.h"
0027 #include "amor_debug.h"
0028 
0029 #include <stdlib.h>
0030 #include <unistd.h>
0031 #include <time.h>
0032 
0033 #include <QDBusConnection>
0034 #include <QTimer>
0035 #include <QCursor>
0036 #include <QStandardPaths>
0037 #include <QApplication>
0038 #include <QMenu>
0039 #include <QRandomGenerator>
0040 
0041 #include <KLocalizedString>
0042 #include <KMessageBox>
0043 #include <KStartupInfo>
0044 #include <KWindowInfo>
0045 #include <KHelpMenu>
0046 #include <KAboutData>
0047 
0048 #include <xcb/xcb.h>
0049 #include <QX11Info>
0050 
0051 // #define DEBUG_AMOR
0052 
0053 #define SLEEP_TIMEOUT   180     // Animation sleeps after SLEEP_TIMEOUT seconds
0054                                 // of mouse inactivity.
0055 #define TIPS_FILE       "tips-en"  // Display tips in TIP_FILE-LANG, e.g "tips-en" (this is then translated using i18n() at runtime)
0056 #define TIP_FREQUENCY   20      // Frequency tips are displayed small == more often.
0057 
0058 #define BUBBLE_TIME_STEP 250
0059 
0060 // Standard animation groups
0061 #define ANIM_BASE       "Base"
0062 #define ANIM_NORMAL     "Sequences"
0063 #define ANIM_FOCUS      "Focus"
0064 #define ANIM_BLUR       "Blur"
0065 #define ANIM_DESTROY    "Destroy"
0066 #define ANIM_SLEEP      "Sleep"
0067 #define ANIM_WAKE       "Wake"
0068 
0069 
0070 
0071 Amor::Amor()
0072   : mAmor( 0 ),
0073     mBubble( 0 ),
0074     mForceHideAmorWidget( false )
0075 {
0076     new AmorAdaptor( this );
0077     QDBusConnection::sessionBus().registerObject( QLatin1String( "/Amor" ), this );
0078 
0079     if( !readConfig() ) {
0080         exit(0);
0081     }
0082 
0083     mTargetWin   = 0;
0084     mNextTarget  = 0;
0085     mMenu        = 0;
0086     mCurrAnim    = mBaseAnim;
0087     mPosition    = -1;
0088     mState       = Normal;
0089 
0090     mWin = KWindowSystem::self();
0091     mX11Win = KX11Extras::self();
0092     connect(mWin, &KWindowSystem::activeWindowChanged, this, &Amor::slotWindowActivate);
0093     connect(mWin, &KWindowSystem::windowRemoved, this, &Amor::slotWindowRemove);
0094     connect(mWin, &KWindowSystem::stackingOrderChanged, this, &Amor::slotStackingChanged);
0095     connect(mWin, QOverload<WId,NET::Properties,NET::Properties2>::of(&KWindowSystem::windowChanged),
0096             this, &Amor::slotWindowChange);
0097     connect(mX11Win, &KX11Extras::currentDesktopChanged, this, &Amor::slotDesktopChange);
0098 
0099     mAmor = new AmorWidget;
0100     connect( mAmor, SIGNAL(mouseClicked(QPoint)), SLOT(slotMouseClicked(QPoint)) );
0101     connect( mAmor, SIGNAL(dragged(QPoint,bool)), SLOT(slotWidgetDragged(QPoint,bool)) );
0102     mAmor->resize(mTheme.maximumSize());
0103 
0104     mTimer = new QTimer( this );
0105     connect( mTimer, SIGNAL(timeout()), SLOT(slotTimeout()) );
0106 
0107     mStackTimer = new QTimer( this );
0108     connect( mStackTimer, SIGNAL(timeout()), SLOT(restack()) );
0109 
0110     mBubbleTimer = new QTimer( this );
0111     connect( mBubbleTimer, SIGNAL(timeout()), SLOT(slotBubbleTimeout()) );
0112 
0113     std::time( &mActiveTime );
0114     mCursPos = QCursor::pos();
0115     mCursorTimer = new QTimer( this );
0116     connect( mCursorTimer, SIGNAL(timeout()), SLOT(slotCursorTimeout()) );
0117     mCursorTimer->start( 500 );
0118 
0119     mNextTarget = mWin->activeWindow();
0120     selectAnimation( Focus );
0121     mTimer->setSingleShot( true );
0122     mTimer->start( 0 );
0123 
0124     if( !QDBusConnection::sessionBus().connect( QStringLiteral( "org.freedesktop.ScreenSaver" ), QStringLiteral( "/ScreenSaver" ), QStringLiteral( "org.freedesktop.ScreenSaver" ),
0125             QStringLiteral( "ActiveChanged" ), this, SLOT(screenSaverStatusChanged(bool)) ) )
0126     {
0127         qCDebug(AMOR_LOG) << "Could not attach DBus signal: org.freedesktop.ScreenSaver.ActiveChanged()";
0128     }
0129 
0130     KStartupInfo::appStarted();
0131 }
0132 
0133 
0134 Amor::~Amor()
0135 {
0136     delete mMenu;
0137     delete mAmor;
0138     delete mBubble;
0139 }
0140 
0141 
0142 void Amor::screenSaverStatusChanged( bool active )
0143 {
0144     if( active ) {
0145         screenSaverStarted();
0146     }
0147     else {
0148         screenSaverStopped();
0149     }
0150 }
0151 
0152 
0153 void Amor::screenSaverStopped()
0154 {
0155     mAmor->show();
0156     mForceHideAmorWidget = false;
0157 
0158     mTimer->setSingleShot( true );
0159     mTimer->start( 0 );
0160 }
0161 
0162 
0163 void Amor::screenSaverStarted()
0164 {
0165     mAmor->hide();
0166     mTimer->stop();
0167     mForceHideAmorWidget = true;
0168 
0169     // GP: hide the bubble (if there's any) leaving any current message in the queue
0170     hideBubble();
0171 }
0172 
0173 
0174 void Amor::showTip(const QString &tip)
0175 {
0176     if( mTipsQueue.count() < 5 && !mForceHideAmorWidget ) { // start dropping tips if the queue is too long
0177         mTipsQueue.enqueue( QueueItem( QueueItem::Tip, tip ) );
0178     }
0179 
0180     if( mState == Sleeping ) {
0181         selectAnimation( Waking );        // Set waking immediatedly
0182         mTimer->setSingleShot( true );
0183         mTimer->start( 0 );
0184     }
0185 }
0186 
0187 
0188 void Amor::showMessage( const QString &message , int msec )
0189 {
0190     // FIXME: What should be done about messages and tips while the screensaver is on?
0191     if( mForceHideAmorWidget ) {
0192         return; // do not show messages sent while in the screensaver
0193     }
0194 
0195     mTipsQueue.enqueue( QueueItem( QueueItem::Talk, message, msec ) );
0196 
0197     if( mState == Sleeping ) {
0198         selectAnimation( Waking );      // Set waking immediatedly
0199         mTimer->setSingleShot( true );
0200         mTimer->start( 0 );
0201     }
0202 }
0203 
0204 
0205 void Amor::reset()
0206 {
0207     hideBubble();
0208     mAmor->setPixmap( 0L ); // get rid of your old copy of the pixmap
0209 
0210     AmorPixmapManager::manager()->reset();
0211     mTips.reset();
0212 
0213     readConfig();
0214 
0215     mCurrAnim = mBaseAnim;
0216     mPosition = mCurrAnim->hotspot().x();
0217     mState = Normal;
0218 
0219     mAmor->resize( mTheme.maximumSize() );
0220     mCurrAnim->reset();
0221 
0222     mTimer->setSingleShot( true );
0223     mTimer->start( 0 );
0224 }
0225 
0226 
0227 bool Amor::readConfig()
0228 {
0229     // Read user preferences
0230     mConfig.read();
0231 
0232     if( mConfig.mTips ) {
0233         mTips.setFile(QLatin1String( TIPS_FILE ) );
0234     }
0235 
0236     // Select a random theme if user requested it
0237     if( mConfig.mRandomTheme ) {
0238         QStringList files;
0239         // Store relative paths into files to avoid storing absolute pathnames.
0240         const QStringList dirs = QStandardPaths::standardLocations(QStandardPaths::AppDataLocation);;
0241         for (const QString& dir : dirs) {
0242             const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*rc"));
0243             for (const QString& file : fileNames) {
0244                 files.append(dir + QLatin1Char('/') + file);
0245             }
0246         }
0247         if (files.isEmpty()) {
0248             return false;
0249         }
0250         const int randomTheme = QRandomGenerator::global()->bounded(files.count());
0251         mConfig.mTheme = files.at(randomTheme);
0252     }
0253 
0254     // read selected theme
0255     if( !mTheme.setTheme( mConfig.mTheme ) ) {
0256         KMessageBox::error( 0, i18nc( "@info:status", "Error reading theme: %1", mConfig.mTheme ) );
0257         return false;
0258     }
0259 
0260     if( !mTheme.isStatic() ) {
0261         const char *groups[] = { ANIM_BASE, ANIM_NORMAL, ANIM_FOCUS, ANIM_BLUR, ANIM_DESTROY, ANIM_SLEEP, ANIM_WAKE, 0 };
0262 
0263         // Read all the standard animation groups
0264         for(int i = 0; groups[i]; ++i) {
0265             if( !mTheme.readGroup(QLatin1String( groups[i] ) ) ) {
0266                 KMessageBox::error( 0, i18nc( "@info:status", "Error reading group: %1", QLatin1String( groups[i] ) ) );
0267                 return false;
0268             }
0269         }
0270     }
0271     else {
0272         if( !mTheme.readGroup(QLatin1String( ANIM_BASE ) ) ) {
0273             KMessageBox::error( 0, i18nc( "@info:status", "Error reading group: %1", QLatin1String( ANIM_BASE ) ) );
0274             return false;
0275         }
0276     }
0277 
0278     // Get the base animation
0279     mBaseAnim = mTheme.random(QLatin1String( ANIM_BASE ) );
0280 
0281     return true;
0282 }
0283 
0284 
0285 void Amor::showBubble()
0286 {
0287     if( !mTipsQueue.isEmpty() ) {
0288         if( !mBubble ) {
0289             mBubble = new AmorBubble;
0290         }
0291 
0292         mBubble->setOrigin( mAmor->x()+mAmor->width()/2, mAmor->y()+mAmor->height()/2 );
0293         mBubble->setMessage( mTipsQueue.head().text() );
0294 
0295         // mBubbleTimer->start(mTipsQueue.head().time(), true);
0296         mBubbleTimer->setSingleShot(true);
0297         mBubbleTimer->start(BUBBLE_TIME_STEP);
0298     }
0299 }
0300 
0301 
0302 void Amor::hideBubble(bool forceDequeue)
0303 {
0304     if( mBubble ) {
0305         // GP: stop mBubbleTimer to avoid deleting the first element, just in case we are changing windows
0306         // or something before the tip was shown long enough
0307         mBubbleTimer->stop();
0308 
0309         // GP: the first message on the queue should be taken off for a
0310         // number of reasons: a) forceDequeue == true, only when called
0311         // from slotBubbleTimeout; b) the bubble is not visible ; c)
0312         // the bubble is visible, but there's Tip being displayed. The
0313         // latter is to keep backwards compatibility and because
0314         // carrying around a tip bubble when switching windows quickly is really
0315         // annoyying
0316         if( forceDequeue || !mBubble->isVisible() || ( mTipsQueue.head().type() == QueueItem::Tip ) ) {
0317             /* there's always an item in the queue here */
0318             mTipsQueue.dequeue();
0319         }
0320 
0321         delete mBubble;
0322         mBubble = 0;
0323     }
0324 }
0325 
0326 
0327 void Amor::selectAnimation(State state)
0328 {
0329     bool changedLocation = true;
0330     AmorAnimation *oldAnim = mCurrAnim;
0331 
0332     switch( state ) {
0333     case Blur:
0334         hideBubble();
0335         mCurrAnim = mTheme.random(QLatin1String( ANIM_BLUR ) );
0336         mState = Focus;
0337         break;
0338 
0339     case Focus:
0340         hideBubble();
0341         mCurrAnim = mTheme.random(QLatin1String( ANIM_FOCUS ) );
0342         if( oldAnim != mCurrAnim ) {
0343             mCurrAnim->reset();
0344         }
0345 
0346         mTargetWin = mNextTarget;
0347 
0348         if( mTargetWin != XCB_NONE ) {
0349             KWindowInfo windowInfo( mTargetWin, NET::WMFrameExtents | NET::WMState );
0350             mTargetRect = windowInfo.frameGeometry();
0351 
0352             // if the animation falls outside of the working area,
0353             // then relocate it so that is inside the desktop again
0354             QRect desktopArea = mWin->workArea(KWindowSystem::currentDesktop());
0355             KWindowSystem::setOnDesktop(mAmor->winId(), KWindowSystem::currentDesktop());
0356 
0357             bool fitsInWorkArea = mTargetRect.y() - mCurrAnim->hotspot().y() + mConfig.mOffset < desktopArea.y();
0358             if( windowInfo.hasState(NET::MaxVert) || fitsInWorkArea ) {
0359                 if( mInDesktopBottom ) {
0360                     changedLocation = false;
0361                 }
0362 
0363                 // relocate the animation at the bottom of the screen
0364                 mTargetRect = QRect( desktopArea.x(), desktopArea.y() + desktopArea.height(), desktopArea.width(), 0 );
0365 
0366                 // we'll relocate the animation in the desktop
0367                 // frame, so do not add the offset to its vertical position
0368                 mInDesktopBottom = true;
0369             }
0370             else {
0371                 mInDesktopBottom = false;
0372             }
0373 
0374             if( mTheme.isStatic() ) {
0375                 if( mConfig.mStaticPos < 0 ) {
0376                     mPosition = mTargetRect.width() + mConfig.mStaticPos;
0377                 }
0378                 else {
0379                     mPosition = mConfig.mStaticPos;
0380                 }
0381 
0382                 if( mPosition >= mTargetRect.width() ) {
0383                     mPosition = mTargetRect.width()-1;
0384                 }
0385                 else if( mPosition < 0 ) {
0386                     mPosition = 0;
0387                 }
0388             }
0389             else {
0390                 if( mCurrAnim->frame() ) {
0391                     if( mTargetRect.width() == mCurrAnim->frame()->width() ) {
0392                         mPosition = mCurrAnim->hotspot().x();
0393                     }
0394                     else if(changedLocation || mPosition < 0) {
0395                         mPosition = QRandomGenerator::global()->bounded( mTargetRect.width() - mCurrAnim->frame()->width() );
0396                         mPosition += mCurrAnim->hotspot().x();
0397                     }
0398                 }
0399                 else {
0400                     mPosition = mTargetRect.width()/2;
0401                 }
0402             }
0403         }
0404         else {
0405             // We don't want to do anything until a window comes into focus.
0406             mTimer->stop();
0407         }
0408         mAmor->hide();
0409         restack();
0410         mState = Normal;
0411         break;
0412 
0413     case Destroy:
0414         hideBubble();
0415         mCurrAnim = mTheme.random(QLatin1String( ANIM_DESTROY ) );
0416         mState = Focus;
0417         break;
0418 
0419     case Sleeping:
0420         mCurrAnim = mTheme.random(QLatin1String( ANIM_SLEEP ) );
0421         break;
0422 
0423     case Waking:
0424         mCurrAnim = mTheme.random(QLatin1String( ANIM_WAKE ) );
0425         mState = Normal;
0426         break;
0427 
0428     default:
0429         // Select a random normal animation if the current animation
0430         // is not the base, otherwise select the base.  This makes us
0431         // alternate between the base animation and a random animination.
0432         if( !mBubble && mCurrAnim == mBaseAnim ) {
0433             mCurrAnim = mTheme.random(QLatin1String( ANIM_NORMAL ) );
0434         }
0435         else {
0436             mCurrAnim = mBaseAnim;
0437         }
0438         break;
0439     }
0440 
0441     if( mCurrAnim->totalMovement() + mPosition > mTargetRect.width() || mCurrAnim->totalMovement() + mPosition < 0 ) {
0442         // The selected animation would end outside of this window's width
0443         // We could randomly select a different one, but I prefer to just
0444         // use the default animation.
0445         mCurrAnim = mBaseAnim;
0446     }
0447 
0448     if( changedLocation ) {
0449         mCurrAnim->reset();
0450     }
0451     else {
0452         mCurrAnim = oldAnim;
0453     }
0454 }
0455 
0456 
0457 void Amor::restack()
0458 {
0459     if( mTargetWin == XCB_NONE ) {
0460         return;
0461     }
0462 
0463     if( mConfig.mOnTop ) {
0464         // simply raise the widget to the top
0465         mAmor->raise();
0466         return;
0467     }
0468 
0469     xcb_window_t sibling = mTargetWin;
0470     xcb_window_t dw, parent = XCB_NONE, *wins;
0471 
0472     do {
0473         unsigned int nwins = 0;
0474 
0475         // We must use the target window's parent as our sibling.
0476         // Is there a faster way to get parent window than XQueryTree?
0477         const auto cookie = xcb_query_tree(QX11Info::connection(), sibling);
0478         const auto reply = xcb_query_tree_reply(QX11Info::connection(), cookie, nullptr);
0479         if (!reply) {
0480             return;
0481         }
0482 
0483         nwins = xcb_query_tree_children_length(reply);
0484         dw = reply->root;
0485         parent = reply->parent;
0486 
0487         free(reply);
0488 
0489         if( parent != XCB_NONE && parent != dw ) {
0490             sibling = parent;
0491         }
0492     } while( parent != XCB_NONE && parent != dw );
0493 
0494     // Set animation's stacking order to be above the window manager's
0495     // decoration of target window.
0496     const uint32_t values[] = { sibling, XCB_STACK_MODE_ABOVE };
0497     xcb_configure_window(QX11Info::connection(), mAmor->winId(),
0498                          XCB_CONFIG_WINDOW_SIBLING | XCB_CONFIG_WINDOW_STACK_MODE,
0499                          values);
0500 }
0501 
0502 
0503 void Amor::slotMouseClicked(const QPoint &pos)
0504 {
0505     bool restartTimer = mTimer->isActive();
0506 
0507     // Stop the animation while the menu is open.
0508     if( restartTimer ) {
0509         mTimer->stop();
0510     }
0511 
0512     if( !mMenu ) {
0513         KHelpMenu* help = new KHelpMenu(0, KAboutData::applicationData(), false );
0514         QMenu* helpMenu = help->menu();
0515 #ifdef __GNUC__
0516 #warning the following is kinda dirty and should be done by KHelpMenu::menu() I think. (hermier)
0517 #endif
0518         helpMenu->setIcon(QIcon::fromTheme(QStringLiteral("help-contents")));
0519         helpMenu->setTitle( i18nc( "@action:inmenu Amor", "&Help" ) );
0520 
0521         mMenu = new QMenu( 0 );
0522         mMenu->setTitle( QLatin1String( "Amor" ) ); // I really don't want this i18n'ed
0523         mMenu->addAction(QIcon::fromTheme(QStringLiteral("configure")), i18nc( "@action:inmenu Amor", "&Configure..." ), this, SLOT(slotConfigure()) );
0524         mMenu->addSeparator();
0525         mMenu->addMenu( helpMenu );
0526         mMenu->addAction(QIcon::fromTheme(QStringLiteral("application-exit")), i18nc( "@action:inmenu Amor", "&Quit" ), qApp, SLOT(quit()) );
0527     }
0528 
0529     mMenu->exec( pos );
0530 
0531     if( restartTimer ) {
0532         mTimer->setSingleShot( true );
0533         mTimer->start( 1000 );
0534     }
0535 }
0536 
0537 
0538 void Amor::slotCursorTimeout()
0539 {
0540     QPoint currPos = QCursor::pos();
0541     QPoint diff = currPos - mCursPos;
0542     std::time_t now = std::time( 0 );
0543 
0544     if( mForceHideAmorWidget ) {
0545         return; // we're hidden, do nothing
0546     }
0547 
0548     if( abs( diff.x() ) > 1 || abs( diff.y() ) > 1 ) {
0549         if( mState == Sleeping ) {
0550             // Set waking immediatedly
0551             selectAnimation( Waking );
0552         }
0553         mActiveTime = now;
0554         mCursPos = currPos;
0555     }
0556     else if( mState != Sleeping && now - mActiveTime > SLEEP_TIMEOUT ) {
0557         // GP: can't go to sleep if there are tips in the queue
0558         if( mTipsQueue.isEmpty() ) {
0559             mState = Sleeping;  // The next animation will become sleeping
0560         }
0561     }
0562 }
0563 
0564 
0565 void Amor::slotTimeout()
0566 {
0567     if( mForceHideAmorWidget ) {
0568         return;
0569     }
0570 
0571     if( !mTheme.isStatic() ) {
0572         mPosition += mCurrAnim->movement();
0573     }
0574 
0575     mAmor->setPixmap( mCurrAnim->frame() );
0576     mAmor->move( mTargetRect.x() + mPosition - mCurrAnim->hotspot().x(),
0577                  mTargetRect.y() - mCurrAnim->hotspot().y() + ( !mInDesktopBottom?mConfig.mOffset:0 ) );
0578 
0579     if( !mAmor->isVisible() ) {
0580         mAmor->show();
0581         restack();
0582     }
0583 
0584     if( mCurrAnim == mBaseAnim && mCurrAnim->validFrame() ) {
0585         // GP: Application tips/messages can be shown in any frame number; amor tips are
0586         // only displayed on the first frame of mBaseAnim (the old way of doing this).
0587         if( !mTipsQueue.isEmpty() && !mBubble &&  mConfig.mAppTips ) {
0588             showBubble();
0589         }
0590         else if( QRandomGenerator::global()->bounded(TIP_FREQUENCY) == 1 && mConfig.mTips && !mBubble && !mCurrAnim->frameNum() ) {
0591             mTipsQueue.enqueue( QueueItem( QueueItem::Tip, mTips.tip() ) );
0592             showBubble();
0593         }
0594     }
0595 
0596     if( mTheme.isStatic() ) {
0597         mTimer->setSingleShot( true );
0598         mTimer->start( ( mState == Normal ) || ( mState == Sleeping ) ? 1000 : 100 );
0599     }
0600     else {
0601         mTimer->setSingleShot( true );
0602         mTimer->start( mCurrAnim->delay() );
0603     }
0604 
0605     if( !mCurrAnim->next() ) {
0606         if( mBubble ) {
0607             mCurrAnim->reset();
0608         }
0609         else {
0610             selectAnimation( mState );
0611         }
0612     }
0613 }
0614 
0615 
0616 void Amor::slotConfigure()
0617 {
0618     AmorDialog *mAmorDialog = new AmorDialog();
0619     connect( mAmorDialog, SIGNAL(changed()), SLOT(slotConfigChanged()) );
0620     connect( mAmorDialog, SIGNAL(offsetChanged(int)), SLOT(slotOffsetChanged(int)) );
0621     mAmorDialog->show();
0622 }
0623 
0624 
0625 void Amor::slotConfigChanged()
0626 {
0627     reset();
0628 }
0629 
0630 
0631 void Amor::slotOffsetChanged(int off)
0632 {
0633     mConfig.mOffset = off;
0634 
0635     if( mCurrAnim->frame() ) {
0636         mAmor->move( mPosition + mTargetRect.x() - mCurrAnim->hotspot().x(),
0637                      mTargetRect.y() - mCurrAnim->hotspot().y() + ( !mInDesktopBottom ? mConfig.mOffset : 0 ) );
0638     }
0639 }
0640 
0641 
0642 void Amor::slotWidgetDragged(const QPoint &delta, bool release)
0643 {
0644     mTimer->stop();
0645 
0646     if( mCurrAnim->frame() ) {
0647         int newPosition = mPosition + delta.x();
0648 
0649         if( mCurrAnim->totalMovement() + newPosition > mTargetRect.width() ) {
0650             newPosition = mTargetRect.width() - mCurrAnim->totalMovement();
0651         }
0652         else if( mCurrAnim->totalMovement() + newPosition < 0 ) {
0653             newPosition = -mCurrAnim->totalMovement();
0654         }
0655 
0656         mPosition = newPosition;
0657         mAmor->move( mTargetRect.x() + mPosition - mCurrAnim->hotspot().x(), mAmor->y() );
0658 
0659         if( mTheme.isStatic() && release ) {
0660             // static animations save the new position as preferred.
0661             int savePos = mPosition;
0662             if( savePos > mTargetRect.width()/2 ) {
0663                 savePos -= (mTargetRect.width()+1);
0664             }
0665             mConfig.mStaticPos = savePos;
0666             mConfig.write();
0667         }
0668     }
0669 
0670     if (release) {
0671         mTimer->setSingleShot( true );
0672         mTimer->start( 0 );
0673     }
0674 }
0675 
0676 
0677 void Amor::slotWindowActivate(WId win)
0678 {
0679     mTimer->stop();
0680     mNextTarget = win;
0681 
0682     // This is an active event that affects the target window
0683     std::time( &mActiveTime );
0684 
0685     // A window gaining focus implies that the current window has lost
0686     // focus.  Initiate a blur event if there is a current active window.
0687     if( mTargetWin ) {
0688         // We are losing focus from the current window
0689         mTimer->setSingleShot( true );
0690         mTimer->start( 0 );
0691         selectAnimation( Destroy );
0692     }
0693     else if( mNextTarget ) {
0694         // We are setting focus to a new window
0695         if( mState != Focus ) {
0696             selectAnimation( Focus );
0697         }
0698         mTimer->setSingleShot( true );
0699         mTimer->start( 0 );
0700     }
0701     else {
0702         // No action - We can get this when we switch between two empty desktops
0703         mAmor->hide();
0704     }
0705 }
0706 
0707 
0708 void Amor::slotWindowRemove(WId win)
0709 {
0710     if( win == mTargetWin ) {
0711         // This is an active event that affects the target window
0712         std::time( &mActiveTime );
0713 
0714         selectAnimation( Destroy );
0715         mTimer->stop();
0716         mTimer->setSingleShot( true );
0717         mTimer->start( 0 );
0718     }
0719 }
0720 
0721 
0722 void Amor::slotStackingChanged()
0723 {
0724     // This is an active event that affects the target window
0725     std::time( &mActiveTime );
0726 
0727     // We seem to get this signal before the window has been restacked,
0728     // so we just schedule a restack.
0729     mStackTimer->setSingleShot( true );
0730     mStackTimer->start( 20 );
0731 }
0732 
0733 
0734 void Amor::slotWindowChange(WId win, NET::Properties properties, NET::Properties2 properties2)
0735 {
0736     if( win != mTargetWin ) {
0737         return;
0738     }
0739 
0740     // This is an active event that affects the target window
0741     std::time( &mActiveTime );
0742 
0743     KWindowInfo windowInfo( mTargetWin, NET::WMFrameExtents );
0744     NET::MappingState mappingState = windowInfo.mappingState();
0745 
0746     if( mappingState == NET::Iconic || mappingState == NET::Withdrawn ) {
0747         // The target window has been iconified
0748         selectAnimation( Destroy );
0749         mTargetWin = XCB_NONE;
0750         mTimer->stop();
0751         mTimer->setSingleShot( true );
0752         mTimer->start( 0 );
0753 
0754         return;
0755     }
0756 
0757     if( properties & NET::WMGeometry ) {
0758         QRect newTargetRect = windowInfo.frameGeometry();
0759 
0760         // if the change in the window caused the animation to fall
0761         // out of the working area of the desktop, or if the animation
0762         // didn't fall in the working area before but it does now, then
0763         //  refocus on the current window so that the animation is
0764         // relocated.
0765         QRect desktopArea = mWin->workArea();
0766 
0767         bool fitsInWorkArea = !( newTargetRect.y() - mCurrAnim->hotspot().y() + mConfig.mOffset < desktopArea.y() );
0768         if( ( !fitsInWorkArea && !mInDesktopBottom ) || ( fitsInWorkArea && mInDesktopBottom ) ) {
0769             mNextTarget = mTargetWin;
0770             selectAnimation( Blur );
0771             mTimer->setSingleShot( true );
0772             mTimer->start( 0 );
0773 
0774             return;
0775         }
0776 
0777         if( !mInDesktopBottom ) {
0778             mTargetRect = newTargetRect;
0779         }
0780 
0781         // make sure the animation is still on the window.
0782         if( mCurrAnim->frame() ) {
0783             hideBubble();
0784             if( mTheme.isStatic() ) {
0785                 if( mConfig.mStaticPos < 0 ) {
0786                     mPosition = mTargetRect.width() + mConfig.mStaticPos;
0787                 }
0788                 else {
0789                     mPosition = mConfig.mStaticPos;
0790                 }
0791 
0792                 if( mPosition >= mTargetRect.width() ) {
0793                     mPosition = mTargetRect.width()-1;
0794                 }
0795                 else if( mPosition < 0 ) {
0796                     mPosition = 0;
0797                 }
0798             }
0799             else if( mPosition > mTargetRect.width() - ( mCurrAnim->frame()->width() - mCurrAnim->hotspot().x() ) ) {
0800                 mPosition = mTargetRect.width() - ( mCurrAnim->frame()->width() - mCurrAnim->hotspot().x() );
0801             }
0802             mAmor->move( mTargetRect.x() + mPosition - mCurrAnim->hotspot().x(),
0803                          mTargetRect.y() - mCurrAnim->hotspot().y() + ( !mInDesktopBottom ? mConfig.mOffset : 0 ) );
0804         }
0805     }
0806 }
0807 
0808 
0809 void Amor::slotDesktopChange(int desktop)
0810 {
0811     mNextTarget = XCB_NONE;
0812     mTargetWin = XCB_NONE;
0813     selectAnimation( Normal );
0814     mTimer->stop();
0815     mAmor->hide();
0816 }
0817 
0818 
0819 void Amor::slotBubbleTimeout()
0820 {
0821     // has the queue item been displayed for long enough?
0822     QueueItem &first = mTipsQueue.head();
0823 
0824     if( first.time() > BUBBLE_TIME_STEP && mBubble->isVisible() ) {
0825         first.setTime( first.time() - BUBBLE_TIME_STEP );
0826         mBubbleTimer->setSingleShot( true );
0827         mBubbleTimer->start( BUBBLE_TIME_STEP );
0828         return;
0829     }
0830 
0831     // do not do anything if the mouse pointer is in the bubble
0832     if( mBubble->mouseWithin() ) {
0833         first.setTime( 500 );                  // show this item for another 500ms
0834         mBubbleTimer->setSingleShot( true );
0835         mBubbleTimer->start( BUBBLE_TIME_STEP );
0836         return;
0837     }
0838 
0839     // are there any other tips pending?
0840     if( mTipsQueue.count() > 1 ) {
0841         mTipsQueue.dequeue();
0842         showBubble();           // shows the next item in the queue
0843     }
0844     else {
0845         hideBubble( true );     // hideBubble calls dequeue() for itself.
0846     }
0847 }
0848 
0849 
0850 // kate: word-wrap off; encoding utf-8; indent-width 4; tab-width 4; line-numbers on; mixed-indent off; remove-trailing-space-save on; replace-tabs-save on; replace-tabs on; space-indent on;
0851 // vim:set spell et sw=4 ts=4 nowrap cino=l1,cs,U1: