1 /****************************************************************************************
2  * Copyright (c) 2003 Stanislav Karchebny <berkus@users.sf.net>                         *
3  * Copyright (c) 2003 Max Howell <max.howell@methylblue.com>                            *
4  * Copyright (c) 2004 Enrico Ros <eros.kde@email.it>                                    *
5  * Copyright (c) 2006 Ian Monroe <ian@monroe.nu>                                        *
6  * Copyright (c) 2009-2011 Kevin Funk <krf@electrostorm.net>                            *
7  * Copyright (c) 2009 Mark Kretschmann <kretschmann@kde.org>                            *
8  *                                                                                      *
9  * This program is free software; you can redistribute it and/or modify it under        *
10  * the terms of the GNU General Public License as published by the Free Software        *
11  * Foundation; either version 2 of the License, or (at your option) any later           *
12  * version.                                                                             *
13  *                                                                                      *
14  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
15  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
16  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
17  *                                                                                      *
18  * You should have received a copy of the GNU General Public License along with         *
19  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
20  ****************************************************************************************/
21 
22 #include "TrayIcon.h"
23 
24 #include "App.h"
25 #include "EngineController.h"
26 #include "GlobalCurrentTrackActions.h"
27 #include "SvgHandler.h"
28 #include "amarokconfig.h"
29 #include "core/capabilities/ActionsCapability.h"
30 #include "core/capabilities/BookmarkThisCapability.h"
31 #include "core/meta/Meta.h"
32 #include "core/meta/Statistics.h"
33 #include "core/support/Amarok.h"
34 #include "playlist/PlaylistActions.h"
35 
36 #include <KLocalizedString>
37 #include <KIconLoader>
38 
39 #include <QAction>
40 #include <QFontMetrics>
41 #include <QMenu>
42 #include <QPixmap>
43 #include <QStandardPaths>
44 #include <QToolTip>
45 
46 #ifdef Q_WS_MAC
47     extern void qt_mac_set_dock_menu(QMenu *);
48 #endif
49 
TrayIcon(QObject * parent)50 Amarok::TrayIcon::TrayIcon( QObject *parent )
51     : KStatusNotifierItem( parent )
52     , m_track( The::engineController()->currentTrack() )
53 {
54     PERF_LOG( "Beginning TrayIcon Constructor" );
55     KActionCollection* const ac = Amarok::actionCollection();
56 
57     setStatus( KStatusNotifierItem::Active );
58 
59     // Remove the "Configure Amarok..." action, as it makes no sense in the tray menu
60     const QString preferences = KStandardAction::name( KStandardAction::Preferences );
61     contextMenu()->removeAction( ac->action( preferences ) );
62 
63     PERF_LOG( "Before adding actions" );
64 
65 #ifdef Q_WS_MAC
66     // Add these functions to the dock icon menu in OS X
67     qt_mac_set_dock_menu( contextMenu() );
68     contextMenu()->addAction( ac->action( "playlist_playmedia" ) );
69     contextMenu()->addSeparator();
70 #endif
71 
72     contextMenu()->addAction( ac->action( "prev"       ) );
73     contextMenu()->addAction( ac->action( "play_pause" ) );
74     contextMenu()->addAction( ac->action( "stop"       ) );
75     contextMenu()->addAction( ac->action( "next"       ) );
76 
77     contextMenu()->addSeparator();
78 
79     contextMenu()->setObjectName( "TrayIconContextMenu" );
80 
81     PERF_LOG( "Initializing system tray icon" );
82 
83     setIconByName( "amarok" );
84     updateOverlayIcon();
85     updateToolTipIcon();
86     updateMenu();
87 
88     const EngineController* engine = The::engineController();
89     connect( engine, &EngineController::trackPlaying,
90              this, &TrayIcon::trackPlaying );
91     connect( engine, &EngineController::stopped,
92              this, &TrayIcon::stopped );
93     connect( engine, &EngineController::paused,
94              this, &TrayIcon::paused );
95 
96     connect( engine, &EngineController::trackMetadataChanged,
97              this, &TrayIcon::trackMetadataChanged );
98 
99     connect( engine, &EngineController::albumMetadataChanged,
100              this, &TrayIcon::albumMetadataChanged );
101 
102     connect( engine, &EngineController::volumeChanged,
103              this, &TrayIcon::updateToolTip );
104 
105     connect( engine, &EngineController::muteStateChanged,
106              this, &TrayIcon::updateToolTip );
107 
108     connect( engine, &EngineController::playbackStateChanged,
109              this, &TrayIcon::updateOverlayIcon );
110 
111     connect( this, &TrayIcon::scrollRequested, this, &TrayIcon::slotScrollRequested );
112     connect( this, &TrayIcon::secondaryActivateRequested,
113              The::engineController(), &EngineController::playPause );
114 }
115 
116 void
updateToolTipIcon()117 Amarok::TrayIcon::updateToolTipIcon()
118 {
119     updateToolTip(); // the normal update
120 
121     if( m_track )
122     {
123         if( m_track->album() && m_track->album()->hasImage() )
124         {
125             QPixmap image = The::svgHandler()->imageWithBorder( m_track->album(), KIconLoader::SizeLarge, 5 );
126             setToolTipIconByPixmap( image );
127         }
128         else
129         {
130             setToolTipIconByName( "amarok" );
131         }
132     }
133     else
134     {
135         setToolTipIconByName( "amarok" );
136     }
137 }
138 
139 
140 void
updateToolTip()141 Amarok::TrayIcon::updateToolTip()
142 {
143     if( m_track )
144     {
145         setToolTipTitle( i18n( "Now playing" ) );
146 
147         QStringList tooltip;
148         tooltip << The::engineController()->prettyNowPlaying( false );
149 
150         QString volume;
151         if ( The::engineController()->isMuted() )
152         {
153             volume = i18n( "Muted" );
154         }
155         else
156         {
157             volume = i18n( "%1%", The::engineController()->volume() );
158         }
159         tooltip << i18n( "<i>Volume: %1</i>", volume );
160 
161         Meta::StatisticsPtr statistics = m_track->statistics();
162         const float score = statistics->score();
163         if( score > 0.f )
164         {
165             tooltip << i18n( "Score: %1", QString::number( score, 'f', 2 ) );
166         }
167 
168         const int rating = statistics->rating();
169         if( rating > 0 )
170         {
171             QString stars;
172             for( int i = 0; i < rating / 2; ++i )
173                 stars += QStringLiteral( "<img src=\"%1\" height=\"%2\" width=\"%3\">" )
174                         .arg( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/star.png" ) )
175                         .arg( QFontMetrics( QToolTip::font() ).height() )
176                         .arg( QFontMetrics( QToolTip::font() ).height() );
177             if( rating % 2 )
178                 stars += QStringLiteral( "<img src=\"%1\" height=\"%2\" width=\"%3\">" )
179                         .arg( QStandardPaths::locate( QStandardPaths::GenericDataLocation, "amarok/images/smallstar.png" ) )
180                         .arg( QFontMetrics( QToolTip::font() ).height() )
181                         .arg( QFontMetrics( QToolTip::font() ).height() );
182 
183             tooltip << i18n( "Rating: %1", stars );
184         }
185 
186         const int count = statistics->playCount();
187         if( count > 0 )
188         {
189             tooltip << i18n( "Play count: %1", count );
190         }
191 
192         const QDateTime lastPlayed = statistics->lastPlayed();
193         tooltip << i18n( "Last played: %1", Amarok::verboseTimeSince( lastPlayed ) );
194 
195         setToolTipSubTitle( tooltip.join("<br>") );
196     }
197     else
198     {
199         setToolTipTitle( pApp->applicationDisplayName() );
200         setToolTipSubTitle( The::engineController()->prettyNowPlaying( false ) );
201     }
202 }
203 
204 void
trackPlaying(const Meta::TrackPtr & track)205 Amarok::TrayIcon::trackPlaying( const Meta::TrackPtr &track )
206 {
207     m_track = track;
208 
209     updateMenu();
210     updateToolTipIcon();
211 }
212 
213 void
paused()214 Amarok::TrayIcon::paused()
215 {
216     updateToolTipIcon();
217 
218 }
219 
220 void
stopped()221 Amarok::TrayIcon::stopped()
222 {
223     m_track = 0;
224     updateMenu(); // remove custom track actions on stop
225     updateToolTipIcon();
226 }
227 
228 void
trackMetadataChanged(const Meta::TrackPtr & track)229 Amarok::TrayIcon::trackMetadataChanged( const Meta::TrackPtr &track )
230 {
231     Q_UNUSED( track )
232 
233     updateToolTip();
234     updateMenu();
235 }
236 
237 void
albumMetadataChanged(const Meta::AlbumPtr & album)238 Amarok::TrayIcon::albumMetadataChanged( const Meta::AlbumPtr &album )
239 {
240     Q_UNUSED( album )
241 
242     updateToolTipIcon();
243     updateMenu();
244 }
245 
246 void
slotScrollRequested(int delta,Qt::Orientation orientation)247 Amarok::TrayIcon::slotScrollRequested( int delta, Qt::Orientation orientation )
248 {
249     Q_UNUSED( orientation )
250 
251     The::engineController()->increaseVolume( delta / Amarok::VOLUME_SENSITIVITY );
252 }
253 
254 QAction*
action(const QString & name,const QMap<QString,QAction * > & actionByName)255 Amarok::TrayIcon::action( const QString& name, const QMap<QString, QAction*> &actionByName )
256 {
257   QAction* action = nullptr;
258 
259   if ( !name.isEmpty() )
260     action = actionByName.value(name);
261 
262   return action;
263 }
264 
265 void
updateMenu()266 Amarok::TrayIcon::updateMenu()
267 {
268     foreach( QAction* action, m_extraActions )
269     {
270         contextMenu()->removeAction( action );
271         // -- delete actions without parent (e.g. the ones from the capabilities)
272         if( action && !action->parent() )
273         {
274             delete action;
275         }
276     }
277 
278     QMap<QString, QAction*> actionByName;
279     foreach (QAction* action, actionCollection())
280     {
281         actionByName.insert(action->text(), action);
282     }
283 
284     m_extraActions.clear();
285 
286     contextMenu()->removeAction( m_separator.data() );
287 
288     delete m_separator.data();
289 
290     if( m_track )
291     {
292         foreach( QAction *action, The::globalCurrentTrackActions()->actions() )
293         {
294             m_extraActions.append( action );
295             connect( action, &QObject::destroyed, this, [this, action]() { m_extraActions.removeAll( action ); } );
296         }
297 
298         QScopedPointer<Capabilities::ActionsCapability> ac( m_track->create<Capabilities::ActionsCapability>() );
299         if( ac )
300         {
301             QList<QAction*> actions = ac->actions();
302             foreach( QAction *action, actions )
303             {
304                 m_extraActions.append( action );
305                 connect( action, &QObject::destroyed, this, [this, action]() { m_extraActions.removeAll( action ); } );
306             }
307         }
308 
309         QScopedPointer<Capabilities::BookmarkThisCapability> btc( m_track->create<Capabilities::BookmarkThisCapability>() );
310         if( btc )
311         {
312             QAction *action = btc->bookmarkAction();
313             m_extraActions.append( action );
314             connect( action, &QObject::destroyed, this, [this, action]() { m_extraActions.removeAll( action ); } );
315         }
316     }
317 
318     // second statement checks if the menu has already been populated (first startup), if not: do it
319     if( m_extraActions.count() > 0 ||
320         contextMenu()->actions().last() != actionByName.value( "file_quit" ) )
321     {
322         // remove the 2 bottom items, so we can push them to the bottom again
323         contextMenu()->removeAction( action( "file_quit", actionByName ) );
324         contextMenu()->removeAction( action( "minimizeRestore", actionByName ) );
325 
326         foreach( QAction* action, m_extraActions )
327             contextMenu()->addAction( action );
328 
329         m_separator = contextMenu()->addSeparator();
330         // readd
331         contextMenu()->addAction( action( "minimizeRestore", actionByName  ) );
332         contextMenu()->addAction( action( "file_quit", actionByName  ) );
333     }
334 }
335 
336 void
updateOverlayIcon()337 Amarok::TrayIcon::updateOverlayIcon()
338 {
339     if( The::engineController()->isPlaying() )
340         setOverlayIconByName( "media-playback-start" );
341     else if( The::engineController()->isPaused() )
342         setOverlayIconByName( "media-playback-pause" );
343     else
344         setOverlayIconByName( QString() );
345 }
346 
347