File indexing completed on 2024-04-21 04:47:53

0001 /****************************************************************************************
0002  * Copyright (c) 2003 Stanislav Karchebny <berkus@users.sf.net>                         *
0003  * Copyright (c) 2003 Max Howell <max.howell@methylblue.com>                            *
0004  * Copyright (c) 2004 Enrico Ros <eros.kde@email.it>                                    *
0005  * Copyright (c) 2006 Ian Monroe <ian@monroe.nu>                                        *
0006  * Copyright (c) 2009-2011 Kevin Funk <krf@electrostorm.net>                            *
0007  * Copyright (c) 2009 Mark Kretschmann <kretschmann@kde.org>                            *
0008  *                                                                                      *
0009  * This program is free software; you can redistribute it and/or modify it under        *
0010  * the terms of the GNU General Public License as published by the Free Software        *
0011  * Foundation; either version 2 of the License, or (at your option) any later           *
0012  * version.                                                                             *
0013  *                                                                                      *
0014  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0015  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0016  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0017  *                                                                                      *
0018  * You should have received a copy of the GNU General Public License along with         *
0019  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0020  ****************************************************************************************/
0021 
0022 #include "TrayIcon.h"
0023 
0024 #include "App.h"
0025 #include "EngineController.h"
0026 #include "GlobalCurrentTrackActions.h"
0027 #include "SvgHandler.h"
0028 #include "amarokconfig.h"
0029 #include "core/capabilities/ActionsCapability.h"
0030 #include "core/capabilities/BookmarkThisCapability.h"
0031 #include "core/meta/Meta.h"
0032 #include "core/meta/Statistics.h"
0033 #include "core/support/Amarok.h"
0034 #include "playlist/PlaylistActions.h"
0035 
0036 #include <KLocalizedString>
0037 #include <KIconLoader>
0038 
0039 #include <QAction>
0040 #include <QFontMetrics>
0041 #include <QMenu>
0042 #include <QPixmap>
0043 #include <QStandardPaths>
0044 #include <QToolTip>
0045 
0046 #ifdef Q_WS_MAC
0047     extern void qt_mac_set_dock_menu(QMenu *);
0048 #endif
0049 
0050 Amarok::TrayIcon::TrayIcon( QObject *parent )
0051     : KStatusNotifierItem( parent )
0052     , m_track( The::engineController()->currentTrack() )
0053 {
0054     PERF_LOG( "Beginning TrayIcon Constructor" );
0055     KActionCollection* const ac = Amarok::actionCollection();
0056 
0057     setStatus( KStatusNotifierItem::Active );
0058 
0059     // Remove the "Configure Amarok..." action, as it makes no sense in the tray menu
0060     const QString preferences = KStandardAction::name( KStandardAction::Preferences );
0061     contextMenu()->removeAction( ac->action( preferences ) );
0062 
0063     PERF_LOG( "Before adding actions" );
0064 
0065 #ifdef Q_WS_MAC
0066     // Add these functions to the dock icon menu in OS X
0067     qt_mac_set_dock_menu( contextMenu() );
0068     contextMenu()->addAction( ac->action( "playlist_playmedia" ) );
0069     contextMenu()->addSeparator();
0070 #endif
0071 
0072     contextMenu()->addAction( ac->action( "prev"       ) );
0073     contextMenu()->addAction( ac->action( "play_pause" ) );
0074     contextMenu()->addAction( ac->action( "stop"       ) );
0075     contextMenu()->addAction( ac->action( "next"       ) );
0076 
0077     contextMenu()->addSeparator();
0078 
0079     contextMenu()->setObjectName( "TrayIconContextMenu" );
0080 
0081     PERF_LOG( "Initializing system tray icon" );
0082 
0083     setIconByName( "amarok" );
0084     updateOverlayIcon();
0085     updateToolTipIcon();
0086     updateMenu();
0087 
0088     const EngineController* engine = The::engineController();
0089     connect( engine, &EngineController::trackPlaying,
0090              this, &TrayIcon::trackPlaying );
0091     connect( engine, &EngineController::stopped,
0092              this, &TrayIcon::stopped );
0093     connect( engine, &EngineController::paused,
0094              this, &TrayIcon::paused );
0095 
0096     connect( engine, &EngineController::trackMetadataChanged,
0097              this, &TrayIcon::trackMetadataChanged );
0098 
0099     connect( engine, &EngineController::albumMetadataChanged,
0100              this, &TrayIcon::albumMetadataChanged );
0101 
0102     connect( engine, &EngineController::volumeChanged,
0103              this, &TrayIcon::updateToolTip );
0104 
0105     connect( engine, &EngineController::muteStateChanged,
0106              this, &TrayIcon::updateToolTip );
0107 
0108     connect( engine, &EngineController::playbackStateChanged,
0109              this, &TrayIcon::updateOverlayIcon );
0110 
0111     connect( this, &TrayIcon::scrollRequested, this, &TrayIcon::slotScrollRequested );
0112     connect( this, &TrayIcon::secondaryActivateRequested,
0113              The::engineController(), &EngineController::playPause );
0114 }
0115 
0116 void
0117 Amarok::TrayIcon::updateToolTipIcon()
0118 {
0119     updateToolTip(); // the normal update
0120 
0121     if( m_track )
0122     {
0123         if( m_track->album() && m_track->album()->hasImage() )
0124         {
0125             QPixmap image = The::svgHandler()->imageWithBorder( m_track->album(), KIconLoader::SizeLarge, 5 );
0126             setToolTipIconByPixmap( image );
0127         }
0128         else
0129         {
0130             setToolTipIconByName( "amarok" );
0131         }
0132     }
0133     else
0134     {
0135         setToolTipIconByName( "amarok" );
0136     }
0137 }
0138 
0139 
0140 void
0141 Amarok::TrayIcon::updateToolTip()
0142 {
0143     if( m_track )
0144     {
0145         setToolTipTitle( i18n( "Now playing" ) );
0146 
0147         QStringList tooltip;
0148         tooltip << The::engineController()->prettyNowPlaying( false );
0149 
0150         QString volume;
0151         if ( The::engineController()->isMuted() )
0152         {
0153             volume = i18n( "Muted" );
0154         }
0155         else
0156         {
0157             volume = i18n( "%1%", The::engineController()->volume() );
0158         }
0159         tooltip << i18n( "<i>Volume: %1</i>", volume );
0160 
0161         Meta::StatisticsPtr statistics = m_track->statistics();
0162         const float score = statistics->score();
0163         if( score > 0.f )
0164         {
0165             tooltip << i18n( "Score: %1", QString::number( score, 'f', 2 ) );
0166         }
0167 
0168         const int rating = statistics->rating();
0169         if( rating > 0 )
0170         {
0171             QString stars;
0172             for( int i = 0; i < rating / 2; ++i )
0173                 stars += QStringLiteral( "<img src=\"%1\" height=\"%2\" width=\"%3\">" )
0174                         .arg( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/star.png" ) )
0175                         .arg( QFontMetrics( QToolTip::font() ).height() )
0176                         .arg( QFontMetrics( QToolTip::font() ).height() );
0177             if( rating % 2 )
0178                 stars += QStringLiteral( "<img src=\"%1\" height=\"%2\" width=\"%3\">" )
0179                         .arg( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/smallstar.png" ) )
0180                         .arg( QFontMetrics( QToolTip::font() ).height() )
0181                         .arg( QFontMetrics( QToolTip::font() ).height() );
0182 
0183             tooltip << i18n( "Rating: %1", stars );
0184         }
0185 
0186         const int count = statistics->playCount();
0187         if( count > 0 )
0188         {
0189             tooltip << i18n( "Play count: %1", count );
0190         }
0191 
0192         const QDateTime lastPlayed = statistics->lastPlayed();
0193         tooltip << i18n( "Last played: %1", Amarok::verboseTimeSince( lastPlayed ) );
0194 
0195         setToolTipSubTitle( tooltip.join("<br>") );
0196     }
0197     else
0198     {
0199         setToolTipTitle( pApp->applicationDisplayName() );
0200         setToolTipSubTitle( The::engineController()->prettyNowPlaying( false ) );
0201     }
0202 }
0203 
0204 void
0205 Amarok::TrayIcon::trackPlaying( const Meta::TrackPtr &track )
0206 {
0207     m_track = track;
0208 
0209     updateMenu();
0210     updateToolTipIcon();
0211 }
0212 
0213 void
0214 Amarok::TrayIcon::paused()
0215 {
0216     updateToolTipIcon();
0217 
0218 }
0219 
0220 void
0221 Amarok::TrayIcon::stopped()
0222 {
0223     m_track = nullptr;
0224     updateMenu(); // remove custom track actions on stop
0225     updateToolTipIcon();
0226 }
0227 
0228 void
0229 Amarok::TrayIcon::trackMetadataChanged( const Meta::TrackPtr &track )
0230 {
0231     Q_UNUSED( track )
0232 
0233     updateToolTip();
0234     updateMenu();
0235 }
0236 
0237 void
0238 Amarok::TrayIcon::albumMetadataChanged( const Meta::AlbumPtr &album )
0239 {
0240     Q_UNUSED( album )
0241 
0242     updateToolTipIcon();
0243     updateMenu();
0244 }
0245 
0246 void
0247 Amarok::TrayIcon::slotScrollRequested( int delta, Qt::Orientation orientation )
0248 {
0249     Q_UNUSED( orientation )
0250 
0251     The::engineController()->increaseVolume( delta / Amarok::VOLUME_SENSITIVITY );
0252 }
0253 
0254 QAction*
0255 Amarok::TrayIcon::action( const QString& name, const QMap<QString, QAction*> &actionByName )
0256 {
0257   QAction* action = nullptr;
0258 
0259   if ( !name.isEmpty() )
0260     action = actionByName.value(name);
0261 
0262   return action;
0263 }
0264 
0265 void
0266 Amarok::TrayIcon::updateMenu()
0267 {
0268     foreach( QAction* action, m_extraActions )
0269     {
0270         contextMenu()->removeAction( action );
0271         // -- delete actions without parent (e.g. the ones from the capabilities)
0272         if( action && !action->parent() )
0273         {
0274             delete action;
0275         }
0276     }
0277 
0278     QMap<QString, QAction*> actionByName;
0279     foreach (QAction* action, actionCollection())
0280     {
0281         actionByName.insert(action->text(), action);
0282     }
0283 
0284     m_extraActions.clear();
0285 
0286     contextMenu()->removeAction( m_separator.data() );
0287 
0288     delete m_separator.data();
0289 
0290     if( m_track )
0291     {
0292         foreach( QAction *action, The::globalCurrentTrackActions()->actions() )
0293         {
0294             m_extraActions.append( action );
0295             connect( action, &QObject::destroyed, this, [this, action]() { m_extraActions.removeAll( action ); } );
0296         }
0297 
0298         QScopedPointer<Capabilities::ActionsCapability> ac( m_track->create<Capabilities::ActionsCapability>() );
0299         if( ac )
0300         {
0301             QList<QAction*> actions = ac->actions();
0302             foreach( QAction *action, actions )
0303             {
0304                 m_extraActions.append( action );
0305                 connect( action, &QObject::destroyed, this, [this, action]() { m_extraActions.removeAll( action ); } );
0306             }
0307         }
0308 
0309         QScopedPointer<Capabilities::BookmarkThisCapability> btc( m_track->create<Capabilities::BookmarkThisCapability>() );
0310         if( btc )
0311         {
0312             QAction *action = btc->bookmarkAction();
0313             m_extraActions.append( action );
0314             connect( action, &QObject::destroyed, this, [this, action]() { m_extraActions.removeAll( action ); } );
0315         }
0316     }
0317 
0318     // second statement checks if the menu has already been populated (first startup), if not: do it
0319     if( m_extraActions.count() > 0 ||
0320         contextMenu()->actions().last() != actionByName.value( "file_quit" ) )
0321     {
0322         // remove the 2 bottom items, so we can push them to the bottom again
0323         contextMenu()->removeAction( action( "file_quit", actionByName ) );
0324         contextMenu()->removeAction( action( "minimizeRestore", actionByName ) );
0325 
0326         foreach( QAction* action, m_extraActions )
0327             contextMenu()->addAction( action );
0328 
0329         m_separator = contextMenu()->addSeparator();
0330         // readd
0331         contextMenu()->addAction( action( "minimizeRestore", actionByName  ) );
0332         contextMenu()->addAction( action( "file_quit", actionByName  ) );
0333     }
0334 }
0335 
0336 void
0337 Amarok::TrayIcon::updateOverlayIcon()
0338 {
0339     if( The::engineController()->isPlaying() )
0340         setOverlayIconByName( "media-playback-start" );
0341     else if( The::engineController()->isPaused() )
0342         setOverlayIconByName( "media-playback-pause" );
0343     else
0344         setOverlayIconByName( QString() );
0345 }
0346