/* This file is part of Clementine. Clementine 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 3 of the License, or (at your option) any later version. Clementine is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Clementine. If not, see . */ #include "nowplayingwidget.h" #include "core/albumcoverloader.h" #include "core/networkaccessmanager.h" #include #include #include #include #include #include #include #include #include const char* NowPlayingWidget::kSettingsGroup = "NowPlayingWidget"; const char* NowPlayingWidget::kHypnotoadPath = ":/hypnotoad.gif"; // Space between the cover and the details in small mode const int NowPlayingWidget::kPadding = 2; // Width of the transparent to black gradient above and below the text in large // mode const int NowPlayingWidget::kGradientHead = 40; const int NowPlayingWidget::kGradientTail = 20; // Maximum height of the cover in large mode, and offset between the // bottom of the cover and bottom of the widget const int NowPlayingWidget::kMaxCoverSize = 260; const int NowPlayingWidget::kBottomOffset = 0; // Border for large mode const int NowPlayingWidget::kTopBorder = 4; NowPlayingWidget::NowPlayingWidget(QWidget *parent) : QWidget(parent), cover_loader_(new BackgroundThreadImplementation(this)), network_(NULL), mode_(SmallSongDetails), menu_(new QMenu(this)), above_statusbar_action_(NULL), visible_(false), small_ideal_height_(0), cover_height_(0), show_hide_animation_(new QTimeLine(500, this)), fade_animation_(new QTimeLine(1000, this)), load_cover_id_(0), details_(new QTextDocument(this)), previous_track_opacity_(0.0), hypnotoad_(NULL) { // Load settings QSettings s; s.beginGroup(kSettingsGroup); mode_ = Mode(s.value("mode", SmallSongDetails).toInt()); // Context menu QActionGroup* mode_group = new QActionGroup(this); QSignalMapper* mode_mapper = new QSignalMapper(this); connect(mode_mapper, SIGNAL(mapped(int)), SLOT(SetMode(int))); CreateModeAction(SmallSongDetails, tr("Small album cover"), mode_group, mode_mapper); CreateModeAction(LargeSongDetails, tr("Large album cover"), mode_group, mode_mapper); menu_->addActions(mode_group->actions()); menu_->addSeparator(); above_statusbar_action_ = menu_->addAction(tr("Show above status bar")); above_statusbar_action_->setCheckable(true); connect(above_statusbar_action_, SIGNAL(toggled(bool)), SLOT(ShowAboveStatusBar(bool))); above_statusbar_action_->setChecked(s.value("above_status_bar", false).toBool()); // Animations connect(show_hide_animation_, SIGNAL(frameChanged(int)), SLOT(SetHeight(int))); setMaximumHeight(0); connect(fade_animation_, SIGNAL(valueChanged(qreal)), SLOT(FadePreviousTrack(qreal))); fade_animation_->setDirection(QTimeLine::Backward); // 1.0 -> 0.0 // Start loading the cover loader thread cover_loader_->Start(); connect(cover_loader_, SIGNAL(Initialised()), SLOT(CoverLoaderInitialised())); } void NowPlayingWidget::CreateModeAction(Mode mode, const QString &text, QActionGroup *group, QSignalMapper* mapper) { QAction* action = new QAction(text, group); action->setCheckable(true); mapper->setMapping(action, mode); connect(action, SIGNAL(triggered()), mapper, SLOT(map())); if (mode == mode_) action->setChecked(true); } void NowPlayingWidget::set_ideal_height(int height) { small_ideal_height_ = height; UpdateHeight(); } QSize NowPlayingWidget::sizeHint() const { return QSize(cover_height_, total_height_); } void NowPlayingWidget::CoverLoaderInitialised() { UpdateHeight(); cover_loader_->Worker()->SetNetwork(network_); cover_loader_->Worker()->SetPadOutputImage(true); connect(cover_loader_->Worker().get(), SIGNAL(ImageLoaded(quint64,QImage)), SLOT(AlbumArtLoaded(quint64,QImage))); } void NowPlayingWidget::UpdateHeight() { switch (mode_) { case SmallSongDetails: cover_height_ = small_ideal_height_; total_height_ = small_ideal_height_; break; case LargeSongDetails: cover_height_ = qMin(kMaxCoverSize, width()); total_height_ = kTopBorder + cover_height_ + kBottomOffset; break; } // Update the animation settings and resize the widget now if we're visible show_hide_animation_->setFrameRange(0, total_height_); if (visible_ && show_hide_animation_->state() != QTimeLine::Running) setMaximumHeight(total_height_); // Tell the cover loader what size we want the images in cover_loader_->Worker()->SetDesiredHeight(cover_height_); cover_loader_->Worker()->SetDefaultOutputImage(QImage(":nocover.png")); // Re-fetch the current image load_cover_id_ = cover_loader_->Worker()->LoadImageAsync( metadata_.art_automatic(), metadata_.art_manual()); // Tell Qt we've changed size updateGeometry(); } void NowPlayingWidget::NowPlaying(const Song& metadata) { if (visible_) { // Cache the current pixmap so we can fade between them previous_track_ = QPixmap(size()); previous_track_.fill(palette().background().color()); previous_track_opacity_ = 1.0; QPainter p(&previous_track_); DrawContents(&p); p.end(); } metadata_ = metadata; cover_ = QPixmap(); UpdateHeight(); // Loads the cover too UpdateDetailsText(); SetVisible(true); update(); } void NowPlayingWidget::Stopped() { SetVisible(false); } void NowPlayingWidget::UpdateDetailsText() { QString html; switch (mode_) { case SmallSongDetails: details_->setTextWidth(-1); details_->setDefaultStyleSheet(""); html += "

"; break; case LargeSongDetails: details_->setTextWidth(cover_height_); details_->setDefaultStyleSheet("p {" " font-size: small;" " color: white;" "}"); html += "

"; break; } // TODO: Make this configurable html += QString("%1
%2
%3").arg( Qt::escape(metadata_.title()), Qt::escape(metadata_.artist()), Qt::escape(metadata_.album())); html += "

"; details_->setHtml(html); } void NowPlayingWidget::AlbumArtLoaded(quint64 id, const QImage& image) { if (id != load_cover_id_) return; cover_ = QPixmap::fromImage(image); update(); // Were we waiting for this cover to load before we started fading? if (!previous_track_.isNull()) { fade_animation_->start(); } } void NowPlayingWidget::SetHeight(int height) { setMaximumHeight(height); } void NowPlayingWidget::SetVisible(bool visible) { if (visible == visible_) return; visible_ = visible; show_hide_animation_->setDirection(visible ? QTimeLine::Forward : QTimeLine::Backward); show_hide_animation_->start(); } void NowPlayingWidget::paintEvent(QPaintEvent *e) { QPainter p(this); DrawContents(&p); // Draw the previous track's image if we're fading if (!previous_track_.isNull()) { p.setOpacity(previous_track_opacity_); p.drawPixmap(0, 0, previous_track_); } } void NowPlayingWidget::DrawContents(QPainter *p) { const int total_size = qMin(kMaxCoverSize, width()); if (hypnotoad_) { p->drawPixmap(0, 0, total_size, total_size, hypnotoad_->currentPixmap()); return; } switch (mode_) { case SmallSongDetails: // Draw the cover p->drawPixmap(0, 0, small_ideal_height_, small_ideal_height_, cover_); // Draw the details p->translate(small_ideal_height_ + kPadding, 0); details_->drawContents(p); p->translate(-small_ideal_height_ - kPadding, 0); break; case LargeSongDetails: const int x_offset = (width() - cover_height_) / 2; // Draw the black background p->fillRect(QRect(0, kTopBorder, width(), height() - kTopBorder), Qt::black); // Draw the cover p->drawPixmap(x_offset, kTopBorder, total_size, total_size, cover_); // Work out how high the text is going to be const int text_height = details_->size().height(); const int gradient_mid = height() - qMax(text_height, kBottomOffset); // Draw the black fade QLinearGradient gradient(0, gradient_mid - kGradientHead, 0, gradient_mid + kGradientTail); gradient.setColorAt(0, QColor(0, 0, 0, 0)); gradient.setColorAt(1, QColor(0, 0, 0, 255)); p->fillRect(0, gradient_mid - kGradientHead, width(), height() - (gradient_mid - kGradientHead), gradient); // Draw the text on top p->translate(x_offset, height() - text_height); details_->drawContents(p); p->translate(-x_offset, -height() + text_height); break; } } void NowPlayingWidget::FadePreviousTrack(qreal value) { previous_track_opacity_ = value; if (qFuzzyCompare(previous_track_opacity_, 0.0)) { previous_track_ = QPixmap(); } update(); } void NowPlayingWidget::SetMode(int mode) { mode_ = Mode(mode); UpdateHeight(); UpdateDetailsText(); update(); QSettings s; s.beginGroup(kSettingsGroup); s.setValue("mode", mode_); } void NowPlayingWidget::resizeEvent(QResizeEvent* e) { if (visible_ && mode_ == LargeSongDetails && e->oldSize().width() != e->size().width()) { UpdateHeight(); UpdateDetailsText(); } } void NowPlayingWidget::contextMenuEvent(QContextMenuEvent* e) { menu_->popup(mapToGlobal(e->pos())); } void NowPlayingWidget::ShowAboveStatusBar(bool above) { QSettings s; s.beginGroup(kSettingsGroup); s.setValue("above_status_bar", above); emit ShowAboveStatusBarChanged(above); } bool NowPlayingWidget::show_above_status_bar() const { return above_statusbar_action_->isChecked(); } void NowPlayingWidget::AllHail(bool hypnotoad) { hypnotoad_ = new QMovie(kHypnotoadPath, QByteArray(), this); connect(hypnotoad_, SIGNAL(updated(const QRect&)), SLOT(repaint())); hypnotoad_->start(); update(); }