systemtray.cpp 15.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
/**
 * Copyright (C) 2002 Daniel Molkentin <molkentin@kde.org>
 * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org>
 * Copyright (C) 2004-2009 Michael Pyne <mpyne@kde.org>
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <http://www.gnu.org/licenses/>.
 */
18

19 20
#include "systemtray.h"

21
#include <kiconloader.h>
22
#include <kactioncollection.h>
Laurent Montel's avatar
Laurent Montel committed
23
#include <kactionmenu.h>
24
#include <kwindowsystem.h>
Michael Pyne's avatar
Michael Pyne committed
25
#include <KLocalizedString>
26

27 28
#include <QAction>
#include <QMenu>
29
#include <QTimer>
30
#include <QWheelEvent>
31 32
#include <QColor>
#include <QPushButton>
33
#include <QPalette>
Laurent Montel's avatar
Laurent Montel committed
34 35
#include <QPixmap>
#include <QLabel>
36
#include <QIcon>
37
#include <QApplication>
38

39
#include "tag.h"
40
#include "actioncollection.h"
41
#include "playermanager.h"
42
#include "coverinfo.h"
43
#include "juk_debug.h"
44

45 46
using namespace ActionCollection;

47
PassiveInfo::PassiveInfo() :
48
    QFrame(nullptr,
49
        Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint
50
    ),
51
    m_timer(new QTimer(this)),
52 53
    m_layout(new QVBoxLayout(this)),
    m_justDie(false)
54
{
55
    connect(m_timer, SIGNAL(timeout()), SLOT(timerExpired()));
56 57
    setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);

58 59 60
    // Workaround transparent background in Oxygen when (ab-)using Qt::ToolTip
    setAutoFillBackground(true);

61 62
    setFrameStyle(StyledPanel | Plain);
    setLineWidth(2);
63 64
}

65
void PassiveInfo::startTimer(int delay)
66
{
67
    m_timer->start(delay);
68 69 70 71 72
}

void PassiveInfo::show()
{
    m_timer->start(3500);
73
    setWindowOpacity(1.0);
74 75 76 77 78 79 80 81 82
    QFrame::show();
}

void PassiveInfo::setView(QWidget *view)
{
    m_layout->addWidget(view);
    view->show(); // We are still hidden though.
    adjustSize();
    positionSelf();
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
}

void PassiveInfo::timerExpired()
{
    // If m_justDie is set, we should just go, otherwise we should emit the
    // signal and wait for the system tray to delete us.
    if(m_justDie)
        hide();
    else
        emit timeExpired();
}

void PassiveInfo::enterEvent(QEvent *)
{
    m_timer->stop();
    emit mouseEntered();
}

void PassiveInfo::leaveEvent(QEvent *)
{
    m_justDie = true;
    m_timer->start(50);
}
106

107 108 109 110
void PassiveInfo::hideEvent(QHideEvent *)
{
}

111 112 113 114 115 116 117 118 119 120 121 122
void PassiveInfo::wheelEvent(QWheelEvent *e)
{
    if(e->delta() >= 0) {
        emit nextSong();
    }
    else {
        emit previousSong();
    }

    e->accept();
}

123 124 125 126
void PassiveInfo::positionSelf()
{
    // Start with a QRect of our size, move it to the right spot.
    QRect r(rect());
127
    QRect curScreen(KWindowSystem::workArea());
128

129 130
    // Try to position in lower right of the screen
    QPoint anchor(curScreen.right() * 7 / 8, curScreen.bottom());
131 132

    // Now make our rect hit that anchor.
133
    r.moveBottomRight(anchor);
134 135 136 137

    move(r.topLeft());
}

138 139 140 141
////////////////////////////////////////////////////////////////////////////////
// public methods
////////////////////////////////////////////////////////////////////////////////

142 143 144 145 146 147 148
SystemTray::SystemTray(PlayerManager *player, QWidget *parent) :
    KStatusNotifierItem(parent),
    m_popup(0),
    m_player(player),
    m_fadeTimer(0),
    m_fade(true),
    m_hasCompositionManager(false)
149
{
Michael Pyne's avatar
Michael Pyne committed
150 151
    using ActionCollection::action; // Override the KSNI::action call introduced in KF5

152
    // This should be initialized to the number of labels that are used.
153
    m_labels.fill(0, 3);
154

155 156
    setIconByName("juk");
    setCategory(ApplicationStatus);
157
    setStatus(Active); // We were told to dock in systray by user, force us visible
158

159 160
    m_forwardPix = QIcon::fromTheme("media-skip-forward");
    m_backPix = QIcon::fromTheme("media-skip-backward");
161

162 163
    // Just create this here so that it show up in the DBus interface and the
    // key bindings dialog.
164

Michael Pyne's avatar
Michael Pyne committed
165
    QAction *rpaction = new QAction(i18n("Redisplay Popup"), this);
166
    ActionCollection::actions()->addAction("showPopup", rpaction);
167
    connect(rpaction, SIGNAL(triggered(bool)), SLOT(slotPlay()));
168

169
    QMenu *cm = contextMenu();
170

171 172 173
    connect(m_player, SIGNAL(signalPlay()), this, SLOT(slotPlay()));
    connect(m_player, SIGNAL(signalPause()), this, SLOT(slotPause()));
    connect(m_player, SIGNAL(signalStop()), this, SLOT(slotStop()));
174

175 176 177 178 179
    cm->addAction( action("play") );
    cm->addAction( action("pause") );
    cm->addAction( action("stop") );
    cm->addAction( action("forward") );
    cm->addAction( action("back") );
180

Laurent Montel's avatar
Laurent Montel committed
181
    cm->addSeparator();
182

183 184
    // Pity the actionCollection doesn't keep track of what sub-menus it has.

185
    KActionMenu *menu = new KActionMenu(i18n("&Random Play"), this);
186
        // FIXME
187
    //actionCollection()->addAction("randomplay", menu);
188 189 190
    menu->addAction(action("disableRandomPlay"));
    menu->addAction(action("randomPlay"));
    menu->addAction(action("albumRandomPlay"));
191
    cm->addAction( menu );
192

193
    cm->addAction( action("togglePopups") );
194

Tim Beaulen's avatar
Tim Beaulen committed
195
    m_fadeTimer = new QTimer(this);
Laurent Montel's avatar
Laurent Montel committed
196
    m_fadeTimer->setObjectName( QLatin1String("systrayFadeTimer" ));
197
    connect(m_fadeTimer, SIGNAL(timeout()), SLOT(slotNextStep()));
198 199

    // Handle wheel events
Laurent Montel's avatar
Laurent Montel committed
200
    connect(this, SIGNAL(scrollRequested(int,Qt::Orientation)), SLOT(scrollEvent(int,Qt::Orientation)));
201 202

    // Add a quick hook for play/pause toggle
Laurent Montel's avatar
Laurent Montel committed
203
    connect(this, SIGNAL(secondaryActivateRequested(QPoint)),
204
            action("playPause"), SLOT(trigger()));
205

206
    if(m_player->playing())
207
        slotPlay();
208
    else if(m_player->paused())
209
        slotPause();
210 211 212 213 214 215
}

////////////////////////////////////////////////////////////////////////////////
// public slots
////////////////////////////////////////////////////////////////////////////////

216 217
void SystemTray::slotPlay()
{
218
    if(!m_player->playing())
219 220
        return;

221
    QPixmap cover = m_player->playingFile().coverInfo()->pixmap(CoverInfo::FullSize);
222

223
    setOverlayIconByName("media-playback-start");
224
    setToolTip(m_player->playingString(), cover);
225
    createPopup();
226 227
}

228 229 230 231 232 233
void SystemTray::slotPause()
{
    setOverlayIconByName("media-playback-pause");
}

void SystemTray::slotPopupLargeCover()
234
{
235
    if(!m_player->playing())
236 237
        return;

238
    FileHandle playingFile = m_player->playingFile();
239
    playingFile.coverInfo()->popup();
240 241
}

242 243
void SystemTray::slotStop()
{
244
    setToolTip();
245
    setOverlayIconByName(QString());
246 247 248

    delete m_popup;
    m_popup = 0;
249
    m_fadeTimer->stop();
250 251
}

252
void SystemTray::slotPopupDestroyed()
253
{
254
    for(int i = 0; i < m_labels.size(); ++i)
255 256 257 258 259
        m_labels[i] = 0;
}

void SystemTray::slotNextStep()
{
260 261 262
    // Could happen I guess if the timeout event were queued while we're deleting m_popup
    if(!m_popup)
        return;
263 264 265

    ++m_step;

266 267
    // If we're not fading, immediately stop the fadeout
    if(!m_fade || m_step == STEPS) {
268 269 270
        m_step = 0;
        m_fadeTimer->stop();
        emit fadeDone();
271 272 273 274 275 276 277 278 279 280 281 282 283 284
        return;
    }

    if(m_hasCompositionManager) {
        m_popup->setWindowOpacity((1.0 * STEPS - m_step) / STEPS);
    }
    else {
        QColor result = interpolateColor(m_step);

        for(int i = 0; i < m_labels.size() && m_labels[i]; ++i) {
            QPalette palette;
            palette.setColor(m_labels[i]->foregroundRole(), result);
            m_labels[i]->setPalette(palette);
        }
285
    }
286 287 288 289
}

void SystemTray::slotFadeOut()
{
290 291
    m_startColor = m_labels[0]->palette().color( QPalette::Text ); //textColor();
    m_endColor = m_labels[0]->palette().color( QPalette::Window ); //backgroundColor();
292

293
    m_hasCompositionManager = KWindowSystem::compositingActive();
294

295 296 297 298 299 300 301 302 303 304
    connect(this, SIGNAL(fadeDone()), m_popup, SLOT(hide()));
    connect(m_popup, SIGNAL(mouseEntered()), this, SLOT(slotMouseInPopup()));
    m_fadeTimer->start(1500 / STEPS);
}

// If we receive this signal, it's because we were called during fade out.
// That means there is a single shot timer about to call slotNextStep, so we
// don't have to do it ourselves.
void SystemTray::slotMouseInPopup()
{
305
    m_endColor = m_labels[0]->palette().color( QPalette::Text ); //textColor();
306
    disconnect(SIGNAL(fadeDone()));
307

308 309 310
    if(m_hasCompositionManager)
        m_popup->setWindowOpacity(1.0);

311 312
    m_step = STEPS - 1; // Simulate end of fade to solid text
    slotNextStep();
313 314
}

315 316 317 318
////////////////////////////////////////////////////////////////////////////////
// private methods
////////////////////////////////////////////////////////////////////////////////

319
QWidget *SystemTray::createInfoBox(QBoxLayout *parentLayout, const FileHandle &file)
320
{
321
    // We always show the popup on the right side of the current screen, so
322
    // this logic assumes that.  Earlier revisions had logic for popup being
323 324
    // wherever the systray icon is, so if it's decided to go that route again,
    // dig into the source control history. --mpyne
325

326
    if(file.coverInfo()->hasCover()) {
327 328
        addCoverButton(parentLayout, file.coverInfo()->pixmap(CoverInfo::Thumbnail));
        addSeparatorLine(parentLayout);
329
    }
330

331 332 333 334 335 336
    auto infoBox = new QWidget;
    auto infoBoxVLayout = new QVBoxLayout(infoBox);
    infoBoxVLayout->setSpacing(3);
    infoBoxVLayout->setMargin(3);

    parentLayout->addWidget(infoBox);
337

338 339
    addSeparatorLine(parentLayout);
    createButtonBox(parentLayout);
340

341 342
    return infoBox;
}
343

344 345
void SystemTray::createPopup()
{
346
    FileHandle playingFile = m_player->playingFile();
347
    Tag *playingInfo = playingFile.tag();
348

349
    // If the action exists and it's checked, do our stuff
350

351
    if(!ActionCollection::action("togglePopups")->isChecked())
352
        return;
353

354 355 356
    delete m_popup;
    m_popup = 0;
    m_fadeTimer->stop();
357

358 359 360 361
    // This will be reset after this function call by slot(Forward|Back)
    // so it's safe to set it true here.
    m_fade = true;
    m_step = 0;
362

363
    m_popup = new PassiveInfo;
364 365
    connect(m_popup, SIGNAL(destroyed()), SLOT(slotPopupDestroyed()));
    connect(m_popup, SIGNAL(timeExpired()), SLOT(slotFadeOut()));
366 367
    connect(m_popup, SIGNAL(nextSong()), SLOT(slotForward()));
    connect(m_popup, SIGNAL(previousSong()), SLOT(slotBack()));
368

369 370
    auto box = new QWidget;
    auto boxHLayout = new QHBoxLayout(box);
371

372 373 374 375
    boxHLayout->setSpacing(15); // Add space between text and buttons

    QWidget *infoBox = createInfoBox(boxHLayout, playingFile);
    QLayout *infoBoxLayout = infoBox->layout();
376

377
    for(int i = 0; i < m_labels.size(); ++i) {
378
        QLabel *l = new QLabel(" ");
379 380
        l->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
        m_labels[i] = l;
381
        infoBoxLayout->addWidget(l);
382
    }
383

384 385 386
    // We have to set the text of the labels after all of the
    // widgets have been added in order for the width to be calculated
    // correctly.
387

388
    int labelCount = 0;
389

Michael Pyne's avatar
Michael Pyne committed
390
    QString title = playingInfo->title().toHtmlEscaped();
391
    m_labels[labelCount++]->setText(QString("<qt><nobr><h2>%1</h2></nobr></qt>").arg(title));
392

393 394
    if(!playingInfo->artist().isEmpty())
        m_labels[labelCount++]->setText(playingInfo->artist());
395

396
    if(!playingInfo->album().isEmpty()) {
Michael Pyne's avatar
Michael Pyne committed
397
        QString album = playingInfo->album().toHtmlEscaped();
398 399 400 401
        QString s = playingInfo->year() > 0
            ? QString("<qt><nobr>%1 (%2)</nobr></qt>").arg(album).arg(playingInfo->year())
            : QString("<qt><nobr>%1</nobr></qt>").arg(album);
        m_labels[labelCount++]->setText(s);
402
    }
403 404 405

    m_popup->setView(box);
    m_popup->show();
406 407
}

408
void SystemTray::createButtonBox(QBoxLayout *parentLayout)
409
{
410 411
    auto buttonBox = new QWidget;
    auto buttonBoxVLayout = new QVBoxLayout(buttonBox);
412

413
    buttonBoxVLayout->setSpacing(3);
414

415 416
    QPushButton *forwardButton = new QPushButton(m_forwardPix, QString());
    forwardButton->setObjectName(QLatin1String("popup_forward"));
417
    connect(forwardButton, SIGNAL(clicked()), SLOT(slotForward()));
418

419 420
    QPushButton *backButton = new QPushButton(m_backPix, QString());
    backButton->setObjectName(QLatin1String("popup_back"));
421
    connect(backButton, SIGNAL(clicked()), SLOT(slotBack()));
422 423 424 425

    buttonBoxVLayout->addWidget(forwardButton);
    buttonBoxVLayout->addWidget(backButton);
    parentLayout->addWidget(buttonBox);
426 427 428
}

/**
David Faure's avatar
David Faure committed
429
 * What happens here is that the action->trigger() call will end up invoking
430 431 432 433 434
 * createPopup(), which sets m_fade to true.  Before the text starts fading
 * control returns to this function, which resets m_fade to false.
 */
void SystemTray::slotBack()
{
435
    ActionCollection::action("back")->trigger();
436 437 438 439 440
    m_fade = false;
}

void SystemTray::slotForward()
{
441
    ActionCollection::action("forward")->trigger();
442
    m_fade = false;
443 444
}

445
void SystemTray::addSeparatorLine(QBoxLayout *parentLayout)
446
{
447
    QFrame *line = new QFrame;
Laurent Montel's avatar
Laurent Montel committed
448
    line->setFrameShape(QFrame::VLine);
449 450 451 452 453

    // Cover art takes up 80 pixels, make sure we take up at least 80 pixels
    // even if we don't show the cover art for consistency.

    line->setMinimumHeight(80);
454 455

    parentLayout->addWidget(line);
456 457
}

458
void SystemTray::addCoverButton(QBoxLayout *parentLayout, const QPixmap &cover)
459
{
460
    QPushButton *coverButton = new QPushButton;
461

462
    coverButton->setIconSize(cover.size());
Tim Beaulen's avatar
Tim Beaulen committed
463
    coverButton->setIcon(cover);
464
    coverButton->setFixedSize(cover.size());
465 466 467
    coverButton->setFlat(true);

    connect(coverButton, SIGNAL(clicked()), this, SLOT(slotPopupLargeCover()));
468 469

    parentLayout->addWidget(coverButton);
470 471
}

472 473 474 475 476 477 478 479 480 481 482
QColor SystemTray::interpolateColor(int step, int steps)
{
    if(step < 0)
        return m_startColor;
    if(step >= steps)
        return m_endColor;

    // TODO: Perhaps the algorithm here could be better?  For example, it might
    // make sense to go rather quickly from start to end and then slow down
    // the progression.
    return QColor(
483
            (step * m_endColor.red()   + (steps - step) * m_startColor.red())   / steps,
484
            (step * m_endColor.green() + (steps - step) * m_startColor.green()) / steps,
485
            (step * m_endColor.blue()  + (steps - step) * m_startColor.blue())  / steps
486 487 488
           );
}

489
void SystemTray::setToolTip(const QString &tip, const QPixmap &cover)
490
{
491
    if(tip.isEmpty())
492
        KStatusNotifierItem::setToolTip("juk", i18n("JuK"), QString());
493
    else {
494
        QIcon myCover;
495
        if(cover.isNull()) {
496
            myCover = QIcon::fromTheme("juk");
497 498
        } else {
            //Scale to proper icon size, otherwise KStatusNotifierItem will show an unknown icon
499 500
            const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Desktop);
            myCover = QIcon(cover.scaled(iconSize, iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
501
        }
502

503
        KStatusNotifierItem::setToolTip(myCover, i18n("JuK"), tip);
504
    }
505 506
}

507
void SystemTray::scrollEvent(int delta, Qt::Orientation orientation)
508
{
509
    if(orientation == Qt::Horizontal)
510
        return;
511

512
    switch(QApplication::queryKeyboardModifiers()) {
513
    case Qt::ShiftModifier:
514
        if(delta > 0)
515
            ActionCollection::action("volumeUp")->trigger();
516
        else
517
            ActionCollection::action("volumeDown")->trigger();
518
        break;
519
    default:
520
        if(delta > 0)
521
            ActionCollection::action("forward")->trigger();
522
        else
523
            ActionCollection::action("back")->trigger();
524
        break;
525
    }
526 527
}

528
// vim: set et sw=4 tw=0 sta: