File indexing completed on 2023-05-30 11:30:52

0001 /**
0002  * Copyright (C) 2002 Daniel Molkentin <molkentin@kde.org>
0003  * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org>
0004  * Copyright (C) 2004-2009 Michael Pyne <mpyne@kde.org>
0005  *
0006  * This program is free software; you can redistribute it and/or modify it under
0007  * the terms of the GNU General Public License as published by the Free Software
0008  * Foundation; either version 2 of the License, or (at your option) any later
0009  * version.
0010  *
0011  * This program is distributed in the hope that it will be useful, but WITHOUT ANY
0012  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
0013  * PARTICULAR PURPOSE. See the GNU General Public License for more details.
0014  *
0015  * You should have received a copy of the GNU General Public License along with
0016  * this program.  If not, see <http://www.gnu.org/licenses/>.
0017  */
0018 
0019 #include "systemtray.h"
0020 
0021 #include <kiconloader.h>
0022 #include <kactioncollection.h>
0023 #include <kactionmenu.h>
0024 #include <kwindowsystem.h>
0025 #include <KLocalizedString>
0026 #include <KX11Extras>
0027 
0028 #include <QAction>
0029 #include <QMenu>
0030 #include <QTimer>
0031 #include <QWheelEvent>
0032 #include <QColor>
0033 #include <QPushButton>
0034 #include <QPalette>
0035 #include <QPixmap>
0036 #include <QScreen>
0037 #include <QLabel>
0038 #include <QIcon>
0039 #include <QApplication>
0040 
0041 #include "actioncollection.h"
0042 #include "coverinfo.h"
0043 #include "iconsupport.h"
0044 #include "juk_debug.h"
0045 #include "juktag.h"
0046 #include "playermanager.h"
0047 
0048 using namespace IconSupport; // ""_icon
0049 
0050 PassiveInfo::PassiveInfo()
0051   : QFrame(nullptr, Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint)
0052   , m_startFadeTimer(new QTimer(this))
0053   , m_layout(new QVBoxLayout(this))
0054   , m_justDie(false)
0055 {
0056     connect(m_startFadeTimer, &QTimer::timeout, this, &PassiveInfo::timerExpired);
0057     m_startFadeTimer->setSingleShot(true);
0058 
0059     setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
0060 
0061     // Workaround transparent background in Oxygen when (ab-)using Qt::ToolTip
0062     setAutoFillBackground(true);
0063 
0064     setFrameStyle(StyledPanel | Plain);
0065     setLineWidth(2);
0066 }
0067 
0068 void PassiveInfo::show()
0069 {
0070     m_startFadeTimer->start(3500);
0071     setWindowOpacity(1.0);
0072     QFrame::show();
0073 }
0074 
0075 void PassiveInfo::setView(QWidget *view)
0076 {
0077     m_layout->addWidget(view);
0078     view->show(); // We are still hidden though.
0079     adjustSize();
0080     positionSelf();
0081 }
0082 
0083 void PassiveInfo::timerExpired()
0084 {
0085     // If m_justDie is set, we should just go, otherwise we should emit the
0086     // signal and wait for the system tray to delete us.
0087     if(m_justDie)
0088         hide();
0089     else
0090         emit timeExpired();
0091 }
0092 
0093 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0094 void PassiveInfo::enterEvent (QEnterEvent *)
0095 #else
0096 void PassiveInfo::enterEvent (QEvent *)
0097 #endif
0098 {
0099     m_startFadeTimer->stop();
0100     emit mouseEntered();
0101 }
0102 
0103 void PassiveInfo::leaveEvent(QEvent *)
0104 {
0105     m_justDie = true;
0106     m_startFadeTimer->start(50);
0107 }
0108 
0109 void PassiveInfo::wheelEvent(QWheelEvent *e)
0110 {
0111     if(e->angleDelta().y() >= 0) {
0112         emit nextSong();
0113     }
0114     else {
0115         emit previousSong();
0116     }
0117 
0118     e->accept();
0119 }
0120 
0121 void PassiveInfo::positionSelf()
0122 {
0123     // Start with a QRect of our size, move it to the right spot.
0124     QRect r(rect());
0125     QRect curScreen(screen()->availableGeometry());
0126 
0127     // Try to position in lower right of the screen
0128     QPoint anchor(curScreen.right() * 7 / 8, curScreen.bottom());
0129 
0130     // Now make our rect hit that anchor.
0131     r.moveBottomRight(anchor);
0132 
0133     KWindowSystem::setType(winId(), KWindowSystem::NET::Notification);
0134 
0135     move(r.topLeft());
0136 }
0137 
0138 ////////////////////////////////////////////////////////////////////////////////
0139 // public methods
0140 ////////////////////////////////////////////////////////////////////////////////
0141 
0142 SystemTray::SystemTray(PlayerManager *player, QWidget *parent)
0143   : KStatusNotifierItem(parent)
0144   , m_player(player)
0145 {
0146     using ActionCollection::action; // Override the KSNI::action call introduced in KF5
0147 
0148     // This should be initialized to the number of labels that are used.
0149     m_labels.fill(nullptr, 3);
0150 
0151     setIconByName("juk");
0152     setCategory(ApplicationStatus);
0153     setStatus(Active); // We were told to dock in systray by user, force us visible
0154 
0155     m_forwardPix = "media-skip-forward"_icon;
0156     m_backPix = "media-skip-backward"_icon;
0157 
0158     // Just create this here so that it show up in the DBus interface and the
0159     // key bindings dialog.
0160 
0161     QAction *rpaction = new QAction(i18n("Redisplay Popup"), this);
0162     ActionCollection::actions()->addAction("showPopup", rpaction);
0163     connect(rpaction, &QAction::triggered, this, &SystemTray::slotPlay);
0164 
0165     QMenu *cm = contextMenu();
0166 
0167     connect(m_player, &PlayerManager::signalPlay,
0168             this, &SystemTray::slotPlay);
0169     connect(m_player, &PlayerManager::signalPause,
0170             this, &SystemTray::slotPause);
0171     connect(m_player, &PlayerManager::signalStop,
0172             this, &SystemTray::slotStop);
0173 
0174     cm->addAction(action("play"));
0175     cm->addAction(action("pause"));
0176     cm->addAction(action("stop"));
0177     cm->addAction(action("forward"));
0178     cm->addAction(action("back"));
0179 
0180     cm->addSeparator();
0181 
0182     // Pity the actionCollection doesn't keep track of what sub-menus it has.
0183 
0184     KActionMenu *menu = new KActionMenu(i18n("&Random Play"), this);
0185 
0186     menu->addAction(action("disableRandomPlay"));
0187     menu->addAction(action("randomPlay"));
0188     menu->addAction(action("albumRandomPlay"));
0189     cm->addAction(menu);
0190 
0191     cm->addAction(action("togglePopups"));
0192 
0193     m_fadeStepTimer = new QTimer(this);
0194     m_fadeStepTimer->setObjectName(QLatin1String("systrayFadeTimer"));
0195 
0196     // Handle wheel events
0197     connect(this, &KStatusNotifierItem::scrollRequested,
0198             this, &SystemTray::scrollEvent);
0199 
0200     // Add a quick hook for play/pause toggle
0201     connect(this, &KStatusNotifierItem::secondaryActivateRequested,
0202             action("playPause"), &QAction::trigger);
0203 
0204     if(m_player->playing())
0205         slotPlay();
0206     else if(m_player->paused())
0207         slotPause();
0208 }
0209 
0210 ////////////////////////////////////////////////////////////////////////////////
0211 // public slots
0212 ////////////////////////////////////////////////////////////////////////////////
0213 
0214 void SystemTray::slotPlay()
0215 {
0216     if(!m_player->playing())
0217         return;
0218 
0219     QPixmap cover = m_player->playingFile().coverInfo()->pixmap(CoverInfo::FullSize);
0220 
0221     setOverlayIconByName("media-playback-start");
0222     setToolTip(m_player->playingString(), cover);
0223     createPopup();
0224 }
0225 
0226 void SystemTray::slotPause()
0227 {
0228     setOverlayIconByName("media-playback-pause");
0229 }
0230 
0231 void SystemTray::slotPopupLargeCover()
0232 {
0233     if(!m_player->playing())
0234         return;
0235 
0236     const FileHandle playingFile = m_player->playingFile();
0237     playingFile.coverInfo()->popup();
0238 }
0239 
0240 void SystemTray::slotStop()
0241 {
0242     setToolTip();
0243     setOverlayIconByName(QString());
0244 
0245     delete m_popup;
0246     m_popup = nullptr;
0247     m_fadeStepTimer->stop();
0248 }
0249 
0250 void SystemTray::slotPopupDestroyed()
0251 {
0252     m_labels.fill(nullptr);
0253 }
0254 
0255 void SystemTray::slotNextStep()
0256 {
0257     ++m_step;
0258 
0259     // If we're not fading, immediately stop the fadeout
0260     if(!m_fade || m_step == STEPS) {
0261         m_step = 0;
0262         m_fadeStepTimer->stop();
0263         emit fadeDone();
0264         return;
0265     }
0266 
0267     if(m_hasCompositionManager) {
0268         m_popup->setWindowOpacity((1.0 * STEPS - m_step) / STEPS);
0269     }
0270     else {
0271         const QColor result = interpolateColor(m_step);
0272 
0273         for(auto &label : m_labels) {
0274             QPalette palette(label->palette());
0275             palette.setColor(label->foregroundRole(), result);
0276             label->setPalette(palette);
0277         }
0278     }
0279 }
0280 
0281 void SystemTray::slotFadeOut()
0282 {
0283     m_startColor = m_labels[0]->palette().color(QPalette::Text); //textColor();
0284     m_endColor = m_labels[0]->palette().color(QPalette::Window); //backgroundColor();
0285 
0286     m_hasCompositionManager = true;
0287     if(KWindowSystem::isPlatformX11()) {
0288         m_hasCompositionManager = KX11Extras::compositingActive();
0289     }
0290 
0291     connect(this, &SystemTray::fadeDone,
0292             m_popup, &QWidget::hide);
0293     connect(m_popup, &PassiveInfo::mouseEntered,
0294             this, &SystemTray::slotMouseInPopup);
0295 
0296     m_fadeStepTimer->start(1500 / STEPS);
0297 }
0298 
0299 // If we receive this signal, it's because we were called during fade out.
0300 // That means there is a single shot timer about to call slotNextStep, so we
0301 // don't have to do it ourselves.
0302 void SystemTray::slotMouseInPopup()
0303 {
0304     m_endColor = m_labels[0]->palette().color(QPalette::Text); //textColor();
0305     disconnect(this, &SystemTray::fadeDone, nullptr, nullptr);
0306 
0307     if(m_hasCompositionManager)
0308         m_popup->setWindowOpacity(1.0);
0309 
0310     m_step = STEPS - 1; // Simulate end of fade to solid text
0311     slotNextStep();
0312 }
0313 
0314 ////////////////////////////////////////////////////////////////////////////////
0315 // private methods
0316 ////////////////////////////////////////////////////////////////////////////////
0317 
0318 QWidget *SystemTray::createInfoBox(QBoxLayout *parentLayout, const FileHandle &file)
0319 {
0320     // We always show the popup on the right side of the current screen, so
0321     // this logic assumes that.  Earlier revisions had logic for popup being
0322     // wherever the systray icon is, so if it's decided to go that route again,
0323     // dig into the source control history. --mpyne
0324 
0325     if(file.coverInfo()->hasCover()) {
0326         addCoverButton(parentLayout, file.coverInfo()->pixmap(CoverInfo::Thumbnail));
0327         addSeparatorLine(parentLayout);
0328     }
0329 
0330     auto infoBox = new QWidget;
0331     auto infoBoxVLayout = new QVBoxLayout(infoBox);
0332     infoBoxVLayout->setSpacing(3);
0333     infoBoxVLayout->setContentsMargins(3, 3, 3, 3);
0334 
0335     parentLayout->addWidget(infoBox);
0336 
0337     addSeparatorLine(parentLayout);
0338     createButtonBox(parentLayout);
0339 
0340     return infoBox;
0341 }
0342 
0343 void SystemTray::createPopup()
0344 {
0345     // If the action exists and it's checked, do our stuff
0346 
0347     if(!ActionCollection::action("togglePopups")->isChecked())
0348         return;
0349 
0350     const FileHandle playingFile = m_player->playingFile();
0351     const Tag *const playingInfo = playingFile.tag();
0352 
0353     delete m_popup;
0354     m_popup = nullptr;
0355     m_fadeStepTimer->stop();
0356 
0357     // This will be reset after this function call by slot(Forward|Back)
0358     // so it's safe to set it true here.
0359     m_fade = true;
0360     m_step = 0;
0361 
0362     m_popup = new PassiveInfo;
0363     connect(m_popup, &QObject::destroyed,
0364             this,    &SystemTray::slotPopupDestroyed);
0365     connect(m_popup, &PassiveInfo::timeExpired,
0366             this,    &SystemTray::slotFadeOut);
0367     connect(m_popup, &PassiveInfo::nextSong,
0368             this,    &SystemTray::slotForward);
0369     connect(m_popup, &PassiveInfo::previousSong,
0370             this,    &SystemTray::slotBack);
0371 
0372     // The fadeout requires the popup to be alive
0373     connect(m_fadeStepTimer, &QTimer::timeout,
0374             m_popup /* context */, [this]() { slotNextStep(); });
0375 
0376     auto box = new QWidget;
0377     auto boxHLayout = new QHBoxLayout(box);
0378 
0379     boxHLayout->setSpacing(15); // Add space between text and buttons
0380 
0381     QWidget *infoBox = createInfoBox(boxHLayout, playingFile);
0382     QLayout *infoBoxLayout = infoBox->layout();
0383 
0384     for(int i = 0; i < m_labels.size(); ++i) {
0385         QLabel *l = new QLabel(" ");
0386         l->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
0387         m_labels[i] = l;
0388         infoBoxLayout->addWidget(l); // layout takes ownership
0389     }
0390 
0391     // We have to set the text of the labels after all of the
0392     // widgets have been added in order for the width to be calculated
0393     // correctly.
0394 
0395     int labelCount = 0;
0396 
0397     QString title = playingInfo->title().toHtmlEscaped();
0398     m_labels[labelCount++]->setText(QString("<qt><nobr><h2>%1</h2></nobr></qt>").arg(title));
0399 
0400     if(!playingInfo->artist().isEmpty())
0401         m_labels[labelCount++]->setText(playingInfo->artist());
0402 
0403     if(!playingInfo->album().isEmpty()) {
0404         QString album = playingInfo->album().toHtmlEscaped();
0405         QString s = playingInfo->year() > 0
0406             ? QString("<qt><nobr>%1 (%2)</nobr></qt>").arg(album).arg(playingInfo->year())
0407             : QString("<qt><nobr>%1</nobr></qt>").arg(album);
0408         m_labels[labelCount++]->setText(s);
0409     }
0410 
0411     m_popup->setView(box);
0412     m_popup->show();
0413 }
0414 
0415 void SystemTray::createButtonBox(QBoxLayout *parentLayout)
0416 {
0417     auto buttonBox = new QWidget;
0418     auto buttonBoxVLayout = new QVBoxLayout(buttonBox);
0419 
0420     buttonBoxVLayout->setSpacing(3);
0421 
0422     QPushButton *forwardButton = new QPushButton(m_forwardPix, QString());
0423     forwardButton->setObjectName(QLatin1String("popup_forward"));
0424     connect(forwardButton, &QPushButton::clicked,
0425             this,          &SystemTray::slotForward);
0426 
0427     QPushButton *backButton = new QPushButton(m_backPix, QString());
0428     backButton->setObjectName(QLatin1String("popup_back"));
0429     connect(backButton, &QPushButton::clicked,
0430             this,       &SystemTray::slotBack);
0431 
0432     buttonBoxVLayout->addWidget(forwardButton);
0433     buttonBoxVLayout->addWidget(backButton);
0434     parentLayout->addWidget(buttonBox);
0435 }
0436 
0437 /**
0438  * What happens here is that the action->trigger() call will end up invoking
0439  * createPopup(), which sets m_fade to true.  Before the text starts fading
0440  * control returns to this function, which resets m_fade to false.
0441  */
0442 void SystemTray::slotBack()
0443 {
0444     ActionCollection::action("back")->trigger();
0445     m_fade = false;
0446 }
0447 
0448 void SystemTray::slotForward()
0449 {
0450     ActionCollection::action("forward")->trigger();
0451     m_fade = false;
0452 }
0453 
0454 void SystemTray::addSeparatorLine(QBoxLayout *parentLayout)
0455 {
0456     QFrame *line = new QFrame;
0457     line->setFrameShape(QFrame::VLine);
0458 
0459     // Cover art takes up 80 pixels, make sure we take up at least 80 pixels
0460     // even if we don't show the cover art for consistency.
0461 
0462     line->setMinimumHeight(80);
0463 
0464     parentLayout->addWidget(line);
0465 }
0466 
0467 void SystemTray::addCoverButton(QBoxLayout *parentLayout, const QPixmap &cover)
0468 {
0469     QPushButton *coverButton = new QPushButton;
0470 
0471     coverButton->setIconSize(cover.size());
0472     coverButton->setIcon(cover);
0473     coverButton->setFixedSize(cover.size());
0474     coverButton->setFlat(true);
0475 
0476     connect(coverButton, &QPushButton::clicked,
0477             this,        &SystemTray::slotPopupLargeCover);
0478 
0479     parentLayout->addWidget(coverButton);
0480 }
0481 
0482 QColor SystemTray::interpolateColor(int step, int steps)
0483 {
0484     if(step < 0)
0485         return m_startColor;
0486     if(step >= steps)
0487         return m_endColor;
0488 
0489     // TODO: Perhaps the algorithm here could be better?  For example, it might
0490     // make sense to go rather quickly from start to end and then slow down
0491     // the progression.
0492     return QColor(
0493             (step * m_endColor.red()   + (steps - step) * m_startColor.red())   / steps,
0494             (step * m_endColor.green() + (steps - step) * m_startColor.green()) / steps,
0495             (step * m_endColor.blue()  + (steps - step) * m_startColor.blue())  / steps
0496            );
0497 }
0498 
0499 void SystemTray::setToolTip(const QString &tip, const QPixmap &cover)
0500 {
0501     if(tip.isEmpty())
0502         KStatusNotifierItem::setToolTip("juk", i18n("JuK"), QString());
0503     else {
0504         QIcon myCover;
0505         if(cover.isNull()) {
0506             myCover = "juk"_icon;
0507         } else {
0508             //Scale to proper icon size, otherwise KStatusNotifierItem will show an unknown icon
0509             const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Desktop);
0510             myCover = QIcon(cover.scaled(iconSize, iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation));
0511         }
0512 
0513         KStatusNotifierItem::setToolTip(myCover, i18n("JuK"), tip);
0514     }
0515 }
0516 
0517 void SystemTray::scrollEvent(int delta, Qt::Orientation orientation)
0518 {
0519     if(orientation == Qt::Horizontal)
0520         return;
0521 
0522     switch(QApplication::queryKeyboardModifiers()) {
0523     case Qt::ShiftModifier:
0524         if(delta > 0)
0525             ActionCollection::action("volumeUp")->trigger();
0526         else
0527             ActionCollection::action("volumeDown")->trigger();
0528         break;
0529     default:
0530         if(delta > 0)
0531             ActionCollection::action("forward")->trigger();
0532         else
0533             ActionCollection::action("back")->trigger();
0534         break;
0535     }
0536 }
0537 
0538 // vim: set et sw=4 tw=0 sta: