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