diff --git a/coverinfo.cpp b/coverinfo.cpp index 0ad7177e..a4c54c56 100644 --- a/coverinfo.cpp +++ b/coverinfo.cpp @@ -1,482 +1,491 @@ /** * Copyright (C) 2004 Nathan Toone * Copyright (C) 2005, 2008 Michael Pyne * * 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. * * This program 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 * this program. If not, see . */ #include "coverinfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef TAGLIB_WITH_MP4 #include #include #include #include #endif #include "mediafiles.h" #include "collectionlist.h" #include "playlistsearch.h" #include "playlistitem.h" #include "tag.h" #include "juk_debug.h" struct CoverPopup : public QWidget { - CoverPopup(const QPixmap &image, const QPoint &p) : + CoverPopup(QPixmap &image, const QPoint &p) : QWidget(0, Qt::WindowFlags(Qt::WA_DeleteOnClose | Qt::X11BypassWindowManagerHint)) { QHBoxLayout *layout = new QHBoxLayout(this); QLabel *label = new QLabel(this); - layout->addWidget(label); + + const auto pixRatio = this->devicePixelRatioF(); + QSizeF imageSize(label->width(), label->height()); + + if (!qFuzzyCompare(pixRatio, 1.0)) { + imageSize /= pixRatio; + image.setDevicePixelRatio(pixRatio); + } + label->setFrameStyle(QFrame::Box | QFrame::Raised); label->setLineWidth(1); label->setPixmap(image); - setGeometry(p.x(), p.y(), label->width(), label->height()); + setGeometry(QRect(p, imageSize.toSize())); + show(); } virtual void leaveEvent(QEvent *) { close(); } virtual void mouseReleaseEvent(QMouseEvent *) { close(); } }; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// CoverInfo::CoverInfo(const FileHandle &file) : m_file(file), m_hasCover(false), m_hasAttachedCover(false), m_haveCheckedForCover(false), m_coverKey(CoverManager::NoMatch) { } bool CoverInfo::hasCover() const { if(m_haveCheckedForCover) return m_hasCover || m_hasAttachedCover; m_haveCheckedForCover = true; // Check for new-style covers. First let's determine what our coverKey is // if it's not already set, as that's also tracked by the CoverManager. if(m_coverKey == CoverManager::NoMatch) m_coverKey = CoverManager::idForTrack(m_file.absFilePath()); // We were assigned a key, let's see if we already have a cover. Notice // that due to the way the CoverManager is structured, we should have a // cover if we have a cover key. If we don't then either there's a logic // error, or the user has been mucking around where they shouldn't. if(m_coverKey != CoverManager::NoMatch) m_hasCover = CoverManager::hasCover(m_coverKey); // Check if it's embedded in the file itself. m_hasAttachedCover = hasEmbeddedAlbumArt(); if(m_hasAttachedCover) return true; // Look for cover.jpg or cover.png in the directory. if(QFile::exists(m_file.fileInfo().absolutePath() + "/cover.jpg") || QFile::exists(m_file.fileInfo().absolutePath() + "/cover.png")) { m_hasCover = true; } return m_hasCover; } void CoverInfo::clearCover() { m_hasCover = false; m_hasAttachedCover = false; // Re-search for cover since we may still have a different type of cover. m_haveCheckedForCover = false; // We don't need to call removeCover because the CoverManager will // automatically unlink the cover if we were the last track to use it. CoverManager::setIdForTrack(m_file.absFilePath(), CoverManager::NoMatch); m_coverKey = CoverManager::NoMatch; } void CoverInfo::setCover(const QImage &image) { if(image.isNull()) return; m_haveCheckedForCover = true; m_hasCover = true; QPixmap cover = QPixmap::fromImage(image); // If we use replaceCover we'll change the cover for every other track // with the same coverKey, which we don't want since that case will be // handled by Playlist. Instead just replace this track's cover. m_coverKey = CoverManager::addCover(cover, m_file.tag()->artist(), m_file.tag()->album()); if(m_coverKey != CoverManager::NoMatch) CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); } void CoverInfo::setCoverId(coverKey id) { m_coverKey = id; m_haveCheckedForCover = true; m_hasCover = id != CoverManager::NoMatch; // Inform CoverManager of the change. CoverManager::setIdForTrack(m_file.absFilePath(), m_coverKey); } void CoverInfo::applyCoverToWholeAlbum(bool overwriteExistingCovers) const { QString artist = m_file.tag()->artist(); QString album = m_file.tag()->album(); PlaylistSearch::ComponentList components; ColumnList columns; columns.append(PlaylistItem::ArtistColumn); components.append(PlaylistSearch::Component(artist, false, columns, PlaylistSearch::Component::Exact)); columns.clear(); columns.append(PlaylistItem::AlbumColumn); components.append(PlaylistSearch::Component(album, false, columns, PlaylistSearch::Component::Exact)); PlaylistList playlists; playlists.append(CollectionList::instance()); PlaylistSearch search(playlists, components, PlaylistSearch::MatchAll); // Search done, iterate through results. PlaylistItemList results = search.matchedItems(); PlaylistItemList::ConstIterator it = results.constBegin(); for(; it != results.constEnd(); ++it) { // Don't worry about files that somehow already have a tag, // unless the conversion is forced. if(!overwriteExistingCovers && (*it)->file().coverInfo()->coverId() != CoverManager::NoMatch) continue; (*it)->file().coverInfo()->setCoverId(m_coverKey); } } coverKey CoverInfo::coverId() const { if(m_coverKey == CoverManager::NoMatch) m_coverKey = CoverManager::idForTrack(m_file.absFilePath()); return m_coverKey; } QPixmap CoverInfo::pixmap(CoverSize size) const { if(hasCover() && m_coverKey != CoverManager::NoMatch) { return CoverManager::coverFromId(m_coverKey, size == Thumbnail ? CoverManager::Thumbnail : CoverManager::FullSize); } QImage cover; // If m_hasCover is still true we must have a directory cover image. if(m_hasCover) { QString fileName = m_file.fileInfo().absolutePath() + "/cover.jpg"; if(!cover.load(fileName)) { fileName = m_file.fileInfo().absolutePath() + "/cover.png"; if(!cover.load(fileName)) return QPixmap(); } } // If we get here, see if there is an embedded cover. cover = embeddedAlbumArt(); if(!cover.isNull() && size == Thumbnail) cover = scaleCoverToThumbnail(cover); if(cover.isNull()) { return QPixmap(); } return QPixmap::fromImage(cover); } QString CoverInfo::localPathToCover(const QString &fallbackFileName) const { if(m_coverKey != CoverManager::NoMatch) { QString path = CoverManager::coverInfo(m_coverKey).path; if(!path.isEmpty()) return path; } if(hasEmbeddedAlbumArt()) { QFile albumArtFile(fallbackFileName); if(!albumArtFile.open(QIODevice::ReadWrite)) { return QString(); } QImage albumArt = embeddedAlbumArt(); albumArt.save(&albumArtFile, "PNG"); return fallbackFileName; } QString basePath = m_file.fileInfo().absolutePath(); if(QFile::exists(basePath + "/cover.jpg")) return basePath + "/cover.jpg"; else if(QFile::exists(basePath + "/cover.png")) return basePath + "/cover.png"; return QString(); } bool CoverInfo::hasEmbeddedAlbumArt() const { QScopedPointer fileTag( MediaFiles::fileFactoryByType(m_file.absFilePath())); if (TagLib::MPEG::File *mpegFile = dynamic_cast(fileTag.data())) { TagLib::ID3v2::Tag *id3tag = mpegFile->ID3v2Tag(false); if (!id3tag) { qCCritical(JUK_LOG) << m_file.absFilePath() << "seems to have invalid ID3 tag"; return false; } // Look for attached picture frames. TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"]; return !frames.isEmpty(); } else if (TagLib::FLAC::File *flacFile = dynamic_cast(fileTag.data())) { // Look if images are embedded. return !flacFile->pictureList().isEmpty(); } #ifdef TAGLIB_WITH_MP4 else if(TagLib::MP4::File *mp4File = dynamic_cast(fileTag.data())) { TagLib::MP4::Tag *tag = mp4File->tag(); if (tag) { TagLib::MP4::ItemListMap &items = tag->itemListMap(); return items.contains("covr"); } } #endif return false; } static QImage embeddedMPEGAlbumArt(TagLib::ID3v2::Tag *id3tag) { if(!id3tag) return QImage(); // Look for attached picture frames. TagLib::ID3v2::FrameList frames = id3tag->frameListMap()["APIC"]; if(frames.isEmpty()) return QImage(); // According to the spec attached picture frames have different types. // So we should look for the corresponding picture depending on what // type of image (i.e. front cover, file info) we want. If only 1 // frame, just return that (scaled if necessary). TagLib::ID3v2::AttachedPictureFrame *selectedFrame = 0; if(frames.size() != 1) { TagLib::ID3v2::FrameList::Iterator it = frames.begin(); for(; it != frames.end(); ++it) { // This must be dynamic_cast<>, TagLib will return UnknownFrame in APIC for // encrypted frames. TagLib::ID3v2::AttachedPictureFrame *frame = dynamic_cast(*it); // Both thumbnail and full size should use FrontCover, as // FileIcon may be too small even for thumbnail. if(frame && frame->type() != TagLib::ID3v2::AttachedPictureFrame::FrontCover) continue; selectedFrame = frame; break; } } // If we get here we failed to pick a picture, or there was only one, // so just use the first picture. if(!selectedFrame) selectedFrame = dynamic_cast(frames.front()); if(!selectedFrame) // Could occur for encrypted picture frames. return QImage(); TagLib::ByteVector picture = selectedFrame->picture(); return QImage::fromData( reinterpret_cast(picture.data()), picture.size()); } static QImage embeddedFLACAlbumArt(TagLib::FLAC::File *flacFile) { TagLib::List flacPictures = flacFile->pictureList(); if(flacPictures.isEmpty()) { // No pictures are embedded. return QImage(); } // Always use first picture - even if multiple are embedded. TagLib::ByteVector coverData = flacPictures[0]->data(); QImage result = QImage::fromData( reinterpret_cast(coverData.data()), coverData.size()); if(!result.isNull()) { return result; } // Error while casting image. return QImage(); } #ifdef TAGLIB_WITH_MP4 static QImage embeddedMP4AlbumArt(TagLib::MP4::Tag *tag) { TagLib::MP4::ItemListMap &items = tag->itemListMap(); if(!items.contains("covr")) return QImage(); TagLib::MP4::CoverArtList covers = items["covr"].toCoverArtList(); TagLib::MP4::CoverArtList::ConstIterator end = covers.end(); for(TagLib::MP4::CoverArtList::ConstIterator it = covers.begin(); it != end; ++it) { TagLib::MP4::CoverArt cover = *it; TagLib::ByteVector coverData = cover.data(); QImage result = QImage::fromData( reinterpret_cast(coverData.data()), coverData.size()); if(!result.isNull()) return result; } // No appropriate image found return QImage(); } #endif void CoverInfo::popup() const { QPixmap image = pixmap(FullSize); QPoint mouse = QCursor::pos(); QRect desktop = QApplication::desktop()->screenGeometry(mouse); int x = mouse.x(); int y = mouse.y(); int height = image.size().height() + 4; int width = image.size().width() + 4; // Detect the right direction to pop up (always towards the center of the // screen), try to pop up with the mouse pointer 10 pixels into the image in // both directions. If we're too close to the screen border for this margin, // show it at the screen edge, accounting for the four pixels (two on each // side) for the window border. if(x - desktop.x() < desktop.width() / 2) x = (x - desktop.x() < 10) ? desktop.x() : (x - 10); else x = (x - desktop.x() > desktop.width() - 10) ? desktop.width() - width +desktop.x() : (x - width + 10); if(y - desktop.y() < desktop.height() / 2) y = (y - desktop.y() < 10) ? desktop.y() : (y - 10); else y = (y - desktop.y() > desktop.height() - 10) ? desktop.height() - height + desktop.y() : (y - height + 10); new CoverPopup(image, QPoint(x, y)); } QImage CoverInfo::embeddedAlbumArt() const { QScopedPointer fileTag( MediaFiles::fileFactoryByType(m_file.absFilePath())); if (TagLib::MPEG::File *mpegFile = dynamic_cast(fileTag.data())) { TagLib::ID3v2::Tag *id3tag = mpegFile->ID3v2Tag(false); return embeddedMPEGAlbumArt(id3tag); } else if (TagLib::FLAC::File *flacFile = dynamic_cast(fileTag.data())) { return embeddedFLACAlbumArt(flacFile); } #ifdef TAGLIB_WITH_MP4 else if(TagLib::MP4::File *mp4File = dynamic_cast(fileTag.data())) { TagLib::MP4::Tag *tag = mp4File->tag(); if (tag) { return embeddedMP4AlbumArt(tag); } } #endif return QImage(); } QImage CoverInfo::scaleCoverToThumbnail(const QImage &image) const { return image.scaled(80, 80, Qt::KeepAspectRatio, Qt::SmoothTransformation); } // vim: set et sw=4 tw=0 sta: diff --git a/nowplaying.cpp b/nowplaying.cpp index 67561e6a..99923f99 100644 --- a/nowplaying.cpp +++ b/nowplaying.cpp @@ -1,334 +1,339 @@ /** * Copyright (C) 2004 Scott Wheeler * Copyright (C) 2009 Michael Pyne * * 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. * * This program 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 * this program. If not, see . */ #include "nowplaying.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlistcollection.h" #include "playlistitem.h" #include "coverinfo.h" #include "covermanager.h" #include "tag.h" #include "collectionlist.h" #include "juk_debug.h" -// Anon namespace to hide symbol from outside this translation unit -namespace { - static int g_imageSize = 64; -}; - //////////////////////////////////////////////////////////////////////////////// // NowPlaying //////////////////////////////////////////////////////////////////////////////// NowPlaying::NowPlaying(QWidget *parent, PlaylistCollection *collection) : QWidget(parent), m_observer(this, collection), // Also watch the collection m_collectionListObserver(this, CollectionList::instance()), m_collection(collection) { setObjectName(QLatin1String("NowPlaying")); QHBoxLayout *layout = new QHBoxLayout(this); setLayout(layout); layout->setMargin(0); layout->setSpacing(3); // With HiDPI the text might actually be bigger... try to account for // that. const QFont defaultLargeFont(QFontDatabase::systemFont(QFontDatabase::TitleFont)); const QFontMetrics fm(defaultLargeFont, this); - g_imageSize = qMax(g_imageSize, fm.lineSpacing()); - setFixedHeight(g_imageSize + 2); + const int coverIconHeight = qMax(64, fm.lineSpacing()); + setFixedHeight(coverIconHeight + 2); layout->addWidget(new CoverItem(this), 0); layout->addWidget(new TrackItem(this), 2); hide(); } void NowPlaying::addItem(NowPlayingItem *item) { m_items.append(item); } PlaylistCollection *NowPlaying::collection() const { return m_collection; } void NowPlaying::slotUpdate(const FileHandle &file) { m_file = file; if(file.isNull()) { hide(); emit nowPlayingHidden(); return; } else show(); foreach(NowPlayingItem *item, m_items) item->update(file); } void NowPlaying::slotReloadCurrentItem() { foreach(NowPlayingItem *item, m_items) item->update(m_file); } //////////////////////////////////////////////////////////////////////////////// // CoverItem //////////////////////////////////////////////////////////////////////////////// CoverItem::CoverItem(NowPlaying *parent) : QLabel(parent), NowPlayingItem(parent) { setObjectName(QLatin1String("CoverItem")); setFixedHeight(parent->height() - parent->layout()->margin() * 2); setMargin(1); setAcceptDrops(true); } void CoverItem::update(const FileHandle &file) { m_file = file; if(!file.isNull() && file.coverInfo()->hasCover()) { show(); - setPixmap( - file.coverInfo()->pixmap(CoverInfo::Thumbnail) - .scaled(g_imageSize, g_imageSize, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + + const auto pixRatio = this->devicePixelRatioF(); + const QSizeF logicalSize = QSizeF(this->height(), this->height()); + const QSizeF scaledSize = logicalSize * pixRatio; + QPixmap pix = + file.coverInfo()->pixmap(CoverInfo::FullSize) + .scaled(scaledSize.toSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + + if (!qFuzzyCompare(pixRatio, 1.0)) { + pix.setDevicePixelRatio(pixRatio); + } + + setPixmap(pix); } else hide(); } void CoverItem::mouseReleaseEvent(QMouseEvent *event) { if(m_dragging) { m_dragging = false; return; } if(event->x() >= 0 && event->y() >= 0 && event->x() < width() && event->y() < height() && event->button() == Qt::LeftButton && m_file.coverInfo()->hasCover()) { m_file.coverInfo()->popup(); } QLabel::mousePressEvent(event); } void CoverItem::mousePressEvent(QMouseEvent *e) { m_dragging = false; m_dragStart = e->globalPos(); } void CoverItem::mouseMoveEvent(QMouseEvent *e) { if(m_dragging) return; QPoint diff = m_dragStart - e->globalPos(); if(diff.manhattanLength() > QApplication::startDragDistance()) { // Start a drag. m_dragging = true; QDrag *drag = new QDrag(this); CoverDrag *data = new CoverDrag(m_file.coverInfo()->coverId()); drag->setMimeData(data); drag->exec(Qt::CopyAction); } } void CoverItem::dragEnterEvent(QDragEnterEvent *e) { e->setAccepted(CoverDrag::isCover(e->mimeData()) || e->mimeData()->hasUrls()); } void CoverItem::dropEvent(QDropEvent *e) { QImage image; QList urls; coverKey key; if(e->source() == this) return; key = CoverDrag::idFromData(e->mimeData()); if(key != CoverManager::NoMatch) { m_file.coverInfo()->setCoverId(key); update(m_file); } else if(e->mimeData()->hasImage()) { m_file.coverInfo()->setCover(qvariant_cast(e->mimeData()->imageData())); update(m_file); } else { urls = e->mimeData()->urls(); if(urls.isEmpty()) return; QString fileName; auto getJob = KIO::storedGet(urls.front()); KJobWidgets::setWindow(getJob, this); if(getJob->exec()) { if(image.loadFromData(getJob->data())) { m_file.coverInfo()->setCover(image); update(m_file); } else qCCritical(JUK_LOG) << "Unable to load image from " << urls.front(); } else qCCritical(JUK_LOG) << "Unable to download " << urls.front(); } } //////////////////////////////////////////////////////////////////////////////// // TrackItem //////////////////////////////////////////////////////////////////////////////// TrackItem::TrackItem(NowPlaying *parent) : QWidget(parent), NowPlayingItem(parent) { setObjectName(QLatin1String("TrackItem")); setFixedHeight(parent->height() - parent->layout()->margin() * 2); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); QVBoxLayout *layout = new QVBoxLayout(this); m_label = new QLabel(this); m_label->setWordWrap(true); m_label->setTextInteractionFlags(Qt::LinksAccessibleByMouse|Qt::LinksAccessibleByKeyboard); layout->addStretch(); layout->addWidget(m_label, 1); layout->addStretch(); connect(m_label, SIGNAL(linkActivated(QString)), this, SLOT(slotOpenLink(QString))); // Ensure that if we're filtering results, that the filtering is cleared if we // hide the now playing bar so that the user can select tracks normally. connect(parent, SIGNAL(nowPlayingHidden()), SLOT(slotClearShowMore())); } void TrackItem::update(const FileHandle &file) { m_file = file; QTimer::singleShot(0, this, SLOT(slotUpdate())); } void TrackItem::slotOpenLink(const QString &link) { PlaylistCollection *collection = parentManager()->collection(); if(link == "artist") collection->showMore(m_file.tag()->artist()); else if(link == "album") collection->showMore(m_file.tag()->artist(), m_file.tag()->album()); else if(link == "clear") collection->clearShowMore(); update(m_file); } void TrackItem::slotUpdate() { if(m_file.isNull()) { m_label->setText(QString()); return; } QString title = m_file.tag()->title().toHtmlEscaped(); QString artist = m_file.tag()->artist().toHtmlEscaped(); QString album = m_file.tag()->album().toHtmlEscaped(); QString separator = (artist.isNull() || album.isNull()) ? QString::null : QString(" - "); //krazy:exclude=nullstrassign for old broken gcc // This block-o-nastiness makes the font smaller and smaller until it actually fits. int size = 4; QString format = "%2" "
" "%4%5%6"; if(parentManager()->collection()->showMoreActive()) format.append(QString(" (%1)").arg(i18n("back to playlist"))); format.append(""); int parentHeight = parentManager()->contentsRect().height(); int neededHeight = 0; do { m_label->setText(format.arg(size).arg(title).arg(size - 2) .arg(artist).arg(separator).arg(album)); --size; neededHeight = m_label->heightForWidth(m_label->width()); } while(neededHeight > parentHeight && size >= -1); m_label->setFixedHeight(qMin(neededHeight, parentHeight)); } void TrackItem::slotClearShowMore() { PlaylistCollection *collection = parentManager()->collection(); Q_ASSERT(collection); collection->clearShowMore(); } // vim: set et sw=4 tw=0 sta: