/*************************************************************************** copyright : (C) 2002 by Daniel Molkentin email : molkentin@kde.org copyright : (C) 2002 - 2004 by Scott Wheeler email : wheeler@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. * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tag.h" #include "systemtray.h" #include "actioncollection.h" #include "playermanager.h" #include "collectionlist.h" #include "coverinfo.h" using namespace ActionCollection; static bool copyImage(TQImage &dest, TQImage &src, int x, int y); class FlickerFreeLabel : public TQLabel { public: FlickerFreeLabel(const TQString &text, TQWidget *parent, const char *name = 0) : TQLabel(text, parent, name) { m_textColor = paletteForegroundColor(); m_bgColor = parentWidget()->paletteBackgroundColor(); setBackgroundMode(NoBackground); } TQColor textColor() const { return m_textColor; } TQColor backgroundColor() const { return m_bgColor; } protected: virtual void drawContents(TQPainter *p) { // We want to intercept the drawContents call and draw on a pixmap // instead of the window to keep flicker to an absolute minimum. // Since TQt doesn't refresh the background, we need to do so // ourselves. TQPixmap pix(size()); TQPainter pixPainter(&pix); pixPainter.fillRect(rect(), m_bgColor); TQLabel::drawContents(&pixPainter); bitBlt(p->device(), TQPoint(0, 0), &pix, rect(), CopyROP); } private: TQColor m_textColor; TQColor m_bgColor; }; PassiveInfo::PassiveInfo(TQWidget *parent, const char *name) : KPassivePopup(parent, name), m_timer(new TQTimer), m_justDie(false) { // I'm so sick and tired of KPassivePopup screwing this up // that I'll just handle the timeout myself, thank you very much. KPassivePopup::setTimeout(0); connect(m_timer, TQ_SIGNAL(timeout()), TQ_SLOT(timerExpired())); } void PassiveInfo::setTimeout(int delay) { m_timer->changeInterval(delay); } void PassiveInfo::show() { KPassivePopup::show(); m_timer->start(3500); } 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(TQEvent *) { m_timer->stop(); emit mouseEntered(); } void PassiveInfo::leaveEvent(TQEvent *) { m_justDie = true; m_timer->start(50); } //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// SystemTray::SystemTray(TQWidget *parent, const char *name) : KSystemTray(parent, name), m_popup(0), m_fadeTimer(0), m_fade(true) { // This should be initialized to the number of labels that are used. m_labels.reserve(3); m_appPix = loadIcon("juk_dock"); m_playPix = createPixmap("media-playback-start"); m_pausePix = createPixmap("media-playback-pause"); m_forwardPix = loadIcon("media-skip-forward"); m_backPix = loadIcon("media-skip-backward"); setPixmap(m_appPix); setToolTip(); // Just create this here so that it show up in the DCOP interface and the key // bindings dialog. new TDEAction(i18n("Redisplay Popup"), TDEShortcut(), this, TQ_SLOT(slotPlay()), ActionCollection::actions(), "showPopup"); TDEPopupMenu *cm = contextMenu(); connect(PlayerManager::instance(), TQ_SIGNAL(signalPlay()), this, TQ_SLOT(slotPlay())); connect(PlayerManager::instance(), TQ_SIGNAL(signalPause()), this, TQ_SLOT(slotPause())); connect(PlayerManager::instance(), TQ_SIGNAL(signalStop()), this, TQ_SLOT(slotStop())); action("play")->plug(cm); action("pause")->plug(cm); action("stop")->plug(cm); action("forward")->plug(cm); action("back")->plug(cm); cm->insertSeparator(); // Pity the actionCollection doesn't keep track of what sub-menus it has. TDEActionMenu *menu = new TDEActionMenu(i18n("&Random Play"), this); menu->insert(action("disableRandomPlay")); menu->insert(action("randomPlay")); menu->insert(action("albumRandomPlay")); menu->plug(cm); action("togglePopups")->plug(cm); m_fadeTimer = new TQTimer(this, "systrayFadeTimer"); connect(m_fadeTimer, TQ_SIGNAL(timeout()), TQ_SLOT(slotNextStep())); if(PlayerManager::instance()->playing()) slotPlay(); else if(PlayerManager::instance()->paused()) slotPause(); } SystemTray::~SystemTray() { } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void SystemTray::slotPlay() { if(!PlayerManager::instance()->playing()) return; TQPixmap cover = PlayerManager::instance()->playingFile().coverInfo()->pixmap(CoverInfo::Thumbnail); setPixmap(m_playPix); setToolTip(PlayerManager::instance()->playingString(), cover); createPopup(); } void SystemTray::slotTogglePopup() { if(m_popup && m_popup->view()->isVisible()) m_popup->setTimeout(50); else slotPlay(); } void SystemTray::slotPopupLargeCover() { if(!PlayerManager::instance()->playing()) return; FileHandle playingFile = PlayerManager::instance()->playingFile(); playingFile.coverInfo()->popup(); } void SystemTray::slotStop() { setPixmap(m_appPix); setToolTip(); delete m_popup; m_popup = 0; } void SystemTray::slotPopupDestroyed() { for(unsigned i = 0; i < m_labels.capacity(); ++i) m_labels[i] = 0; } void SystemTray::slotNextStep() { TQColor result; ++m_step; // If we're not fading, immediately show the labels if(!m_fade) m_step = STEPS; result = interpolateColor(m_step); for(unsigned i = 0; i < m_labels.capacity() && m_labels[i]; ++i) m_labels[i]->setPaletteForegroundColor(result); if(m_step == STEPS) { m_step = 0; m_fadeTimer->stop(); emit fadeDone(); } } void SystemTray::slotFadeOut() { m_startColor = m_labels[0]->textColor(); m_endColor = m_labels[0]->backgroundColor(); connect(this, TQ_SIGNAL(fadeDone()), m_popup, TQ_SLOT(hide())); connect(m_popup, TQ_SIGNAL(mouseEntered()), this, TQ_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() { m_endColor = m_labels[0]->textColor(); disconnect(TQ_SIGNAL(fadeDone())); m_step = STEPS - 1; // Simulate end of fade to solid text slotNextStep(); } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// TQVBox *SystemTray::createPopupLayout(TQWidget *parent, const FileHandle &file) { TQVBox *infoBox = 0; if(buttonsToLeft()) { // They go to the left because JuK is on that side createButtonBox(parent); addSeparatorLine(parent); infoBox = new TQVBox(parent); // Another line, and the cover, if there's a cover, and if // it's selected to be shown if(file.coverInfo()->hasCover()) { addSeparatorLine(parent); addCoverButton(parent, file.coverInfo()->pixmap(CoverInfo::Thumbnail)); } } else { // Like above, but reversed. if(file.coverInfo()->hasCover()) { addCoverButton(parent, file.coverInfo()->pixmap(CoverInfo::Thumbnail)); addSeparatorLine(parent); } infoBox = new TQVBox(parent); addSeparatorLine(parent); createButtonBox(parent); } infoBox->setSpacing(3); infoBox->setMargin(3); return infoBox; } void SystemTray::createPopup() { FileHandle playingFile = PlayerManager::instance()->playingFile(); Tag *playingInfo = playingFile.tag(); // If the action exists and it's checked, do our stuff if(!action("togglePopups")->isChecked()) return; delete m_popup; m_popup = 0; m_fadeTimer->stop(); // 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; m_popup = new PassiveInfo(this); connect(m_popup, TQ_SIGNAL(destroyed()), TQ_SLOT(slotPopupDestroyed())); connect(m_popup, TQ_SIGNAL(timeExpired()), TQ_SLOT(slotFadeOut())); TQHBox *box = new TQHBox(m_popup, "popupMainLayout"); box->setSpacing(15); // Add space between text and buttons TQVBox *infoBox = createPopupLayout(box, playingFile); for(unsigned i = 0; i < m_labels.capacity(); ++i) { m_labels[i] = new FlickerFreeLabel(" ", infoBox); m_labels[i]->setAlignment(AlignRight | AlignVCenter); } // We don't want an autodelete popup. There are times when it will need // to be hidden before the timeout. m_popup->setAutoDelete(false); // 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. int labelCount = 0; TQString title = TQStyleSheet::escape(playingInfo->title()); m_labels[labelCount++]->setText(TQString("

%1

").arg(title)); if(!playingInfo->artist().isEmpty()) m_labels[labelCount++]->setText(playingInfo->artist()); if(!playingInfo->album().isEmpty()) { TQString album = TQStyleSheet::escape(playingInfo->album()); TQString s = playingInfo->year() > 0 ? TQString("%1 (%2)").arg(album).arg(playingInfo->year()) : TQString("%1").arg(album); m_labels[labelCount++]->setText(s); } m_startColor = m_labels[0]->backgroundColor(); m_endColor = m_labels[0]->textColor(); slotNextStep(); m_fadeTimer->start(1500 / STEPS); m_popup->setView(box); m_popup->show(); } bool SystemTray::buttonsToLeft() const { // The following code was nicked from kpassivepopup.cpp NETWinInfo ni(tqt_xdisplay(), winId(), tqt_xrootwin(), NET::WMIconGeometry | NET::WMKDESystemTrayWinFor); NETRect frame, win; ni.kdeGeometry(frame, win); TQRect bounds = TDEGlobalSettings::desktopGeometry(TQPoint(win.pos.x, win.pos.y)); // This seems to accurately guess what side of the icon that // KPassivePopup will popup on. return(win.pos.x < bounds.center().x()); } TQPixmap SystemTray::createPixmap(const TQString &pixName) { TQPixmap bgPix = m_appPix; TQPixmap fgPix = SmallIcon(pixName); TQImage bgImage = bgPix.convertToImage(); // Probably 22x22 TQImage fgImage = fgPix.convertToImage(); // Should be 16x16 TDEIconEffect::semiTransparent(bgImage); copyImage(bgImage, fgImage, (bgImage.width() - fgImage.width()) / 2, (bgImage.height() - fgImage.height()) / 2); bgPix.convertFromImage(bgImage); return bgPix; } void SystemTray::createButtonBox(TQWidget *parent) { TQVBox *buttonBox = new TQVBox(parent); buttonBox->setSpacing(3); TQPushButton *forwardButton = new TQPushButton(m_forwardPix, 0, buttonBox, "popup_forward"); forwardButton->setFlat(true); connect(forwardButton, TQ_SIGNAL(clicked()), TQ_SLOT(slotForward())); TQPushButton *backButton = new TQPushButton(m_backPix, 0, buttonBox, "popup_back"); backButton->setFlat(true); connect(backButton, TQ_SIGNAL(clicked()), TQ_SLOT(slotBack())); } /** * What happens here is that the action->activate() call will end up invoking * 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() { action("back")->activate(); m_fade = false; } void SystemTray::slotForward() { action("forward")->activate(); m_fade = false; } void SystemTray::addSeparatorLine(TQWidget *parent) { TQFrame *line = new TQFrame(parent); line->setFrameShape(TQFrame::VLine); // 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); } void SystemTray::addCoverButton(TQWidget *parent, const TQPixmap &cover) { TQPushButton *coverButton = new TQPushButton(parent); coverButton->setPixmap(cover); coverButton->setFixedSize(cover.size()); coverButton->setFlat(true); connect(coverButton, TQ_SIGNAL(clicked()), this, TQ_SLOT(slotPopupLargeCover())); } TQColor 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 TQColor( (step * m_endColor.red() + (steps - step) * m_startColor.red()) / steps, (step * m_endColor.green() + (steps - step) * m_startColor.green()) / steps, (step * m_endColor.blue() + (steps - step) * m_startColor.blue()) / steps ); } void SystemTray::setToolTip(const TQString &tip, const TQPixmap &cover) { TQToolTip::remove(this); if(tip.isNull()) TQToolTip::add(this, i18n("JuK")); else { TQPixmap myCover = cover; if(cover.isNull()) myCover = DesktopIcon("juk"); TQImage coverImage = myCover.convertToImage(); if(coverImage.size().width() > 32 || coverImage.size().height() > 32) coverImage = coverImage.smoothScale(32, 32); TQMimeSourceFactory::defaultFactory()->setImage("tipCover", coverImage); TQString html = i18n("%1 is Cover Art, %2 is the playing track, %3 is the appname", "
" "
%1%2
%3
"); html = html.arg("%1").arg(tip), i18n("JuK")); TQToolTip::add(this, html); } } void SystemTray::wheelEvent(TQWheelEvent *e) { if(e->orientation() ==TQt::Horizontal) return; // I already know the type here, but this file doesn't (and I don't want it // to) know about the JuK class, so a static_cast won't work, and I was told // that a reinterpret_cast isn't portable when combined with multiple // inheritance. (This is why I don't check the result.) switch(e->state()) { case ShiftButton: if(e->delta() > 0) action("volumeUp")->activate(); else action("volumeDown")->activate(); break; default: if(e->delta() > 0) action("forward")->activate(); else action("back")->activate(); break; } e->accept(); } /* * Reimplemented this in order to use the middle mouse button */ void SystemTray::mousePressEvent(TQMouseEvent *e) { switch(e->button()) { case TQt::LeftButton: case TQt::RightButton: default: KSystemTray::mousePressEvent(e); break; case TQt::MidButton: if(!rect().contains(e->pos())) return; if(action("pause")->isEnabled()) action("pause")->activate(); else action("play")->activate(); break; } } /* * This function copies the entirety of src into dest, starting in * dest at x and y. This function exists because I was unable to find * a function like it in either TQImage or tdefx */ static bool copyImage(TQImage &dest, TQImage &src, int x, int y) { if(dest.depth() != src.depth()) return false; if((x + src.width()) >= dest.width()) return false; if((y + src.height()) >= dest.height()) return false; // We want to use TDEIconEffect::overlay to do this, since it handles // alpha, but the images need to be the same size. We can handle that. TQImage large_src(dest); // It would perhaps be better to create large_src based on a size, but // this is the easiest way to make a new image with the same depth, size, // etc. large_src.detach(); // However, we do have to specifically ensure that setAlphaBuffer is set // to false large_src.setAlphaBuffer(false); large_src.fill(0); // All transparent pixels large_src.setAlphaBuffer(true); int w = src.width(); int h = src.height(); for(int dx = 0; dx < w; dx++) for(int dy = 0; dy < h; dy++) large_src.setPixel(dx + x, dy + y, src.pixel(dx, dy)); // Apply effect to image TDEIconEffect::overlay(dest, large_src); return true; } #include "systemtray.moc"