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