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: