// -*- mode: C++; c-file-style: "gnu" -*- /*************************************************************************** kmsystemtray.cpp - description ------------------- begin : Fri Aug 31 22:38:44 EDT 2001 copyright : (C) 2001 by Ryan Breen email : ryan@porivo.com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include #include "kmsystemtray.h" #include "kmfolder.h" #include "kmfoldertree.h" #include "kmfoldermgr.h" #include "kmfolderimap.h" #include "kmmainwidget.h" #include "accountmanager.h" using KMail::AccountManager; #include "globalsettings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** * Construct a KSystemTray icon to be displayed when new mail * has arrived in a non-system folder. The KMSystemTray listens * for updateNewMessageNotification events from each non-system * KMFolder and maintains a store of all folders with unread * messages. * * The KMSystemTray also provides a popup menu listing each folder * with its count of unread messages, allowing the user to jump * to the first unread message in each folder. */ KMSystemTray::KMSystemTray(TQWidget *parent, const char *name) : KSystemTray( parent, name ), mParentVisible( true ), mPosOfMainWin( 0, 0 ), mDesktopOfMainWin( 0 ), mMode( GlobalSettings::EnumSystemTrayPolicy::ShowOnUnread ), mCount( 0 ), mNewMessagePopupId(-1), mPopupMenu(0) { tqsetAlignment( AlignCenter ); kdDebug(5006) << "Initting systray" << endl; mLastUpdate = time( 0 ); mUpdateTimer = new TQTimer( this, "systraytimer" ); connect( mUpdateTimer, TQT_SIGNAL( timeout() ), TQT_SLOT( updateNewMessages() ) ); mDefaultIcon = loadIcon( "kmail" ); mLightIconImage = loadIcon( "kmaillight" ).convertToImage(); setPixmap(mDefaultIcon); KMMainWidget * mainWidget = kmkernel->getKMMainWidget(); if ( mainWidget ) { TQWidget * mainWin = mainWidget->tqtopLevelWidget(); if ( mainWin ) { mDesktopOfMainWin = KWin::windowInfo( mainWin->winId(), NET::WMDesktop ).desktop(); mPosOfMainWin = mainWin->pos(); } } // register the applet with the kernel kmkernel->registerSystemTrayApplet( this ); /** Initiate connections between folders and this object */ foldersChanged(); connect( kmkernel->folderMgr(), TQT_SIGNAL(changed()), TQT_SLOT(foldersChanged())); connect( kmkernel->imapFolderMgr(), TQT_SIGNAL(changed()), TQT_SLOT(foldersChanged())); connect( kmkernel->dimapFolderMgr(), TQT_SIGNAL(changed()), TQT_SLOT(foldersChanged())); connect( kmkernel->searchFolderMgr(), TQT_SIGNAL(changed()), TQT_SLOT(foldersChanged())); connect( kmkernel->acctMgr(), TQT_SIGNAL( checkedMail( bool, bool, const TQMap & ) ), TQT_SLOT( updateNewMessages() ) ); connect( this, TQT_SIGNAL( quitSelected() ), TQT_SLOT( tray_quit() ) ); } void KMSystemTray::buildPopupMenu() { // Delete any previously created popup menu delete mPopupMenu; mPopupMenu = new KPopupMenu(); KMMainWidget * mainWidget = kmkernel->getKMMainWidget(); if ( !mainWidget ) return; mPopupMenu->insertTitle(*(this->pixmap()), "KMail"); KAction * action; if ( ( action = mainWidget->action("check_mail") ) ) action->plug( mPopupMenu ); if ( ( action = mainWidget->action("check_mail_in") ) ) action->plug( mPopupMenu ); if ( ( action = mainWidget->action("send_queued") ) ) action->plug( mPopupMenu ); if ( ( action = mainWidget->action("send_queued_via") ) ) action->plug( mPopupMenu ); mPopupMenu->insertSeparator(); if ( ( action = mainWidget->action("new_message") ) ) action->plug( mPopupMenu ); if ( ( action = mainWidget->action("kmail_configure_kmail") ) ) action->plug( mPopupMenu ); mPopupMenu->insertSeparator(); KMainWindow *mainWin = ::tqqt_cast(kmkernel->getKMMainWidget()->tqtopLevelWidget()); mPopupMenu->insertItem( SmallIcon("exit"), i18n("&Quit"), this, TQT_SLOT(maybeQuit()) ); } void KMSystemTray::tray_quit() { // Quit all of KMail kapp->quit(); } KMSystemTray::~KMSystemTray() { // unregister the applet kmkernel->unregisterSystemTrayApplet( this ); delete mPopupMenu; mPopupMenu = 0; } void KMSystemTray::setMode(int newMode) { if(newMode == mMode) return; kdDebug(5006) << "Setting systray mMode to " << newMode << endl; mMode = newMode; switch ( mMode ) { case GlobalSettings::EnumSystemTrayPolicy::ShowAlways: if ( isHidden() ) show(); break; case GlobalSettings::EnumSystemTrayPolicy::ShowOnUnread: if ( mCount == 0 && !isHidden() ) hide(); else if ( mCount > 0 && isHidden() ) show(); break; default: kdDebug(5006) << k_funcinfo << " Unknown systray mode " << mMode << endl; } } int KMSystemTray::mode() const { return mMode; } /** * Update the count of unread messages. If there are unread messages, * overlay the count on top of a transparent version of the KMail icon. * If there is no unread mail, restore the normal KMail icon. */ void KMSystemTray::updateCount() { if(mCount != 0) { int oldPixmapWidth = pixmap()->size().width(); int oldPixmapHeight = pixmap()->size().height(); TQString countString = TQString::number( mCount ); TQFont countFont = KGlobalSettings::generalFont(); countFont.setBold(true); // decrease the size of the font for the number of unread messages if the // number doesn't fit into the available space float countFontSize = countFont.pointSizeFloat(); TQFontMetrics qfm( countFont ); int width = qfm.width( countString ); if( width > oldPixmapWidth ) { countFontSize *= float( oldPixmapWidth ) / float( width ); countFont.setPointSizeFloat( countFontSize ); } // Create an image which represents the number of unread messages // and which has a transparent background. // Unfortunately this required the following twisted code because for some // reason text that is drawn on a transparent pixmap is invisible // (apparently the alpha channel isn't changed when the text is drawn). // Therefore I have to draw the text on a solid background and then remove // the background by making it transparent with TQPixmap::setMask. This // involves the slow createHeuristicMask() function (from the API docs: // "This function is slow because it involves transformation to a TQImage, // non-trivial computations and a transformation back to a TQBitmap."). Then // I have to convert the resulting TQPixmap to a TQImage in order to overlay // the light KMail icon with the number (because KIconEffect::overlay only // works with TQImage). Finally the resulting TQImage has to be converted // back to a TQPixmap. // That's a lot of work for overlaying the KMail icon with the number of // unread messages, but every other approach I tried failed miserably. // IK, 2003-09-22 TQPixmap numberPixmap( oldPixmapWidth, oldPixmapHeight ); numberPixmap.fill( TQt::white ); TQPainter p( &numberPixmap ); p.setFont( countFont ); p.setPen( TQt::blue ); p.drawText( numberPixmap.rect(), TQt::AlignCenter, countString ); numberPixmap.setMask( numberPixmap.createHeuristicMask() ); TQImage numberImage = numberPixmap.convertToImage(); // Overlay the light KMail icon with the number image TQImage iconWithNumberImage = mLightIconImage.copy(); KIconEffect::overlay( iconWithNumberImage, numberImage ); TQPixmap iconWithNumber; iconWithNumber.convertFromImage( iconWithNumberImage ); setPixmap( iconWithNumber ); } else { setPixmap( mDefaultIcon ); } } /** * Refreshes the list of folders we are monitoring. This is called on * startup and is also connected to the 'changed' signal on the KMFolderMgr. */ void KMSystemTray::foldersChanged() { /** * Hide and remove all unread mappings to cover the case where the only * unread message was in a folder that was just removed. */ mFoldersWithUnread.clear(); mCount = 0; if ( mMode == GlobalSettings::EnumSystemTrayPolicy::ShowOnUnread ) { hide(); } /** Disconnect all previous connections */ disconnect(this, TQT_SLOT(updateNewMessageNotification(KMFolder *))); TQStringList folderNames; TQValueList > folderList; kmkernel->folderMgr()->createFolderList(&folderNames, &folderList); kmkernel->imapFolderMgr()->createFolderList(&folderNames, &folderList); kmkernel->dimapFolderMgr()->createFolderList(&folderNames, &folderList); kmkernel->searchFolderMgr()->createFolderList(&folderNames, &folderList); TQStringList::iterator strIt = folderNames.begin(); for(TQValueList >::iterator it = folderList.begin(); it != folderList.end() && strIt != folderNames.end(); ++it, ++strIt) { KMFolder * currentFolder = *it; TQString currentName = *strIt; if ( ((!currentFolder->isSystemFolder() || (currentFolder->name().lower() == "inbox")) || (currentFolder->folderType() == KMFolderTypeImap)) && !currentFolder->ignoreNewMail() ) { /** If this is a new folder, start listening for messages */ connect(currentFolder, TQT_SIGNAL(numUnreadMsgsChanged(KMFolder *)), this, TQT_SLOT(updateNewMessageNotification(KMFolder *))); /** Check all new folders to see if we started with any new messages */ updateNewMessageNotification(currentFolder); } else { disconnect(currentFolder, TQT_SIGNAL(numUnreadMsgsChanged(KMFolder *)), this, TQT_SLOT(updateNewMessageNotification(KMFolder *)) ); } } } /** * On left mouse click, switch focus to the first KMMainWidget. On right * click, bring up a list of all folders with a count of unread messages. */ void KMSystemTray::mousePressEvent(TQMouseEvent *e) { // switch to kmail on left mouse button if( e->button() == Qt::LeftButton ) { if( mParentVisible && mainWindowIsOnCurrentDesktop() ) hideKMail(); else showKMail(); } // open popup menu on right mouse button if( e->button() == Qt::RightButton ) { mPopupFolders.clear(); mPopupFolders.reserve( mFoldersWithUnread.count() ); // Rebuild popup menu at click time to minimize race condition if // the base KMainWidget is closed. buildPopupMenu(); if(mNewMessagePopupId != -1) { mPopupMenu->removeItem(mNewMessagePopupId); } if(mFoldersWithUnread.count() > 0) { KPopupMenu *newMessagesPopup = new KPopupMenu(); TQMap, int>::Iterator it = mFoldersWithUnread.begin(); for(uint i=0; it != mFoldersWithUnread.end(); ++i) { kdDebug(5006) << "Adding folder" << endl; mPopupFolders.append( it.key() ); TQString item = prettyName(it.key()) + " (" + TQString::number(it.data()) + ")"; newMessagesPopup->insertItem(item, this, TQT_SLOT(selectedAccount(int)), 0, i); ++it; } mNewMessagePopupId = mPopupMenu->insertItem(i18n("New Messages In"), newMessagesPopup, mNewMessagePopupId, 3); kdDebug(5006) << "Folders added" << endl; } mPopupMenu->popup(e->globalPos()); } } /** * Return the name of the folder in which the mail is deposited, prepended * with the account name if the folder is IMAP. */ TQString KMSystemTray::prettyName(KMFolder * fldr) { TQString rvalue = fldr->label(); if(fldr->folderType() == KMFolderTypeImap) { KMFolderImap * imap = dynamic_cast (fldr->storage()); assert(imap); if((imap->account() != 0) && (imap->account()->name() != 0) ) { kdDebug(5006) << "IMAP folder, prepend label with type" << endl; rvalue = imap->account()->name() + "->" + rvalue; } } kdDebug(5006) << "Got label " << rvalue << endl; return rvalue; } bool KMSystemTray::mainWindowIsOnCurrentDesktop() { KMMainWidget * mainWidget = kmkernel->getKMMainWidget(); if ( !mainWidget ) return false; TQWidget *mainWin = kmkernel->getKMMainWidget()->tqtopLevelWidget(); if ( !mainWin ) return false; return KWin::windowInfo( mainWin->winId(), NET::WMDesktop ).isOnCurrentDesktop(); } /** * Shows and raises the first KMMainWidget and * switches to the appropriate virtual desktop. */ void KMSystemTray::showKMail() { if (!kmkernel->getKMMainWidget()) return; TQWidget *mainWin = kmkernel->getKMMainWidget()->tqtopLevelWidget(); assert(mainWin); if(mainWin) { KWin::WindowInfo cur = KWin::windowInfo( mainWin->winId(), NET::WMDesktop ); if ( cur.valid() ) mDesktopOfMainWin = cur.desktop(); // switch to appropriate desktop if ( mDesktopOfMainWin != NET::OnAllDesktops ) KWin::setCurrentDesktop( mDesktopOfMainWin ); if ( !mParentVisible ) { if ( mDesktopOfMainWin == NET::OnAllDesktops ) KWin::setOnAllDesktops( mainWin->winId(), true ); mainWin->move( mPosOfMainWin ); mainWin->show(); } KWin::activateWindow( mainWin->winId() ); mParentVisible = true; } kmkernel->raise(); //Fake that the folders have changed so that the icon status is correct foldersChanged(); } void KMSystemTray::hideKMail() { if (!kmkernel->getKMMainWidget()) return; TQWidget *mainWin = kmkernel->getKMMainWidget()->tqtopLevelWidget(); assert(mainWin); if(mainWin) { mDesktopOfMainWin = KWin::windowInfo( mainWin->winId(), NET::WMDesktop ).desktop(); mPosOfMainWin = mainWin->pos(); // iconifying is unnecessary, but it looks cooler KWin::iconifyWindow( mainWin->winId() ); mainWin->hide(); mParentVisible = false; } } /** * Called on startup of the KMSystemTray and when the numUnreadMsgsChanged signal * is emitted for one of the watched folders. Shows the system tray icon if there * are new messages and the icon was hidden, or hides the system tray icon if there * are no more new messages. */ void KMSystemTray::updateNewMessageNotification(KMFolder * fldr) { //We don't want to count messages from search folders as they // already counted as part of their original folders if( !fldr || fldr->folderType() == KMFolderTypeSearch ) { // kdDebug(5006) << "Null or a search folder, can't mess with that" << endl; return; } mPendingUpdates[ fldr ] = true; if ( time( 0 ) - mLastUpdate > 2 ) { mUpdateTimer->stop(); updateNewMessages(); } else { mUpdateTimer->start(150, true); } } void KMSystemTray::updateNewMessages() { for ( TQMap, bool>::Iterator it = mPendingUpdates.begin(); it != mPendingUpdates.end(); ++it) { KMFolder *fldr = it.key(); if ( !fldr ) // deleted folder continue; /** The number of unread messages in that folder */ int unread = fldr->countUnread(); TQMap, int>::Iterator it = mFoldersWithUnread.find(fldr); bool unmapped = (it == mFoldersWithUnread.end()); /** If the folder is not mapped yet, increment count by numUnread in folder */ if(unmapped) mCount += unread; /* Otherwise, get the difference between the numUnread in the folder and * our last known version, and adjust mCount with that difference */ else { int diff = unread - it.data(); mCount += diff; } if(unread > 0) { /** Add folder to our internal store, or update unread count if already mapped */ mFoldersWithUnread.insert(fldr, unread); //kdDebug(5006) << "There are now " << mFoldersWithUnread.count() << " folders with unread" << endl; } /** * Look for folder in the list of folders already represented. If there are * unread messages and the system tray icon is hidden, show it. If there are * no unread messages, remove the folder from the mapping. */ if(unmapped) { /** Spurious notification, ignore */ if(unread == 0) continue; /** Make sure the icon will be displayed */ if ( ( mMode == GlobalSettings::EnumSystemTrayPolicy::ShowOnUnread ) && isHidden() ) { show(); } } else { if(unread == 0) { kdDebug(5006) << "Removing folder from internal store " << fldr->name() << endl; /** Remove the folder from the internal store */ mFoldersWithUnread.remove(fldr); /** if this was the last folder in the dictionary, hide the systemtray icon */ if(mFoldersWithUnread.count() == 0) { mPopupFolders.clear(); disconnect(this, TQT_SLOT(selectedAccount(int))); mCount = 0; if ( mMode == GlobalSettings::EnumSystemTrayPolicy::ShowOnUnread ) { hide(); } } } } } mPendingUpdates.clear(); updateCount(); /** Update tooltip to reflect count of unread messages */ TQToolTip::remove(this); TQToolTip::add(this, mCount == 0 ? i18n("There are no unread messages") : i18n("There is 1 unread message.", "There are %n unread messages.", mCount)); mLastUpdate = time( 0 ); } /** * Called when user selects a folder name from the popup menu. Shows * the first KMMainWin in the memberlist and jumps to the first unread * message in the selected folder. */ void KMSystemTray::selectedAccount(int id) { showKMail(); KMMainWidget * mainWidget = kmkernel->getKMMainWidget(); if (!mainWidget) { kmkernel->openReader(); mainWidget = kmkernel->getKMMainWidget(); } assert(mainWidget); /** Select folder */ KMFolder * fldr = mPopupFolders.at(id); if(!fldr) return; KMFolderTree * ft = mainWidget->folderTree(); if(!ft) return; TQListViewItem * fldrIdx = ft->indexOfFolder(fldr); if(!fldrIdx) return; ft->setCurrentItem(fldrIdx); ft->selectCurrentFolder(); } bool KMSystemTray::hasUnreadMail() const { return ( mCount != 0 ); } #include "kmsystemtray.moc"