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 Kevin Funk <krf@electrostorm.net>                       *
7
 *                                                                            *
8
 * This program is free software; you can redistribute it and/or              *
9
 * modify it under the terms of the GNU General Public License as             *
10
 * published by the Free Software Foundation; either version 2 of             *
11
 * the License, or (at your option) any later version.                        *
12
 *                                                                            *
13
 * This program is distributed in the hope that it will be useful,            *
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
16
 * GNU General Public License for more details.                               *
17
 *                                                                            *
18
 * You should have received a copy of the GNU General Public License          *
19
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.      *
20
 ******************************************************************************/
21
22
#include "Systray.h"
23
24
#include "Amarok.h"
25
#include "Debug.h"
26
#include "EngineController.h"
27
#include "amarokconfig.h"
28
#include "context/popupdropper/libpud/PopupDropperAction.h"
29
#include "GlobalCurrentTrackActions.h"
30
#include "meta/Meta.h"
31
#include "meta/MetaConstants.h"
32
#include "meta/MetaUtility.h" // for time formatting
33
#include "meta/capabilities/CurrentTrackActionsCapability.h"
34
#include "playlist/PlaylistActions.h"
35
#include "playlist/PlaylistModel.h"
36
37
#include <KAction>
38
#include <KApplication>
39
#include <KIcon>
40
#include <KIconEffect>
41
#include <KLocale>
42
#include <KMenu>
43
#include <KStandardDirs>
44
45
#include <QEvent>
46
#include <QFontMetrics>
47
#include <QMouseEvent>
48
#include <QPainter>
49
#include <QPixmap>
50
#include <QTextDocument> // for Qt::escape()
51
#include <QToolTip>
52
53
namespace Amarok
54
{
55
    static QPixmap
56
    loadOverlay( const QString &iconName )
57
    {
58
        KIcon icon( iconName );
59
        if ( !icon.isNull() )
60
            return icon.pixmap( 10, 10 ); // overlay size, adjust here
61
62
        return 0;
63
    }
64
}
65
66
Amarok::TrayIcon::TrayIcon( QWidget *playerWidget )
67
        : KSystemTrayIcon( playerWidget )
68
        , EngineObserver( The::engineController() )
69
        , m_trackLength( 0 )
70
        , m_separator( 0 )
71
{
72
    DEBUG_BLOCK
73
74
    PERF_LOG( "Beginning TrayIcon Constructor" );
75
    KActionCollection* const ac = Amarok::actionCollection();
76
77
    //seems to be necessary
78
    /*QAction *quit = actionCollection()->action( "file_quit" );
79
    quit->disconnect();
80
    connect( quit, SIGNAL(activated()), kapp, SLOT(quit()) );*/
81
82
    PERF_LOG( "Before adding actions" );
83
84
    #ifdef Q_WS_MAC
85
    // Add these functions to the dock icon menu in OS X
86
    extern void qt_mac_set_dock_menu(QMenu *);
87
    qt_mac_set_dock_menu( contextMenu() );
88
    contextMenu()->addAction( ac->action( "playlist_playmedia" ) );
89
    contextMenu()->addAction( ac->action( "play_audiocd" ) );
90
    contextMenu()->addSeparator();
91
    #endif
92
93
    contextMenu()->addAction( ac->action( "prev"       ) );
94
    contextMenu()->addAction( ac->action( "play_pause" ) );
95
    contextMenu()->addAction( ac->action( "stop"       ) );
96
    contextMenu()->addAction( ac->action( "next"       ) );
97
98
    m_playOverlay = loadOverlay( "media-playback-start" );
99
    m_pauseOverlay = loadOverlay( "media-playback-pause" );
100
101
    PERF_LOG( "Adding system tray icon" );
102
    paintIcon();
103
104
    setupToolTip();
105
106
    connect( this, SIGNAL( activated( QSystemTrayIcon::ActivationReason ) ), SLOT( slotActivated( QSystemTrayIcon::ActivationReason ) ) );
107
    #ifdef Q_WS_MAC
108
    KSystemTrayIcon::setVisible( false );
109
    #endif
110
}
111
112
void
113
Amarok::TrayIcon::setVisible( bool visible )
114
{
115
    #ifdef Q_WS_MAC
116
    Q_UNUSED( visible )
117
    #else
118
    KSystemTrayIcon::setVisible( visible );
119
    #endif
120
}
121
122
void
123
Amarok::TrayIcon::setupToolTip()
124
{
125
    if( m_track )
126
    {
127
        QString tooltip;
128
129
        QFontMetrics fm( QToolTip::font() );
130
        const int elideWidth = 200;
131
        tooltip = "<center><b>" + fm.elidedText( Qt::escape(m_track->prettyName()), Qt::ElideRight, elideWidth ) + "</b>";
132
        if( m_track->artist() ) {
133
            const QString artist = fm.elidedText( Qt::escape(m_track->artist()->prettyName()), Qt::ElideRight, elideWidth );
134
            if( !artist.isEmpty() )
135
                tooltip += i18n( " by <b>%1</b>", artist );
136
        }
137
        if( m_track->album() ) {
138
            const QString album = fm.elidedText( Qt::escape(m_track->album()->prettyName()), Qt::ElideRight, elideWidth );
139
            if( !album.isEmpty() )
140
                tooltip += i18n( " on <b>%1</b>", album );
141
        }
142
        tooltip += "</center>";
143
144
        tooltip += "<table cellspacing='2' align='center' width='100%'>";
145
146
        // HACK: This block is inefficient and more or less stupid
147
        // (Unnecessary I/O on disk. Workaround?)
148
        const QString tmpFilename = Amarok::saveLocation() + "tooltipcover.png";
149
        if( m_track->album() )
150
        {
151
            const QPixmap image = m_track->album()->imageWithBorder( 100, 5 );
152
            image.save( tmpFilename, "PNG" );
153
            tooltip += "<tr><td width='10' align='left' valign='bottom' rowspan='9'>";
154
            tooltip += "<img src='"+tmpFilename+"' /></td></tr>";
155
        }
156
157
        QStringList left, right;
158
159
        QString volume;
160
        if ( The::engineController()->isMuted() )
161
            volume = i18n( "Muted" );
162
        else
163
            volume = QString( "%1%" ).arg( The::engineController()->volume() );
164
        right << QString("<i>%1</i>").arg( volume );
165
        left << "<i>Volume</i>";
166
167
        const float score = m_track->score();
168
        if( score > 0.f )
169
        {
170
            right << QString::number( score, 'f', 2 );  // 2 digits after decimal point
171
            left << i18n( "Score" );
172
        }
173
174
        const int rating = m_track->rating();
175
        if( rating > 0 )
176
        {
177
            QString s;
178
            for( int i = 0; i < rating / 2; ++i )
179
                s += QString( "<img src=\"%1\" height=\"%2\" width=\"%3\">" )
180
                        .arg( KStandardDirs::locate( "data", "amarok/images/star.png" ) )
181
                        .arg( QFontMetrics( QToolTip::font() ).height() )
182
                        .arg( QFontMetrics( QToolTip::font() ).height() );
183
            if( rating % 2 )
184
                s += QString( "<img src=\"%1\" height=\"%2\" width=\"%3\">" )
185
                        .arg( KStandardDirs::locate( "data", "amarok/images/smallstar.png" ) )
186
                        .arg( QFontMetrics( QToolTip::font() ).height() )
187
                        .arg( QFontMetrics( QToolTip::font() ).height() );
188
            right << s;
189
            left << i18n( "Rating" );
190
        }
191
192
        const int count = m_track->playCount();
193
        if( count > 0 )
194
        {
195
            right << QString::number( count );
196
            left << i18n( "Play Count" );
197
        }
198
199
        const uint lastPlayed = m_track->lastPlayed();
200
        right << Amarok::verboseTimeSince( lastPlayed );
201
        left << i18n( "Last Played" );
202
203
        if( m_trackLength > 0 )
204
        {
205
            right << Meta::secToPrettyTime( m_trackLength );
206
            left << i18n( "Length" );
207
        }
208
209
        // NOTE: It seems to be necessary to <center> each element indivdually
210
        const QString tableRow = "<tr><td align='right'>%1: </td><td align='left'>%2</td></tr>";
211
        for( int x = 0; x < left.count(); ++x )
212
            if ( !right[x].isEmpty() )
213
                tooltip += tableRow.arg( left[x] ).arg( right[x] );
214
215
        tooltip += "</table>";
216
217
        setToolTip( tooltip );
218
    }
219
    else
220
    {
221
        setToolTip( i18n( "Amarok - No track playing" ) );
222
    }
223
}
224
225
bool
226
Amarok::TrayIcon::event( QEvent *e )
227
{
228
    switch( e->type() )
229
    {
230
    case QEvent::DragEnter:
231
        #define e static_cast<QDragEnterEvent*>(e)
232
        e->setAccepted( KUrl::List::canDecode( e->mimeData() ) );
233
        break;
234
        #undef e
235
236
    case QEvent::Drop:
237
        #define e static_cast<QDropEvent*>(e)
238
        {
239
            const KUrl::List list = KUrl::List::fromMimeData( e->mimeData() );
240
            if( !list.isEmpty() )
241
            {
242
                KMenu *popup = new KMenu;
243
                popup->addAction( KIcon( "media-track-add-amarok" ), i18n( "&Append to Playlist" ), this, SLOT( appendDrops() ) );
244
                popup->addAction( KIcon( "media-track-add-amarok" ), i18n( "Append && &Play" ), this, SLOT( appendAndPlayDrops() ) );
245
                if( The::playlistModel()->activeRow() >= 0 )
246
                    popup->addAction( KIcon( "go-next-amarok" ), i18n( "&Queue Track" ), this, SLOT( queueDrops() ) );
247
248
                popup->addSeparator();
249
                popup->addAction( i18n( "&Cancel" ) );
250
                popup->exec( e->pos() );
251
            }
252
            break;
253
        }
254
        #undef e
255
256
    case QEvent::Wheel:
257
        #define e static_cast<QWheelEvent*>(e)
258
        if( e->modifiers() == Qt::ControlModifier )
259
        {
260
            const bool up = e->delta() > 0;
261
            if( up ) The::playlistActions()->back();
262
            else     The::playlistActions()->next();
263
            break;
264
        }
265
        else if( e->modifiers() == Qt::ShiftModifier )
266
        {
267
            The::engineController()->seekRelative( (e->delta() / 120) * 5000 ); // 5 seconds for keyboard seeking
268
            break;
269
        }
270
        else
271
            The::engineController()->increaseVolume( e->delta() / Amarok::VOLUME_SENSITIVITY );
272
273
        e->accept();
274
        #undef e
275
        break;
276
277
    default:
278
        return KSystemTrayIcon::event( e );
279
    }
280
    return true;
281
}
282
283
void
284
Amarok::TrayIcon::engineStateChanged( Phonon::State state, Phonon::State /*oldState*/ )
285
{
286
    switch( state )
287
    {
288
        case Phonon::PlayingState:
289
            m_track = The::engineController()->currentTrack();
290
            m_trackLength = m_track ? m_track->length() : 0;
291
292
            paintIcon( 0 );
293
            setupMenu();
294
            break;
295
296
        case Phonon::StoppedState:
297
            m_track = 0;
298
            m_trackLength = 0;
299
300
            paintIcon();
301
            setupMenu(); // remove custom track actions on stop
302
            break;
303
304
        case Phonon::PausedState:
305
            blendOverlay( m_pauseOverlay );
306
            break;
307
308
        case Phonon::LoadingState:
309
        case Phonon::ErrorState:
310
        case Phonon::BufferingState:
311
            break;
312
    }
313
314
    setupToolTip();
315
}
316
317
void
318
Amarok::TrayIcon::engineNewTrackPlaying()
319
{
320
    setupToolTip();
321
    setupMenu();
322
}
323
324
void
325
Amarok::TrayIcon::engineNewMetaData( const QHash<qint64, QString> &newMetaData, bool trackChanged )
326
{
327
    Q_UNUSED( trackChanged )
328
    Q_UNUSED( &newMetaData );
329
330
    setupToolTip();
331
    setupMenu();
332
}
333
334
void
335
Amarok::TrayIcon::engineTrackPositionChanged( long position, bool userSeek )
336
{
337
    Q_UNUSED( userSeek );
338
339
    if( m_trackLength )
340
        paintIcon( position );
341
}
342
343
void
344
Amarok::TrayIcon::engineVolumeChanged( int percent )
345
{
346
    Q_UNUSED( percent );
347
348
    setupToolTip();
349
}
350
351
void
352
Amarok::TrayIcon::engineMuteStateChanged( bool mute )
353
{
354
    Q_UNUSED( mute );
355
356
    setupToolTip();
357
}
358
359
void
360
Amarok::TrayIcon::paletteChange( const QPalette & op )
361
{
362
    Q_UNUSED( op );
363
364
    paintIcon();
365
}
366
367
void
368
Amarok::TrayIcon::paintIcon( long trackPosition )
369
{
370
    static int oldMergePos = -1;
371
372
    // start up
373
    if( m_baseIcon.isNull() )
374
    {
375
        QIcon icon = KSystemTrayIcon::loadIcon( "amarok" );
376
        m_baseIcon = icon.pixmap( geometry().size() );
377
        setIcon( icon ); // show icon
378
        return; // return because m_baseIcon is still null after first startup
379
    }
380
381
    if( m_grayedIcon.isNull() )
382
    {
383
        m_grayedIcon = m_baseIcon; // copies object
384
        KIconEffect::semiTransparent( m_grayedIcon );
385
    }
386
387
    // trackPosition < 0 means reset
388
    if( trackPosition < 0 )
389
    {
390
        oldMergePos = -1;
391
        setIcon( m_baseIcon );
392
        return;
393
    }
394
395
    // check if we are playing a stream
396
    if( !m_trackLength )
397
    {
398
        m_icon = m_baseIcon;
399
        blendOverlay( m_playOverlay );
400
        return;
401
    }
402
403
    const int mergePos = ( ( float( trackPosition ) / 1000 ) / m_trackLength ) * geometry().height();
404
405
    // return if pixmap would stay the same
406
    if( oldMergePos == mergePos )
407
        return;
408
409
    // draw m_baseIcon on top of the gray version
410
    m_icon = m_grayedIcon; // copies object
411
    QPainter p( &m_icon );
412
    p.drawPixmap( 0, 0, m_baseIcon, 0, 0, 0, geometry().height() - mergePos );
413
    p.end();
414
415
    oldMergePos = mergePos;
416
417
    blendOverlay( m_playOverlay );
418
}
419
420
void
421
Amarok::TrayIcon::blendOverlay( const QPixmap &overlay )
422
{
423
    if ( !overlay.isNull() )
424
    {
425
        // draw overlay at bottom right
426
        const int x = geometry().size().width() - overlay.size().width();
427
        const int y = geometry().size().height() - overlay.size().width();
428
        QPainter p( &m_icon );
429
        p.drawPixmap( x, y, overlay );
430
        p.end();
431
        setIcon( m_icon );
432
    }
433
}
434
435
void
436
Amarok::TrayIcon::setupMenu()
437
{
438
    foreach( QAction* action, m_extraActions )
439
        contextMenu()->removeAction( action );
440
    
441
    contextMenu()->removeAction( m_separator );
442
    
443
    delete m_separator;
444
445
    if( !m_track )
446
        return;
447
448
    m_extraActions.clear();
449
    foreach( QAction *action, The::globalCurrentTrackActions()->actions() )
450
        m_extraActions.append( action );
451
452
    if ( m_track->hasCapabilityInterface( Meta::Capability::CurrentTrackActions ) )
453
    {
454
        Meta::CurrentTrackActionsCapability *cac = m_track->create<Meta::CurrentTrackActionsCapability>();
455
        if( cac )
456
        {
457
            QList<PopupDropperAction *> currentTrackActions = cac->customActions();
458
            foreach( PopupDropperAction *action, currentTrackActions )
459
                m_extraActions.append( action );
460
        }
461
        delete cac;
462
    }
463
464
    if ( m_extraActions.count() > 0 )
465
    {
466
        // remove the two bottom items, so we can push them to the button again
467
        contextMenu()->removeAction( actionCollection()->action( "file_quit" ) );
468
        contextMenu()->removeAction( actionCollection()->action( "minimizeRestore" ) );
469
470
        foreach( QAction* action, m_extraActions )
471
            contextMenu()->addAction( action );
472
473
        m_separator = contextMenu()->addSeparator();
474
        // readd
475
        contextMenu()->addAction( actionCollection()->action( "minimizeRestore" ) );
476
        contextMenu()->addAction( actionCollection()->action( "file_quit" ) );
477
    }
478
}
479
480
void
481
Amarok::TrayIcon::slotActivated( QSystemTrayIcon::ActivationReason reason )
482
{
483
    if( reason == QSystemTrayIcon::MiddleClick )
484
        The::engineController()->playPause();
485
}
486
487
#include "Systray.moc"