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