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: