File indexing completed on 2022-10-04 14:26:21

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