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: