diff --git a/deletedialog.cpp b/deletedialog.cpp index 39120195..c33025bc 100644 --- a/deletedialog.cpp +++ b/deletedialog.cpp @@ -1,143 +1,143 @@ /** * Copyright (C) 2004, 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 "deletedialog.h" #include "ui_deletedialogbase.h" #include #include #include #include #include #include #include #include #include #include ////////////////////////////////////////////////////////////////////////////// // DeleteWidget implementation ////////////////////////////////////////////////////////////////////////////// DeleteWidget::DeleteWidget(QWidget *parent) : QWidget(parent) , m_ui(new Ui::DeleteDialogBase) { m_ui->setupUi(this); setObjectName(QLatin1String("delete_dialog_widget")); KConfigGroup messageGroup(KSharedConfig::openConfig(), "FileRemover"); bool deleteInstead = messageGroup.readEntry("deleteInsteadOfTrash", false); slotShouldDelete(deleteInstead); m_ui->ddShouldDelete->setChecked(deleteInstead); // Forward on signals connect(m_ui->ddShouldDelete, SIGNAL(toggled(bool)), SIGNAL(signalShouldDelete(bool))); connect(m_ui->ddButtonBox, SIGNAL(accepted()), SIGNAL(accepted())); connect(m_ui->ddButtonBox, SIGNAL(rejected()), SIGNAL(rejected())); } void DeleteWidget::setFiles(const QStringList &files) { m_ui->ddFileList->clear(); m_ui->ddFileList->insertItems(0, files); m_ui->ddNumFiles->setText(i18np("1 file selected.", "%1 files selected.", files.count())); } bool DeleteWidget::shouldDelete() const { return m_ui->ddShouldDelete->isChecked(); } void DeleteWidget::slotShouldDelete(bool shouldDelete) { if(shouldDelete) { m_ui->ddDeleteText->setText(i18n("These items will be permanently " "deleted from your hard disk.")); - m_ui->ddWarningIcon->setPixmap(KIconLoader::global()->loadIcon("dialog-warning", - KIconLoader::Desktop, KIconLoader::SizeLarge)); + m_ui->ddWarningIcon->setPixmap( + QIcon::fromTheme("dialog-warning").pixmap(KIconLoader::SizeLarge)); } else { m_ui->ddDeleteText->setText(i18n("These items will be moved to the Trash Bin.")); - m_ui->ddWarningIcon->setPixmap(KIconLoader::global()->loadIcon("user-trash-full", - KIconLoader::Desktop, KIconLoader::SizeLarge)); + m_ui->ddWarningIcon->setPixmap( + QIcon::fromTheme("user-trash-full").pixmap(KIconLoader::SizeLarge)); } } ////////////////////////////////////////////////////////////////////////////// // DeleteDialog implementation ////////////////////////////////////////////////////////////////////////////// DeleteDialog::DeleteDialog(QWidget *parent) : QDialog(parent), m_trashGuiItem(i18n("&Send to Trash"), "user-trash-full") { setObjectName(QLatin1String("juk_delete_dialog")); setModal(true); setWindowTitle(i18n("About to delete selected files")); auto layout = new QVBoxLayout(this); m_widget = new DeleteWidget(this); m_widget->setMinimumSize(400, 300); layout->addWidget(m_widget); // Trying to adjust for Qt bug with rich text where the layout is ignored // (something about not being able to get height-for-width on X11?) setMinimumSize(410, 326); adjustSize(); slotShouldDelete(shouldDelete()); connect(m_widget, SIGNAL(accepted()), SLOT(accept())); connect(m_widget, SIGNAL(rejected()), SLOT(reject())); connect(m_widget, SIGNAL(signalShouldDelete(bool)), SLOT(slotShouldDelete(bool))); } bool DeleteDialog::confirmDeleteList(const QStringList &condemnedFiles) { m_widget->setFiles(condemnedFiles); return exec() == QDialog::Accepted; } void DeleteDialog::setFiles(const QStringList &files) { m_widget->setFiles(files); } void DeleteDialog::accept() { KConfigGroup messageGroup(KSharedConfig::openConfig(), "FileRemover"); // Save user's preference messageGroup.writeEntry("deleteInsteadOfTrash", shouldDelete()); messageGroup.sync(); QDialog::accept(); } void DeleteDialog::slotShouldDelete(bool shouldDelete) { KGuiItem::assign(m_widget->m_ui->ddButtonBox->button(QDialogButtonBox::Ok), shouldDelete ? KStandardGuiItem::del() : m_trashGuiItem); } // vim: set et sw=4 tw=0 sta: diff --git a/filerenamer.cpp b/filerenamer.cpp index 7bd4c9cd..f747a30e 100644 --- a/filerenamer.cpp +++ b/filerenamer.cpp @@ -1,1073 +1,1073 @@ /** * Copyright (C) 2004, 2007, 2009 Michael Pyne * Copyright (C) 2003 Frerich Raabe * Copyright (C) 2014 Arnold Dumas * * 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 "filerenamer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tag.h" #include "filerenameroptions.h" #include "filehandle.h" #include "exampleoptions.h" #include "playlistitem.h" #include "playlist.h" // processEvents() #include "coverinfo.h" #include "juk_debug.h" class ConfirmationDialog : public QDialog { public: ConfirmationDialog(const QMap &files, QWidget *parent = nullptr) : QDialog(parent) { setModal(true); setWindowTitle(i18nc("warning about mass file rename", "Warning")); auto vboxLayout = new QVBoxLayout(this); auto hbox = new QWidget(this); auto hboxVLayout = new QVBoxLayout(hbox); vboxLayout->addWidget(hbox); QLabel *l = new QLabel(hbox); - l->setPixmap(SmallIcon("dialog-warning", 32)); + l->setPixmap(QIcon::fromTheme("dialog-warning").pixmap(KIconLoader::SizeLarge)); hboxVLayout->addWidget(l); l = new QLabel(i18n("You are about to rename the following files. " "Are you sure you want to continue?"), hbox); hboxVLayout->addWidget(l, 1); QTreeWidget *lv = new QTreeWidget(this); QStringList headers; headers << i18n("Original Name"); headers << i18n("New Name"); lv->setHeaderLabels(headers); lv->setRootIsDecorated(false); vboxLayout->addWidget(lv); auto buttonBox = new QDialogButtonBox(this); vboxLayout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); int lvHeight = 0; QMap::ConstIterator it = files.constBegin(); for(; it != files.constEnd(); ++it) { QTreeWidgetItem *item = new QTreeWidgetItem(lv); item->setText(0, it.key()); if (it.key() != it.value()) { item->setText(1, it.value()); } else { item->setText(1, i18n("No Change")); } lvHeight += lv->visualItemRect(item).height(); } lvHeight += lv->horizontalScrollBar()->height() + lv->header()->height(); lv->setMinimumHeight(qMin(lvHeight, 400)); resize(qMin(width(), 500), qMin(minimumHeight(), 400)); show(); } }; // // Implementation of ConfigCategoryReader // ConfigCategoryReader::ConfigCategoryReader() : CategoryReaderInterface(), m_currentItem(0) { KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); QList categoryOrder = config.readEntry("CategoryOrder", QList()); int categoryCount[NumTypes] = { 0 }; // Keep track of each category encountered. // Set a default: if(categoryOrder.isEmpty()) categoryOrder << Artist << Album << Title << Track; QList::ConstIterator catIt = categoryOrder.constBegin(); for(; catIt != categoryOrder.constEnd(); ++catIt) { int catCount = categoryCount[*catIt]++; TagType category = static_cast(*catIt); CategoryID catId(category, catCount); m_options[catId] = TagRenamerOptions(catId); m_categoryOrder << catId; } m_folderSeparators.fill(false, m_categoryOrder.count() - 1); QList checkedSeparators = config.readEntry("CheckedDirSeparators", QList()); QList::ConstIterator it = checkedSeparators.constBegin(); for(; it != checkedSeparators.constEnd(); ++it) { if(*it < m_folderSeparators.count()) m_folderSeparators[*it] = true; } m_musicFolder = config.readPathEntry("MusicFolder", "${HOME}/music"); m_separator = config.readEntry("Separator", " - "); } QString ConfigCategoryReader::categoryValue(TagType type) const { if(!m_currentItem) return QString(); Tag *tag = m_currentItem->file().tag(); switch(type) { case Track: return QString::number(tag->track()); case Year: return QString::number(tag->year()); case Title: return tag->title(); case Artist: return tag->artist(); case Album: return tag->album(); case Genre: return tag->genre(); default: return QString(); } } QString ConfigCategoryReader::prefix(const CategoryID &category) const { return m_options[category].prefix(); } QString ConfigCategoryReader::suffix(const CategoryID &category) const { return m_options[category].suffix(); } TagRenamerOptions::EmptyActions ConfigCategoryReader::emptyAction(const CategoryID &category) const { return m_options[category].emptyAction(); } QString ConfigCategoryReader::emptyText(const CategoryID &category) const { return m_options[category].emptyText(); } QList ConfigCategoryReader::categoryOrder() const { return m_categoryOrder; } QString ConfigCategoryReader::separator() const { return m_separator; } QString ConfigCategoryReader::musicFolder() const { return m_musicFolder; } int ConfigCategoryReader::trackWidth(int categoryNum) const { return m_options[CategoryID(Track, categoryNum)].trackWidth(); } bool ConfigCategoryReader::hasFolderSeparator(int index) const { if(index >= m_folderSeparators.count()) return false; return m_folderSeparators[index]; } bool ConfigCategoryReader::isDisabled(const CategoryID &category) const { return m_options[category].disabled(); } // // Implementation of FileRenamerWidget // FileRenamerWidget::FileRenamerWidget(QWidget *parent) : QWidget(parent), CategoryReaderInterface(), m_ui(new Ui::FileRenamerBase), m_exampleFromFile(false) { m_ui->setupUi(this); // This must be created before createTagRows() is called. m_exampleDialog = new ExampleOptionsDialog(this); createTagRows(); loadConfig(); // Add correct text to combo box. m_ui->m_category->clear(); for(int i = StartTag; i < NumTypes; ++i) { QString category = TagRenamerOptions::tagTypeText(static_cast(i)); m_ui->m_category->addItem(category); } connect(m_exampleDialog, SIGNAL(signalShown()), SLOT(exampleDialogShown())); connect(m_exampleDialog, SIGNAL(signalHidden()), SLOT(exampleDialogHidden())); connect(m_exampleDialog, SIGNAL(dataChanged()), SLOT(dataSelected())); connect(m_exampleDialog, SIGNAL(fileChanged(QString)), this, SLOT(fileSelected(QString))); connect(m_ui->dlgButtonBox, SIGNAL(accepted()), SIGNAL(accepted())); connect(m_ui->dlgButtonBox, SIGNAL(rejected()), SIGNAL(rejected())); exampleTextChanged(); } void FileRenamerWidget::loadConfig() { QList checkedSeparators; KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); for(int i = 0; i < m_rows.count(); ++i) m_rows[i].options = TagRenamerOptions(m_rows[i].category); checkedSeparators = config.readEntry("CheckedDirSeparators", QList()); foreach(int separator, checkedSeparators) { if(separator < m_folderSwitches.count()) m_folderSwitches[separator]->setChecked(true); } QString path = config.readEntry("MusicFolder", "${HOME}/music"); m_ui->m_musicFolder->setUrl(QUrl::fromLocalFile(path)); m_ui->m_musicFolder->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); m_ui->m_separator->setEditText(config.readEntry("Separator", " - ")); } void FileRenamerWidget::saveConfig() { KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); QList checkedSeparators; QList categoryOrder; for(int i = 0; i < m_rows.count(); ++i) { int rowId = idOfPosition(i); // Write out in GUI order, not m_rows order m_rows[rowId].options.saveConfig(m_rows[rowId].category.categoryNumber); categoryOrder += m_rows[rowId].category.category; } for(int i = 0; i < m_folderSwitches.count(); ++i) if(m_folderSwitches[i]->isChecked() == true) checkedSeparators += i; config.writeEntry("CheckedDirSeparators", checkedSeparators); config.writeEntry("CategoryOrder", categoryOrder); config.writePathEntry("MusicFolder", m_ui->m_musicFolder->url().path()); config.writeEntry("Separator", m_ui->m_separator->currentText()); config.sync(); } FileRenamerWidget::~FileRenamerWidget() { } int FileRenamerWidget::addRowCategory(TagType category) { - static QIcon up = SmallIcon("go-up"); - static QIcon down = SmallIcon("go-down"); + static QIcon up = QIcon::fromTheme("go-up"); + static QIcon down = QIcon::fromTheme("go-down"); // Find number of categories already of this type. int categoryCount = 0; for(int i = 0; i < m_rows.count(); ++i) if(m_rows[i].category.category == category) ++categoryCount; Row row; row.category = CategoryID(category, categoryCount); row.position = m_rows.count(); int id = row.position; QFrame *frame = new QFrame(m_mainFrame); QHBoxLayout *frameLayout = new QHBoxLayout(frame); frameLayout->setMargin(3); row.widget = frame; frame->setFrameShape(QFrame::Box); frame->setLineWidth(1); QBoxLayout *mainFrameLayout = static_cast(m_mainFrame->layout()); mainFrameLayout->addWidget(frame, 1); QFrame *buttons = new QFrame(frame); QVBoxLayout *buttonLayout = new QVBoxLayout(buttons); frameLayout->addWidget(buttons); buttons->setFrameStyle(QFrame::Plain | QFrame::Box); buttons->setLineWidth(1); row.upButton = new QPushButton(buttons); row.downButton = new QPushButton(buttons); row.upButton->setIcon(up); row.downButton->setIcon(down); row.upButton->setFlat(true); row.downButton->setFlat(true); upMapper->connect(row.upButton, SIGNAL(clicked()), SLOT(map())); upMapper->setMapping(row.upButton, id); downMapper->connect(row.downButton, SIGNAL(clicked()), SLOT(map())); downMapper->setMapping(row.downButton, id); buttonLayout->addWidget(row.upButton); buttonLayout->addWidget(row.downButton); QString labelText = QString("%1").arg(TagRenamerOptions::tagTypeText(category)); QLabel *label = new QLabel(labelText, frame); frameLayout->addWidget(label, 1); label->setAlignment(Qt::AlignCenter); QVBoxLayout *optionLayout = new QVBoxLayout; frameLayout->addLayout(optionLayout); row.enableButton = new QPushButton(i18nc("remove music genre from file renamer", "Remove"), frame); optionLayout->addWidget(row.enableButton); toggleMapper->connect(row.enableButton, SIGNAL(clicked()), SLOT(map())); toggleMapper->setMapping(row.enableButton, id); row.optionsButton = new QPushButton(i18nc("file renamer genre options", "Options"), frame); optionLayout->addWidget(row.optionsButton); mapper->connect(row.optionsButton, SIGNAL(clicked()), SLOT(map())); mapper->setMapping(row.optionsButton, id); row.widget->show(); m_rows.append(row); // Disable add button if there's too many rows. if(m_rows.count() == MAX_CATEGORIES) m_ui->m_insertCategory->setEnabled(false); return id; } void FileRenamerWidget::moveSignalMappings(int oldId, int newId) { mapper->setMapping(m_rows[oldId].optionsButton, newId); downMapper->setMapping(m_rows[oldId].downButton, newId); upMapper->setMapping(m_rows[oldId].upButton, newId); toggleMapper->setMapping(m_rows[oldId].enableButton, newId); } bool FileRenamerWidget::removeRow(int id) { if(id >= m_rows.count()) { qCWarning(JUK_LOG) << "Trying to remove row, but " << id << " is out-of-range.\n"; return false; } if(m_rows.count() == 1) { qCCritical(JUK_LOG) << "Can't remove last row of File Renamer.\n"; return false; } // Remove widget. Don't delete it since it appears QSignalMapper may still need it. m_rows[id].widget->deleteLater(); m_rows[id].widget = 0; m_rows[id].enableButton = 0; m_rows[id].upButton = 0; m_rows[id].optionsButton = 0; m_rows[id].downButton = 0; int checkboxPosition = 0; // Remove first checkbox. // If not the first row, remove the checkbox before it. if(m_rows[id].position > 0) checkboxPosition = m_rows[id].position - 1; // The checkbox is contained within a layout widget, so the layout // widget is the one the needs to die. delete m_folderSwitches[checkboxPosition]->parent(); m_folderSwitches.erase(&m_folderSwitches[checkboxPosition]); // Go through all the rows and if they have the same category and a // higher categoryNumber, decrement the number. Also update the // position identifier. for(int i = 0; i < m_rows.count(); ++i) { if(i == id) continue; // Don't mess with ourself. if((m_rows[id].category.category == m_rows[i].category.category) && (m_rows[id].category.categoryNumber < m_rows[i].category.categoryNumber)) { --m_rows[i].category.categoryNumber; } // Items are moving up. if(m_rows[id].position < m_rows[i].position) --m_rows[i].position; } // Every row after the one we delete will have a different identifier, since // the identifier is simply its index into m_rows. So we need to re-do the // signal mappings for the affected rows. for(int i = id + 1; i < m_rows.count(); ++i) moveSignalMappings(i, i - 1); m_rows.erase(&m_rows[id]); // Make sure we update the buttons of affected rows. m_rows[idOfPosition(0)].upButton->setEnabled(false); m_rows[idOfPosition(m_rows.count() - 1)].downButton->setEnabled(false); // We can insert another row now, make sure GUI is updated to match. m_ui->m_insertCategory->setEnabled(true); QTimer::singleShot(0, this, SLOT(exampleTextChanged())); return true; } void FileRenamerWidget::addFolderSeparatorCheckbox() { QWidget *temp = new QWidget(m_mainFrame); m_mainFrame->layout()->addWidget(temp); QHBoxLayout *l = new QHBoxLayout(temp); QCheckBox *cb = new QCheckBox(i18n("Insert folder separator"), temp); m_folderSwitches.append(cb); l->addWidget(cb, 0, Qt::AlignCenter); cb->setChecked(false); connect(cb, SIGNAL(toggled(bool)), SLOT(exampleTextChanged())); temp->show(); } void FileRenamerWidget::createTagRows() { KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); QList categoryOrder = config.readEntry("CategoryOrder", QList()); if(categoryOrder.isEmpty()) categoryOrder << Artist << Album << Title << Track; // Setup arrays. m_rows.reserve(categoryOrder.count()); m_folderSwitches.reserve(categoryOrder.count() - 1); mapper = new QSignalMapper(this); mapper->setObjectName( QLatin1String("signal mapper" )); toggleMapper = new QSignalMapper(this); toggleMapper->setObjectName( QLatin1String("toggle mapper" )); upMapper = new QSignalMapper(this); upMapper->setObjectName( QLatin1String("up button mapper" )); downMapper = new QSignalMapper(this); downMapper->setObjectName( QLatin1String("down button mapper" )); connect(mapper, SIGNAL(mapped(int)), SLOT(showCategoryOption(int))); connect(toggleMapper, SIGNAL(mapped(int)), SLOT(slotRemoveRow(int))); connect(upMapper, SIGNAL(mapped(int)), SLOT(moveItemUp(int))); connect(downMapper, SIGNAL(mapped(int)), SLOT(moveItemDown(int))); m_mainFrame = new QFrame(m_ui->m_mainView); m_ui->m_mainView->setWidget(m_mainFrame); m_ui->m_mainView->setWidgetResizable(true); QVBoxLayout *frameLayout = new QVBoxLayout(m_mainFrame); frameLayout->setMargin(10); frameLayout->setSpacing(5); // OK, the deal with the categoryOrder variable is that we need to create // the rows in the order that they were saved in (the order given by categoryOrder). // The signal mappers operate according to the row identifier. To find the position of // a row given the identifier, use m_rows[id].position. To find the id of a given // position, use idOfPosition(position). QList::ConstIterator it = categoryOrder.constBegin(); for(; it != categoryOrder.constEnd(); ++it) { if(*it < StartTag || *it >= NumTypes) { qCCritical(JUK_LOG) << "Invalid category encountered in file renamer configuration.\n"; continue; } if(m_rows.count() == MAX_CATEGORIES) { qCCritical(JUK_LOG) << "Maximum number of File Renamer tags reached, bailing.\n"; break; } TagType i = static_cast(*it); addRowCategory(i); // Insert the directory separator checkbox if this isn't the last // item. QList::ConstIterator dup(it); // Check for last item if(++dup != categoryOrder.constEnd()) addFolderSeparatorCheckbox(); } m_rows.first().upButton->setEnabled(false); m_rows.last().downButton->setEnabled(false); // If we have maximum number of categories already, don't let the user // add more. if(m_rows.count() >= MAX_CATEGORIES) m_ui->m_insertCategory->setEnabled(false); } void FileRenamerWidget::exampleTextChanged() { // Just use .mp3 as an example if(m_exampleFromFile && (m_exampleFile.isEmpty() || !FileHandle(m_exampleFile).tag()->isValid())) { m_ui->m_exampleText->setText(i18n("No file selected, or selected file has no tags.")); return; } m_ui->m_exampleText->setText(FileRenamer::fileName(*this) + ".mp3"); } QString FileRenamerWidget::fileCategoryValue(TagType category) const { FileHandle file(m_exampleFile); Tag *tag = file.tag(); switch(category) { case Track: return QString::number(tag->track()); case Year: return QString::number(tag->year()); case Title: return tag->title(); case Artist: return tag->artist(); case Album: return tag->album(); case Genre: return tag->genre(); default: return QString(); } } QString FileRenamerWidget::categoryValue(TagType category) const { if(m_exampleFromFile) return fileCategoryValue(category); const ExampleOptions *example = m_exampleDialog->widget(); switch (category) { case Track: return example->m_exampleTrack->text(); case Year: return example->m_exampleYear->text(); case Title: return example->m_exampleTitle->text(); case Artist: return example->m_exampleArtist->text(); case Album: return example->m_exampleAlbum->text(); case Genre: return example->m_exampleGenre->text(); default: return QString(); } } QList FileRenamerWidget::categoryOrder() const { QList list; // Iterate in GUI row order. for(int i = 0; i < m_rows.count(); ++i) { int rowId = idOfPosition(i); list += m_rows[rowId].category; } return list; } bool FileRenamerWidget::hasFolderSeparator(int index) const { if(index >= m_folderSwitches.count()) return false; return m_folderSwitches[index]->isChecked(); } void FileRenamerWidget::moveItem(int id, MovementDirection direction) { QWidget *l = m_rows[id].widget; int bottom = m_rows.count() - 1; int pos = m_rows[id].position; int newPos = (direction == MoveUp) ? pos - 1 : pos + 1; // Item we're moving can't go further down after this. if((pos == (bottom - 1) && direction == MoveDown) || (pos == bottom && direction == MoveUp)) { int idBottomRow = idOfPosition(bottom); int idAboveBottomRow = idOfPosition(bottom - 1); m_rows[idBottomRow].downButton->setEnabled(true); m_rows[idAboveBottomRow].downButton->setEnabled(false); } // We're moving the top item, do some button switching. if((pos == 0 && direction == MoveDown) || (pos == 1 && direction == MoveUp)) { int idTopItem = idOfPosition(0); int idBelowTopItem = idOfPosition(1); m_rows[idTopItem].upButton->setEnabled(true); m_rows[idBelowTopItem].upButton->setEnabled(false); } // This is the item we're swapping with. int idSwitchWith = idOfPosition(newPos); QWidget *w = m_rows[idSwitchWith].widget; // Update the table of widget rows. std::swap(m_rows[id].position, m_rows[idSwitchWith].position); // Move the item two spaces above/below its previous position. It has to // be 2 spaces because of the checkbox. QBoxLayout *layout = dynamic_cast(m_mainFrame->layout()); if ( !layout ) return; layout->removeWidget(l); layout->insertWidget(2 * newPos, l); // Move the top item two spaces in the opposite direction, for a similar // reason. layout->removeWidget(w); layout->insertWidget(2 * pos, w); layout->invalidate(); QTimer::singleShot(0, this, SLOT(exampleTextChanged())); } int FileRenamerWidget::idOfPosition(int position) const { if(position >= m_rows.count()) { qCCritical(JUK_LOG) << "Search for position " << position << " out-of-range.\n"; return -1; } for(int i = 0; i < m_rows.count(); ++i) if(m_rows[i].position == position) return i; qCCritical(JUK_LOG) << "Unable to find identifier for position " << position; return -1; } int FileRenamerWidget::findIdentifier(const CategoryID &category) const { for(int index = 0; index < m_rows.count(); ++index) if(m_rows[index].category == category) return index; qCCritical(JUK_LOG) << "Unable to find match for category " << TagRenamerOptions::tagTypeText(category.category) << ", number " << category.categoryNumber; return MAX_CATEGORIES; } void FileRenamerWidget::enableAllUpButtons() { for(int i = 0; i < m_rows.count(); ++i) m_rows[i].upButton->setEnabled(true); } void FileRenamerWidget::enableAllDownButtons() { for(int i = 0; i < m_rows.count(); ++i) m_rows[i].downButton->setEnabled(true); } void FileRenamerWidget::showCategoryOption(int id) { TagOptionsDialog *dialog = new TagOptionsDialog(this, m_rows[id].options, m_rows[id].category.categoryNumber); if(dialog->exec() == QDialog::Accepted) { m_rows[id].options = dialog->options(); exampleTextChanged(); } delete dialog; } void FileRenamerWidget::moveItemUp(int id) { moveItem(id, MoveUp); } void FileRenamerWidget::moveItemDown(int id) { moveItem(id, MoveDown); } void FileRenamerWidget::toggleExampleDialog() { m_exampleDialog->setHidden(!m_exampleDialog->isHidden()); } void FileRenamerWidget::insertCategory() { TagType category = static_cast(m_ui->m_category->currentIndex()); if(m_ui->m_category->currentIndex() < 0 || category >= NumTypes) { qCCritical(JUK_LOG) << "Trying to add unknown category somehow.\n"; return; } // We need to enable the down button of the current bottom row since it // can now move down. int idBottom = idOfPosition(m_rows.count() - 1); m_rows[idBottom].downButton->setEnabled(true); addFolderSeparatorCheckbox(); // Identifier of new row. int id = addRowCategory(category); // Set its down button to be disabled. m_rows[id].downButton->setEnabled(false); m_mainFrame->layout()->invalidate(); m_ui->m_mainView->update(); // Now update according to the code in loadConfig(). m_rows[id].options = TagRenamerOptions(m_rows[id].category); exampleTextChanged(); } void FileRenamerWidget::exampleDialogShown() { m_ui->m_showExample->setText(i18n("Hide Renamer Test Dialog")); } void FileRenamerWidget::exampleDialogHidden() { m_ui->m_showExample->setText(i18n("Show Renamer Test Dialog")); } void FileRenamerWidget::fileSelected(const QString &file) { m_exampleFromFile = true; m_exampleFile = file; exampleTextChanged(); } void FileRenamerWidget::dataSelected() { m_exampleFromFile = false; exampleTextChanged(); } QString FileRenamerWidget::separator() const { return m_ui->m_separator->currentText(); } QString FileRenamerWidget::musicFolder() const { return m_ui->m_musicFolder->url().path(); } void FileRenamerWidget::slotRemoveRow(int id) { // Remove the given identified row. if(!removeRow(id)) qCCritical(JUK_LOG) << "Unable to remove row " << id; } // // Implementation of FileRenamer // FileRenamer::FileRenamer() { } void FileRenamer::rename(PlaylistItem *item) { PlaylistItemList list; list.append(item); rename(list); } void FileRenamer::rename(const PlaylistItemList &items) { ConfigCategoryReader reader; QStringList errorFiles; QMap map; QMap itemMap; for(PlaylistItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { reader.setPlaylistItem(*it); QString oldFile = (*it)->file().absFilePath(); QString extension = (*it)->file().fileInfo().suffix(); QString newFile = fileName(reader) + '.' + extension; if(oldFile != newFile) { map[oldFile] = newFile; itemMap[oldFile] = *it; } } if(itemMap.isEmpty() || ConfirmationDialog(map).exec() != QDialog::Accepted) return; QApplication::setOverrideCursor(Qt::WaitCursor); for(QMap::ConstIterator it = map.constBegin(); it != map.constEnd(); ++it) { if(moveFile(it.key(), it.value())) { itemMap[it.key()]->setFile(it.value()); itemMap[it.key()]->refresh(); setFolderIcon(QUrl::fromLocalFile(it.value()), itemMap[it.key()]); } else errorFiles << i18n("%1 to %2", it.key(), it.value()); processEvents(); } QApplication::restoreOverrideCursor(); if(!errorFiles.isEmpty()) KMessageBox::errorList(0, i18n("The following rename operations failed:\n"), errorFiles); } bool FileRenamer::moveFile(const QString &src, const QString &dest) { qCDebug(JUK_LOG) << "Moving file " << src << " to " << dest; QUrl srcURL = QUrl::fromLocalFile(src); QUrl dstURL = QUrl::fromLocalFile(dest); if(!srcURL.isValid() || !dstURL.isValid() || srcURL == dstURL) return false; QUrl dir = dstURL.resolved(QUrl::fromUserInput(".")); // resolves to path w/out filename if(!QDir().mkpath(dir.path())) { qCCritical(JUK_LOG) << "Unable to create directory " << dir.path(); return false; } // Move the file. KIO::Job *job = KIO::file_move(srcURL, dstURL); return job->exec(); } void FileRenamer::setFolderIcon(const QUrl &dstURL, const PlaylistItem *item) { if(item->file().tag()->album().isEmpty() || !item->file().coverInfo()->hasCover()) { return; } // Split path, and go through each path element. If a path element has // the album information, set its folder icon. QStringList elements = dstURL.path().split('/', QString::SkipEmptyParts); QString path; for(QStringList::ConstIterator it = elements.constBegin(); it != elements.constEnd(); ++it) { path.append('/' + (*it)); qCDebug(JUK_LOG) << "Checking path: " << path; if((*it).contains(item->file().tag()->album()) && QDir(path).exists() && !QFile::exists(path + "/.directory")) { // Seems to be a match, let's set the folder icon for the current // path. First we should write out the file. QPixmap thumb = item->file().coverInfo()->pixmap(CoverInfo::Thumbnail); thumb.save(path + "/.juk-thumbnail.png", "PNG"); KDesktopFile dirFile(path + "/.directory"); KConfigGroup desktopGroup(dirFile.desktopGroup()); if(!desktopGroup.hasKey("Icon")) { desktopGroup.writePathEntry("Icon", QString("%1/.juk-thumbnail.png").arg(path)); dirFile.sync(); } return; } } } /** * Returns iterator pointing to the last item enabled in the given list with * a non-empty value (or is required to be included). */ QList::ConstIterator lastEnabledItem(const QList &list, const CategoryReaderInterface &interface) { QList::ConstIterator it = list.constBegin(); QList::ConstIterator last = list.constEnd(); for(; it != list.constEnd(); ++it) { if(interface.isRequired(*it) || (!interface.isDisabled(*it) && !interface.categoryValue((*it).category).isEmpty())) { last = it; } } return last; } QString FileRenamer::fileName(const CategoryReaderInterface &interface) { const QList categoryOrder = interface.categoryOrder(); const QString separator = interface.separator(); const QString folder = interface.musicFolder(); QList::ConstIterator lastEnabled; int i = 0; QStringList list; QChar dirSeparator (QDir::separator()); // Use lastEnabled to properly handle folder separators. lastEnabled = lastEnabledItem(categoryOrder, interface); bool pastLast = false; // Toggles to true once we've passed lastEnabled. for(QList::ConstIterator it = categoryOrder.constBegin(); it != categoryOrder.constEnd(); ++it, ++i) { if(it == lastEnabled) pastLast = true; if(interface.isDisabled(*it)) continue; QString value = interface.value(*it); // The user can use the folder separator checkbox to add folders, so don't allow // slashes that slip in to accidentally create new folders. Should we filter this // back out when showing it in the GUI? value.replace('/', "%2f"); if(!pastLast && interface.hasFolderSeparator(i)) value.append(dirSeparator); if(interface.isRequired(*it) || !value.isEmpty()) list.append(value); } // Construct a single string representation, handling strings ending in // '/' specially QString result; for(QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); /* Empty */) { result += *it; ++it; // Manually advance iterator to check for end-of-list. // Add separator unless at a directory boundary if(it != list.constEnd() && !(*it).startsWith(dirSeparator) && // Check beginning of next item. !result.endsWith(dirSeparator)) { result += separator; } } return QString(folder + dirSeparator + result); } // vim: set et sw=4 tw=0 sta: diff --git a/playlist.cpp b/playlist.cpp index 7cc34182..3592f994 100644 --- a/playlist.cpp +++ b/playlist.cpp @@ -1,2125 +1,2127 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2008-2018 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 "playlist.h" #include "juk-exception.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlistitem.h" #include "playlistcollection.h" #include "playlistsearch.h" #include "playlistsharedsettings.h" #include "mediafiles.h" #include "collectionlist.h" #include "filerenamer.h" #include "actioncollection.h" #include "tracksequencemanager.h" #include "tag.h" #include "upcomingplaylist.h" #include "deletedialog.h" #include "webimagefetcher.h" #include "coverinfo.h" #include "coverdialog.h" #include "tagtransactionmanager.h" #include "cache.h" #include "juk_debug.h" using namespace ActionCollection; /** * Used to give every track added in the program a unique identifier. See * PlaylistItem */ quint32 g_trackID = 0; /** * Just a shortcut of sorts. */ static bool manualResize() { return action("resizeColumnsManually")->isChecked(); } //////////////////////////////////////////////////////////////////////////////// // static members //////////////////////////////////////////////////////////////////////////////// bool Playlist::m_visibleChanged = false; bool Playlist::m_shuttingDown = false; PlaylistItemList Playlist::m_history; QVector Playlist::m_backMenuItems; int Playlist::m_leftColumn = 0; //////////////////////////////////////////////////////////////////////////////// // public members //////////////////////////////////////////////////////////////////////////////// Playlist::Playlist(PlaylistCollection *collection, const QString &name, const QString &iconName) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), m_allowDuplicates(true), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_playlistName(name), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, const PlaylistItemList &items, const QString &name, const QString &iconName) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), m_allowDuplicates(true), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_playlistName(name), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); collection->setupPlaylist(this, iconName); createItems(items); } Playlist::Playlist(PlaylistCollection *collection, const QFileInfo &playlistFile, const QString &iconName) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), m_allowDuplicates(true), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_fileName(playlistFile.canonicalFilePath()), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { setup(); loadFile(m_fileName, playlistFile); collection->setupPlaylist(this, iconName); } Playlist::Playlist(PlaylistCollection *collection, bool delaySetup, int extraColumns) : QTreeWidget(collection->playlistStack()), m_collection(collection), m_fetcher(new WebImageFetcher(this)), m_allowDuplicates(true), m_applySharedSettings(true), m_columnWidthModeChanged(false), m_disableColumnWidthUpdates(true), m_time(0), m_widthsDirty(true), m_searchEnabled(true), m_rmbMenu(0), m_toolTip(0), m_blockDataChanged(false) { for(int i = 0; i < extraColumns; ++i) { addColumn(i18n("JuK")); // Placeholder text! } setup(); if(!delaySetup) collection->setupPlaylist(this, "audio-midi"); } Playlist::~Playlist() { // In some situations the dataChanged signal from clearItems will cause observers to // subsequently try to access a deleted item. Since we're going away just remove all // observers. clearObservers(); // clearItem() will take care of removing the items from the history, // so call clearItems() to make sure it happens. clearItems(items()); /* delete m_toolTip; */ if(!m_shuttingDown) m_collection->removePlaylist(this); } QString Playlist::name() const { if(m_playlistName.isEmpty()) return m_fileName.section(QDir::separator(), -1).section('.', 0, -2); else return m_playlistName; } FileHandle Playlist::currentFile() const { return playingItem() ? playingItem()->file() : FileHandle(); } int Playlist::time() const { // Since this method gets a lot of traffic, let's optimize for such. if(!m_addTime.isEmpty()) { foreach(const PlaylistItem *item, m_addTime) { if(item) m_time += item->file().tag()->seconds(); } m_addTime.clear(); } if(!m_subtractTime.isEmpty()) { foreach(const PlaylistItem *item, m_subtractTime) { if(item) m_time -= item->file().tag()->seconds(); } m_subtractTime.clear(); } return m_time; } void Playlist::playFirst() { TrackSequenceManager::instance()->setNextItem(static_cast( *QTreeWidgetItemIterator(const_cast(this), QTreeWidgetItemIterator::NotHidden))); action("forward")->trigger(); } void Playlist::playNextAlbum() { PlaylistItem *current = TrackSequenceManager::instance()->currentItem(); if(!current) return; // No next album if we're not already playing. QString currentAlbum = current->file().tag()->album(); current = TrackSequenceManager::instance()->nextItem(); while(current && current->file().tag()->album() == currentAlbum) current = TrackSequenceManager::instance()->nextItem(); TrackSequenceManager::instance()->setNextItem(current); action("forward")->trigger(); } void Playlist::playNext() { TrackSequenceManager::instance()->setCurrentPlaylist(this); setPlaying(TrackSequenceManager::instance()->nextItem()); } void Playlist::stop() { m_history.clear(); setPlaying(nullptr); } void Playlist::playPrevious() { if(!playingItem()) return; bool random = action("randomPlay") && action("randomPlay")->isChecked(); PlaylistItem *previous = 0; if(random && !m_history.isEmpty()) { PlaylistItemList::Iterator last = --m_history.end(); previous = *last; m_history.erase(last); } else { m_history.clear(); previous = TrackSequenceManager::instance()->previousItem(); } if(!previous) previous = static_cast(playingItem()->itemAbove()); setPlaying(previous, false); } void Playlist::setName(const QString &n) { m_collection->addNameToDict(n); m_collection->removeNameFromDict(m_playlistName); m_playlistName = n; emit signalNameChanged(m_playlistName); } void Playlist::save() { if(m_fileName.isEmpty()) return saveAs(); QFile file(m_fileName); if(!file.open(QIODevice::WriteOnly)) return KMessageBox::error(this, i18n("Could not save to file %1.", m_fileName)); QTextStream stream(&file); QStringList fileList = files(); foreach(const QString &file, fileList) stream << file << endl; file.close(); } void Playlist::saveAs() { m_collection->removeFileFromDict(m_fileName); m_fileName = MediaFiles::savePlaylistDialog(name(), this); if(!m_fileName.isEmpty()) { m_collection->addFileToDict(m_fileName); // If there's no playlist name set, use the file name. if(m_playlistName.isEmpty()) emit signalNameChanged(name()); save(); } } void Playlist::updateDeletedItem(PlaylistItem *item) { m_members.remove(item->file().absFilePath()); m_search.clearItem(item); m_history.removeAll(item); m_addTime.removeAll(item); m_subtractTime.removeAll(item); } void Playlist::clearItem(PlaylistItem *item) { // Automatically updates internal structs via updateDeletedItem delete item; playlistItemsChanged(); } void Playlist::clearItems(const PlaylistItemList &items) { foreach(PlaylistItem *item, items) delete item; playlistItemsChanged(); } PlaylistItem *Playlist::playingItem() // static { return PlaylistItem::playingItems().isEmpty() ? 0 : PlaylistItem::playingItems().front(); } QStringList Playlist::files() const { QStringList list; for(QTreeWidgetItemIterator it(const_cast(this)); *it; ++it) list.append(static_cast(*it)->file().absFilePath()); return list; } PlaylistItemList Playlist::items() { return items(QTreeWidgetItemIterator::IteratorFlag(0)); } PlaylistItemList Playlist::visibleItems() { return items(QTreeWidgetItemIterator::NotHidden); } PlaylistItemList Playlist::selectedItems() { return items(QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden); } PlaylistItem *Playlist::firstChild() const { return static_cast(topLevelItem(0)); } void Playlist::updateLeftColumn() { int newLeftColumn = leftMostVisibleColumn(); if(m_leftColumn != newLeftColumn) { updatePlaying(); m_leftColumn = newLeftColumn; } } void Playlist::setItemsVisible(const PlaylistItemList &items, bool visible) // static { m_visibleChanged = true; foreach(PlaylistItem *playlistItem, items) playlistItem->setHidden(!visible); } void Playlist::setSearch(const PlaylistSearch &s) { m_search = s; if(!m_searchEnabled) return; setItemsVisible(s.matchedItems(), true); setItemsVisible(s.unmatchedItems(), false); TrackSequenceManager::instance()->iterator()->playlistChanged(); } void Playlist::setSearchEnabled(bool enabled) { if(m_searchEnabled == enabled) return; m_searchEnabled = enabled; if(enabled) { setItemsVisible(m_search.matchedItems(), true); setItemsVisible(m_search.unmatchedItems(), false); } else setItemsVisible(items(), true); } // Mostly seems to be for DynamicPlaylist // TODO: See if this can't all be eliminated by making 'is-playing' a predicate // of the playlist item itself void Playlist::synchronizePlayingItems(const PlaylistList &sources, bool setMaster) { foreach(const Playlist *p, sources) { if(p->playing()) { CollectionListItem *base = playingItem()->collectionItem(); for(QTreeWidgetItemIterator itemIt(this); *itemIt; ++itemIt) { PlaylistItem *item = static_cast(*itemIt); if(base == item->collectionItem()) { item->setPlaying(true, setMaster); PlaylistItemList playing = PlaylistItem::playingItems(); TrackSequenceManager::instance()->setCurrent(item); return; } } return; } } } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void Playlist::copy() { PlaylistItemList items = selectedItems(); QList urls; foreach(PlaylistItem *item, items) { urls << QUrl::fromLocalFile(item->file().absFilePath()); } QMimeData *mimeData = new QMimeData; mimeData->setUrls(urls); QApplication::clipboard()->setMimeData(mimeData, QClipboard::Clipboard); } void Playlist::paste() { decode(QApplication::clipboard()->mimeData(), static_cast(currentItem())); } void Playlist::clear() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = items(); clearItems(l); } void Playlist::slotRefresh() { PlaylistItemList l = selectedItems(); if(l.isEmpty()) l = visibleItems(); QApplication::setOverrideCursor(Qt::WaitCursor); foreach(PlaylistItem *item, l) { item->refreshFromDisk(); if(!item->file().tag() || !item->file().fileInfo().exists()) { qCDebug(JUK_LOG) << "Error while trying to refresh the tag. " << "This file has probably been removed."; delete item->collectionItem(); } processEvents(); } QApplication::restoreOverrideCursor(); } void Playlist::slotRenameFile() { FileRenamer renamer; PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; emit signalEnableDirWatch(false); m_blockDataChanged = true; renamer.rename(items); m_blockDataChanged = false; playlistItemsChanged(); emit signalEnableDirWatch(true); } void Playlist::slotViewCover() { const PlaylistItemList items = selectedItems(); if (items.isEmpty()) return; foreach(const PlaylistItem *item, items) item->file().coverInfo()->popup(); } void Playlist::slotRemoveCover() { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; int button = KMessageBox::warningContinueCancel(this, i18n("Are you sure you want to delete these covers?"), QString(), KGuiItem(i18n("&Delete Covers"))); if(button == KMessageBox::Continue) refreshAlbums(items); } void Playlist::slotShowCoverManager() { static CoverDialog *managerDialog = 0; if(!managerDialog) managerDialog = new CoverDialog(this); managerDialog->show(); } void Playlist::slotAddCover(bool retrieveLocal) { PlaylistItemList items = selectedItems(); if(items.isEmpty()) return; if(!retrieveLocal) { m_fetcher->setFile((*items.begin())->file()); m_fetcher->searchCover(); return; } QUrl file = QFileDialog::getOpenFileUrl( this, i18n("Select Cover Image File"), QUrl::fromLocalFile(QDir::home().path()), i18n("Images (*.png *.jpg)"), nullptr, 0, QStringList() << QStringLiteral("file") ); if(file.isEmpty()) return; QString artist = items.front()->file().tag()->artist(); QString album = items.front()->file().tag()->album(); coverKey newId = CoverManager::addCover(file, artist, album); if(newId != CoverManager::NoMatch) refreshAlbums(items, newId); } // Called when image fetcher has added a new cover. void Playlist::slotCoverChanged(int coverId) { qCDebug(JUK_LOG) << "Refreshing information for newly changed covers.\n"; refreshAlbums(selectedItems(), coverId); } void Playlist::slotGuessTagInfo(TagGuesser::Type type) { QApplication::setOverrideCursor(Qt::WaitCursor); const PlaylistItemList items = selectedItems(); setDynamicListsFrozen(true); m_blockDataChanged = true; foreach(PlaylistItem *item, items) { item->guessTagInfo(type); processEvents(); } // MusicBrainz queries automatically commit at this point. What would // be nice is having a signal emitted when the last query is completed. if(type == TagGuesser::FileName) TagTransactionManager::instance()->commit(); m_blockDataChanged = false; playlistItemsChanged(); setDynamicListsFrozen(false); QApplication::restoreOverrideCursor(); } void Playlist::slotReload() { QFileInfo fileInfo(m_fileName); if(!fileInfo.exists() || !fileInfo.isFile() || !fileInfo.isReadable()) return; clearItems(items()); loadFile(m_fileName, fileInfo); } void Playlist::slotWeightDirty(int column) { if(column < 0) { m_weightDirty.clear(); for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) m_weightDirty.append(i); } return; } if(!m_weightDirty.contains(column)) m_weightDirty.append(column); } void Playlist::slotShowPlaying() { if(!playingItem()) return; Playlist *l = playingItem()->playlist(); l->clearSelection(); // Raise the playlist before selecting the items otherwise the tag editor // will not update when it gets the selectionChanged() notification // because it will think the user is choosing a different playlist but not // selecting a different item. m_collection->raise(l); l->setCurrentItem(playingItem()); l->scrollToItem(playingItem()); } void Playlist::slotColumnResizeModeChanged() { if(manualResize()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } if(!manualResize()) slotUpdateColumnWidths(); SharedSettings::instance()->sync(); } void Playlist::playlistItemsChanged() { if(m_blockDataChanged) return; PlaylistInterface::playlistItemsChanged(); } //////////////////////////////////////////////////////////////////////////////// // protected members //////////////////////////////////////////////////////////////////////////////// void Playlist::removeFromDisk(const PlaylistItemList &items) { if(isVisible() && !items.isEmpty()) { QStringList files; foreach(const PlaylistItem *item, items) files.append(item->file().absFilePath()); DeleteDialog dialog(this); m_blockDataChanged = true; if(dialog.confirmDeleteList(files)) { bool shouldDelete = dialog.shouldDelete(); QStringList errorFiles; foreach(PlaylistItem *item, items) { if(playingItem() == item) action("forward")->trigger(); QString removePath = item->file().absFilePath(); QUrl removeUrl = QUrl::fromLocalFile(removePath); if((!shouldDelete && KIO::trash(removeUrl)->exec()) || (shouldDelete && QFile::remove(removePath))) { delete item->collectionItem(); } else errorFiles.append(item->file().absFilePath()); } if(!errorFiles.isEmpty()) { QString errorMsg = shouldDelete ? i18n("Could not delete these files") : i18n("Could not move these files to the Trash"); KMessageBox::errorList(this, errorMsg, errorFiles); } } m_blockDataChanged = false; playlistItemsChanged(); } } void Playlist::synchronizeItemsTo(const PlaylistItemList &itemList) { const auto &existingItems = items(); if(qAbs(itemList.count() - existingItems.count()) > qMax(itemList.count(), existingItems.count()) / 2) { // Large imbalance in list sizes, just clear all and add without // figuring out the diff also clearItems(existingItems); createItems(itemList); return; } // Determine differences between existing playlist items and patch up QHash oldItems; oldItems.reserve(qMax(existingItems.count(), itemList.count())); for(const auto &item : existingItems) { oldItems.insert(item->collectionItem(), item); } PlaylistItemList newItems; for(const auto &item : itemList) { if(oldItems.remove(item->collectionItem()) == 0) { newItems.append(item->collectionItem()); } } clearItems(PlaylistItemList(oldItems.values())); createItems(newItems); } void Playlist::dragEnterEvent(QDragEnterEvent *e) { if(CoverDrag::isCover(e->mimeData())) { //setDropHighlighter(true); setDropIndicatorShown(false); e->accept(); return; } setDropIndicatorShown(true); if(e->mimeData()->hasUrls() && !e->mimeData()->urls().isEmpty()) e->acceptProposedAction(); else e->ignore(); } bool Playlist::acceptDrag(QDropEvent *e) const { return CoverDrag::isCover(e->mimeData()) || e->mimeData()->hasUrls(); } void Playlist::decode(const QMimeData *s, PlaylistItem *item) { Q_UNUSED(s); Q_UNUSED(item); // TODO Re-add drag-drop } bool Playlist::eventFilter(QObject *watched, QEvent *e) { if(watched == header()) { switch(e->type()) { case QEvent::MouseMove: { if((static_cast(e)->modifiers() & Qt::LeftButton) == Qt::LeftButton && !action("resizeColumnsManually")->isChecked()) { m_columnWidthModeChanged = true; action("resizeColumnsManually")->setChecked(true); slotColumnResizeModeChanged(); } break; } case QEvent::MouseButtonPress: { if(static_cast(e)->button() == Qt::RightButton) m_headerMenu->popup(QCursor::pos()); break; } case QEvent::MouseButtonRelease: { if(m_columnWidthModeChanged) { m_columnWidthModeChanged = false; notifyUserColumnWidthModeChanged(); } if(!manualResize() && m_widthsDirty) QTimer::singleShot(0, this, SLOT(slotUpdateColumnWidths())); break; } default: break; } } return QTreeWidget::eventFilter(watched, e); } void Playlist::keyPressEvent(QKeyEvent *event) { if(event->key() == Qt::Key_Up) { using ::operator|; QTreeWidgetItemIterator selected(this, QTreeWidgetItemIterator::Selected | QTreeWidgetItemIterator::NotHidden); if(*selected) { QTreeWidgetItemIterator visible(this, QTreeWidgetItemIterator::NotHidden); if(*selected == *visible) QApplication::postEvent(parent(), new FocusUpEvent); } } else if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { slotPlayCurrent(); } QTreeWidget::keyPressEvent(event); } QStringList Playlist::mimeTypes() const { return QStringList("text/uri-list"); } QMimeData* Playlist::mimeData(const QList items) const { QList urls; foreach(QTreeWidgetItem *item, items) { urls << QUrl::fromLocalFile(static_cast(item)->file().absFilePath()); } QMimeData *urlDrag = new QMimeData(); urlDrag->setUrls(urls); return urlDrag; } bool Playlist::dropMimeData(QTreeWidgetItem *parent, int index, const QMimeData *data, Qt::DropAction action) { // TODO: Re-add DND Q_UNUSED(parent); Q_UNUSED(index); Q_UNUSED(data); Q_UNUSED(action); return false; } void Playlist::dropEvent(QDropEvent *e) { QPoint vp = e->pos(); PlaylistItem *item = static_cast(itemAt(vp)); // First see if we're dropping a cover, if so we can get it out of the // way early. if(item && CoverDrag::isCover(e->mimeData())) { coverKey id = CoverDrag::idFromData(e->mimeData()); // If the item we dropped on is selected, apply cover to all selected // items, otherwise just apply to the dropped item. if(item->isSelected()) { const PlaylistItemList selItems = selectedItems(); foreach(PlaylistItem *playlistItem, selItems) { playlistItem->file().coverInfo()->setCoverId(id); playlistItem->refresh(); } } else { item->file().coverInfo()->setCoverId(id); item->refresh(); } return; } // When dropping on the toUpper half of an item, insert before this item. // This is what the user expects, and also allows the insertion at // top of the list QRect rect = visualItemRect(item); if(!item) item = static_cast(topLevelItem(topLevelItemCount() - 1)); else if(vp.y() < rect.y() + rect.height() / 2) item = static_cast(item->itemAbove()); m_blockDataChanged = true; if(e->source() == this) { // Since we're trying to arrange things manually, turn off sorting. sortItems(columnCount() + 1, Qt::AscendingOrder); const QList items = QTreeWidget::selectedItems(); foreach(QTreeWidgetItem *listViewItem, items) { if(!item) { // Insert the item at the top of the list. This is a bit ugly, // but I don't see another way. takeItem(listViewItem); insertItem(listViewItem); } //else // listViewItem->moveItem(item); item = static_cast(listViewItem); } } else decode(e->mimeData(), item); m_blockDataChanged = false; playlistItemsChanged(); emit signalPlaylistItemsDropped(this); QTreeWidget::dropEvent(e); } void Playlist::showEvent(QShowEvent *e) { if(m_applySharedSettings) { SharedSettings::instance()->apply(this); m_applySharedSettings = false; } QTreeWidget::showEvent(e); } void Playlist::applySharedSettings() { m_applySharedSettings = true; } void Playlist::read(QDataStream &s) { s >> m_playlistName >> m_fileName; // m_fileName is probably empty. if(m_playlistName.isEmpty()) throw BICStreamException(); // Do not sort. Add the files in the order they were saved. setSortingEnabled(false); QStringList files; s >> files; QTreeWidgetItem *after = 0; m_blockDataChanged = true; foreach(const QString &file, files) { if(file.isEmpty()) throw BICStreamException(); after = createItem(FileHandle(file), after); } m_blockDataChanged = false; playlistItemsChanged(); m_collection->setupPlaylist(this, "audio-midi"); } void Playlist::paintEvent(QPaintEvent *pe) { // If there are columns that need to be updated, well, update them. if(!m_weightDirty.isEmpty() && !manualResize()) { calculateColumnWeights(); slotUpdateColumnWidths(); } QTreeWidget::paintEvent(pe); } void Playlist::resizeEvent(QResizeEvent *re) { // If the width of the view has changed, manually update the column // widths. if(re->size().width() != re->oldSize().width() && !manualResize()) slotUpdateColumnWidths(); QTreeWidget::resizeEvent(re); } // Reimplemented to show a visual indication of which of the view's playlist // items is actually playing. void Playlist::drawRow(QPainter *p, const QStyleOptionViewItem &option, const QModelIndex &index) const { PlaylistItem *item = static_cast(itemFromIndex(index)); if(Q_LIKELY(!PlaylistItem::playingItems().contains(item))) { return QTreeWidget::drawRow(p, option, index); } // Seems that the view draws the background now so we have to do this // manually p->fillRect(option.rect, QPalette{}.midlight()); QStyleOptionViewItem newOption {option}; newOption.font.setBold(true); QTreeWidget::drawRow(p, newOption, index); } void Playlist::insertItem(QTreeWidgetItem *item) { // Because we're called from the PlaylistItem ctor, item may not be a // PlaylistItem yet (it would be QListViewItem when being inserted. But, // it will be a PlaylistItem by the time it matters, but be careful if // you need to use the PlaylistItem from here. m_addTime.append(static_cast(item)); QTreeWidget::insertTopLevelItem(0, item); } void Playlist::takeItem(QTreeWidgetItem *item) { // See the warning in Playlist::insertItem. m_subtractTime.append(static_cast(item)); int index = indexOfTopLevelItem(item); QTreeWidget::takeTopLevelItem(index); } void Playlist::addColumn(const QString &label, int) { m_columns.append(label); setHeaderLabels(m_columns); } PlaylistItem *Playlist::createItem(const FileHandle &file, QTreeWidgetItem *after) { return createItem(file, after); } void Playlist::createItems(const PlaylistItemList &siblings, PlaylistItem *after) { createItems(siblings, after); } void Playlist::addFiles(const QStringList &files, PlaylistItem *after) { if(!after) after = static_cast(topLevelItem(topLevelItemCount() - 1)); QApplication::setOverrideCursor(Qt::WaitCursor); m_blockDataChanged = true; FileHandleList queue; foreach(const QString &file, files) addFile(file, queue, true, &after); addFileHelper(queue, &after, true); m_blockDataChanged = false; slotWeightDirty(); playlistItemsChanged(); QApplication::restoreOverrideCursor(); } void Playlist::refreshAlbums(const PlaylistItemList &items, coverKey id) { QList< QPair > albums; bool setAlbumCovers = items.count() == 1; foreach(const PlaylistItem *item, items) { QString artist = item->file().tag()->artist(); QString album = item->file().tag()->album(); if(!albums.contains(qMakePair(artist, album))) albums.append(qMakePair(artist, album)); item->file().coverInfo()->setCoverId(id); if(setAlbumCovers) item->file().coverInfo()->applyCoverToWholeAlbum(true); } for(QList< QPair >::ConstIterator it = albums.constBegin(); it != albums.constEnd(); ++it) { refreshAlbum((*it).first, (*it).second); } } void Playlist::updatePlaying() const { foreach(const PlaylistItem *item, PlaylistItem::playingItems()) item->treeWidget()->viewport()->update(); } void Playlist::refreshAlbum(const QString &artist, const QString &album) { ColumnList columns; columns.append(PlaylistItem::ArtistColumn); PlaylistSearch::Component artistComponent(artist, false, columns, PlaylistSearch::Component::Exact); columns.clear(); columns.append(PlaylistItem::AlbumColumn); PlaylistSearch::Component albumComponent(album, false, columns, PlaylistSearch::Component::Exact); PlaylistSearch::ComponentList components; components.append(artist); components.append(album); PlaylistList playlists; playlists.append(CollectionList::instance()); PlaylistSearch search(playlists, components); const PlaylistItemList matches = search.matchedItems(); foreach(PlaylistItem *item, matches) item->refresh(); } void Playlist::hideColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(false); break; } } if(isColumnHidden(c)) return; QTreeWidget::hideColumn(c); if(c == m_leftColumn) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->hideColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::showColumn(int c, bool updateSearch) { foreach (QAction *action, m_headerMenu->actions()) { if(!action) continue; if (action->data().toInt() == c) { action->setChecked(true); break; } } if(!isColumnHidden(c)) return; QTreeWidget::showColumn(c); if(c == leftMostVisibleColumn()) { updatePlaying(); m_leftColumn = leftMostVisibleColumn(); } if(!manualResize()) { slotUpdateColumnWidths(); viewport()->update(); } if(this != CollectionList::instance()) CollectionList::instance()->showColumn(c, false); if(updateSearch) redisplaySearch(); } void Playlist::sortByColumn(int column, Qt::SortOrder order) { setSortingEnabled(true); QTreeWidget::sortByColumn(column, order); } void Playlist::slotInitialize() { addColumn(i18n("Track Name")); addColumn(i18n("Artist")); addColumn(i18n("Album")); addColumn(i18n("Cover")); addColumn(i18nc("cd track number", "Track")); addColumn(i18n("Genre")); addColumn(i18n("Year")); addColumn(i18n("Length")); addColumn(i18n("Bitrate")); addColumn(i18n("Comment")); addColumn(i18n("File Name")); addColumn(i18n("File Name (full path)")); setAllColumnsShowFocus(true); setSelectionMode(QTreeWidget::ExtendedSelection); header()->setSortIndicatorShown(true); m_columnFixedWidths.resize(columnCount()); ////////////////////////////////////////////////// // setup header RMB menu ////////////////////////////////////////////////// QAction *showAction; for(int i = 0; i < header()->count(); ++i) { if(i - columnOffset() == PlaylistItem::FileNameColumn) m_headerMenu->addSeparator(); showAction = new QAction(headerItem()->text(i), m_headerMenu); showAction->setData(i); showAction->setCheckable(true); showAction->setChecked(!isColumnHidden(i)); m_headerMenu->addAction(showAction); resizeColumnToContents(i); } connect(m_headerMenu, SIGNAL(triggered(QAction*)), this, SLOT(slotToggleColumnVisible(QAction*))); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowRMBMenu(QPoint))); connect(this, &QTreeWidget::itemChanged, this, &Playlist::slotInlineEditDone); connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotPlayCurrent())); connect(action("resizeColumnsManually"), SIGNAL(triggered()), this, SLOT(slotColumnResizeModeChanged())); if(action("resizeColumnsManually")->isChecked()) { header()->setSectionResizeMode(QHeaderView::Interactive); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); } else { header()->setSectionResizeMode(QHeaderView::Fixed); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setDragEnabled(true); m_disableColumnWidthUpdates = false; } void Playlist::setupItem(PlaylistItem *item) { item->setTrackId(g_trackID); g_trackID++; if(!m_search.isEmpty()) item->setHidden(!m_search.checkItem(item)); if(topLevelItemCount() <= 2 && !manualResize()) { slotWeightDirty(); slotUpdateColumnWidths(); viewport()->update(); } } void Playlist::setDynamicListsFrozen(bool frozen) { m_collection->setDynamicListsFrozen(frozen); } CollectionListItem *Playlist::collectionListItem(const FileHandle &file) { CollectionListItem *item = CollectionList::instance()->lookup(file.absFilePath()); if(!item) { if(!QFile::exists(file.absFilePath())) { qCCritical(JUK_LOG) << "File" << file.absFilePath() << "does not exist."; return nullptr; } item = CollectionList::instance()->createItem(file); } return item; } //////////////////////////////////////////////////////////////////////////////// // protected slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotPopulateBackMenu() const { if(!playingItem()) return; QMenu *menu = action("back")->menu(); menu->clear(); m_backMenuItems.clear(); m_backMenuItems.reserve(10); int count = 0; PlaylistItemList::ConstIterator it = m_history.constEnd(); QAction *action; while(it != m_history.constBegin() && count < 10) { ++count; --it; action = new QAction((*it)->file().tag()->title(), menu); action->setData(count - 1); menu->addAction(action); m_backMenuItems << *it; } } void Playlist::slotPlayFromBackMenu(QAction *backAction) const { int number = backAction->data().toInt(); if(number >= m_backMenuItems.size()) return; TrackSequenceManager::instance()->setNextItem(m_backMenuItems[number]); action("forward")->trigger(); } //////////////////////////////////////////////////////////////////////////////// // private members //////////////////////////////////////////////////////////////////////////////// void Playlist::setup() { setRootIsDecorated(false); setContextMenuPolicy(Qt::CustomContextMenu); setUniformRowHeights(true); setEditTriggers(QAbstractItemView::EditKeyPressed); // Don't edit on double-click connect(header(), SIGNAL(sectionMoved(int,int,int)), this, SLOT(slotColumnOrderChanged(int,int,int))); connect(m_fetcher, SIGNAL(signalCoverChanged(int)), this, SLOT(slotCoverChanged(int))); // Prevent list of selected items from changing while internet search is in // progress. connect(this, SIGNAL(itemSelectionChanged()), m_fetcher, SLOT(abortSearch())); sortByColumn(1, Qt::AscendingOrder); // This apparently must be created very early in initialization for other // Playlist code requiring m_headerMenu. m_columnVisibleAction = new KActionMenu(i18n("&Show Columns"), this); ActionCollection::actions()->addAction("showColumns", m_columnVisibleAction); m_headerMenu = m_columnVisibleAction->menu(); header()->installEventFilter(this); // TODO: Determine if other stuff in setup must happen before slotInitialize(). // Explicitly call slotInitialize() so that the columns are added before // SharedSettings::apply() sets the visible and hidden ones. slotInitialize(); } void Playlist::loadFile(const QString &fileName, const QFileInfo &fileInfo) { QFile file(fileName); if(!file.open(QIODevice::ReadOnly)) return; QTextStream stream(&file); // Turn off non-explicit sorting. setSortingEnabled(false); PlaylistItem *after = nullptr; m_disableColumnWidthUpdates = true; m_blockDataChanged = true; while(!stream.atEnd()) { QString itemName = stream.readLine().trimmed(); QFileInfo item(itemName); if(item.isRelative()) item.setFile(QDir::cleanPath(fileInfo.absolutePath() + '/' + itemName)); if(item.exists() && item.isFile() && item.isReadable() && MediaFiles::isMediaFile(item.fileName())) { after = createItem(FileHandle(item), after); } } m_blockDataChanged = false; m_disableColumnWidthUpdates = false; file.close(); playlistItemsChanged(); } void Playlist::setPlaying(PlaylistItem *item, bool addToHistory) { if(playingItem() == item) return; if(playingItem()) { if(addToHistory) { if(playingItem()->playlist() == playingItem()->playlist()->m_collection->upcomingPlaylist()) m_history.append(playingItem()->collectionItem()); else m_history.append(playingItem()); } playingItem()->setPlaying(false); } TrackSequenceManager::instance()->setCurrent(item); // TODO is this replaced by MPRIS2? //kapp->dcopClient()->emitDCOPSignal("Player", "trackChanged()", data); if(!item) return; item->setPlaying(true); bool enableBack = !m_history.isEmpty(); action("back")->menu()->setEnabled(enableBack); } bool Playlist::playing() const { return playingItem() && this == playingItem()->playlist(); } int Playlist::leftMostVisibleColumn() const { int i = 0; while(i < PlaylistItem::lastColumn() && isColumnHidden(i)) i++; return i < PlaylistItem::lastColumn() ? i : 0; } PlaylistItemList Playlist::items(QTreeWidgetItemIterator::IteratorFlags flags) { PlaylistItemList list; for(QTreeWidgetItemIterator it(this, flags); *it; ++it) list.append(static_cast(*it)); return list; } void Playlist::calculateColumnWeights() { if(m_disableColumnWidthUpdates) return; PlaylistItemList l = items(); QList::Iterator columnIt; QVector averageWidth(columnCount()); double itemCount = l.size(); QVector cachedWidth; // Here we're not using a real average, but averaging the squares of the // column widths and then using the square root of that value. This gives // a nice weighting to the longer columns without doing something arbitrary // like adding a fixed amount of padding. foreach(PlaylistItem *item, l) { cachedWidth = item->cachedWidths(); // Extra columns start at 0, but those weights aren't shared with all // items. for(int i = 0; i < columnOffset(); ++i) { averageWidth[i] += std::pow(double(columnWidth(i)), 2.0) / itemCount; } for(int column = columnOffset(); column < columnCount(); ++column) { averageWidth[column] += std::pow(double(cachedWidth[column - columnOffset()]), 2.0) / itemCount; } } if(m_columnWeights.isEmpty()) m_columnWeights.fill(-1, columnCount()); foreach(int column, m_weightDirty) { m_columnWeights[column] = int(std::sqrt(averageWidth[column]) + 0.5); } m_weightDirty.clear(); } void Playlist::addFile(const QString &file, FileHandleList &files, bool importPlaylists, PlaylistItem **after) { if(hasItem(file) && !m_allowDuplicates) return; addFileHelper(files, after); // Our biggest thing that we're fighting during startup is too many stats // of files. Make sure that we don't do one here if it's not needed. const CollectionListItem *item = CollectionList::instance()->lookup(file); if(item && !item->file().isNull()) { FileHandle cached(item->file()); cached.tag(); files.append(cached); return; } const QFileInfo fileInfo(QDir::cleanPath(file)); if(!fileInfo.exists()) return; const QString canonicalPath = fileInfo.canonicalFilePath(); if(fileInfo.isFile() && fileInfo.isReadable()) { if(MediaFiles::isMediaFile(file)) { FileHandle f(fileInfo); f.tag(); files.append(f); } } if(importPlaylists && MediaFiles::isPlaylistFile(file) && !m_collection->containsPlaylistFile(canonicalPath)) { new Playlist(m_collection, fileInfo); return; } if(fileInfo.isDir()) { foreach(const QString &directory, m_collection->excludedFolders()) { if(canonicalPath.startsWith(directory)) return; // Exclude it } QDirIterator dirIterator(canonicalPath, QDir::AllEntries | QDir::NoDotAndDotDot); while(dirIterator.hasNext()) { // We set importPlaylists to the value from the add directories // dialog as we want to load all of the ones that the user has // explicitly asked for, but not those that we find in lower // directories. addFile(dirIterator.next(), files, m_collection->importPlaylists(), after); } } } void Playlist::addFileHelper(FileHandleList &files, PlaylistItem **after, bool ignoreTimer) { static QTime time = QTime::currentTime(); // Process new items every 10 seconds, when we've loaded 1000 items, or when // it's been requested in the API. if(ignoreTimer || time.elapsed() > 10000 || (files.count() >= 1000 && time.elapsed() > 1000)) { time.restart(); const bool focus = hasFocus(); const bool visible = isVisible() && files.count() > 20; if(visible) m_collection->raiseDistraction(); PlaylistItem *newAfter = *after; foreach(const FileHandle &fileHandle, files) newAfter = createItem(fileHandle, newAfter); *after = newAfter; files.clear(); if(visible) m_collection->lowerDistraction(); if(focus) setFocus(); } } //////////////////////////////////////////////////////////////////////////////// // private slots //////////////////////////////////////////////////////////////////////////////// void Playlist::slotUpdateColumnWidths() { if(m_disableColumnWidthUpdates || manualResize()) return; // Make sure that the column weights have been initialized before trying to // update the columns. QList visibleColumns; for(int i = 0; i < columnCount(); i++) { if(!isColumnHidden(i)) visibleColumns.append(i); } if(count() == 0) { foreach(int column, visibleColumns) setColumnWidth(column, header()->fontMetrics().width(headerItem()->text(column)) + 10); return; } if(m_columnWeights.isEmpty()) return; // First build a list of minimum widths based on the strings in the listview // header. We won't let the width of the column go below this width. QVector minimumWidth(columnCount(), 0); int minimumWidthTotal = 0; // Also build a list of either the minimum *or* the fixed width -- whichever is // greater. QVector minimumFixedWidth(columnCount(), 0); int minimumFixedWidthTotal = 0; foreach(int column, visibleColumns) { minimumWidth[column] = header()->fontMetrics().width(headerItem()->text(column)) + 10; minimumWidthTotal += minimumWidth[column]; minimumFixedWidth[column] = qMax(minimumWidth[column], m_columnFixedWidths[column]); minimumFixedWidthTotal += minimumFixedWidth[column]; } // Make sure that the width won't get any smaller than this. We have to // account for the scrollbar as well. Since this method is called from the // resize event this will set a pretty hard toLower bound on the size. setMinimumWidth(minimumWidthTotal + verticalScrollBar()->width()); // If we've got enough room for the fixed widths (larger than the minimum // widths) then instead use those for our "minimum widths". if(minimumFixedWidthTotal < viewport()->width()) { minimumWidth = minimumFixedWidth; minimumWidthTotal = minimumFixedWidthTotal; } // We've got a list of columns "weights" based on some statistics gathered // about the widths of the items in that column. We need to find the total // useful weight to use as a divisor for each column's weight. double totalWeight = 0; foreach(int column, visibleColumns) totalWeight += m_columnWeights[column]; // Computed a "weighted width" for each visible column. This would be the // width if we didn't have to handle the cases of minimum and maximum widths. QVector weightedWidth(columnCount(), 0); foreach(int column, visibleColumns) weightedWidth[column] = int(double(m_columnWeights[column]) / totalWeight * viewport()->width() + 0.5); // The "extra" width for each column. This is the weighted width less the // minimum width or zero if the minimum width is greater than the weighted // width. QVector extraWidth(columnCount(), 0); // This is used as an indicator if we have any columns where the weighted // width is less than the minimum width. If this is false then we can // just use the weighted width with no problems, otherwise we have to // "readjust" the widths. bool readjust = false; // If we have columns where the weighted width is less than the minimum width // we need to steal that space from somewhere. The amount that we need to // steal is the "neededWidth". int neededWidth = 0; // While we're on the topic of stealing -- we have to have somewhere to steal // from. availableWidth is the sum of the amount of space beyond the minimum // width that each column has been allocated -- the sum of the values of // extraWidth[]. int availableWidth = 0; // Fill in the values discussed above. foreach(int column, visibleColumns) { if(weightedWidth[column] < minimumWidth[column]) { readjust = true; extraWidth[column] = 0; neededWidth += minimumWidth[column] - weightedWidth[column]; } else { extraWidth[column] = weightedWidth[column] - minimumWidth[column]; availableWidth += extraWidth[column]; } } // The adjustmentRatio is the amount of the "extraWidth[]" that columns will // actually be given. double adjustmentRatio = (double(availableWidth) - double(neededWidth)) / double(availableWidth); // This will be the sum of the total space that we actually use. Because of // rounding error this won't be the exact available width. int usedWidth = 0; // Now set the actual column widths. If the weighted widths are all greater // than the minimum widths, just use those, otherwise use the "readjusted // weighted width". foreach(int column, visibleColumns) { int width; if(readjust) { int adjustedExtraWidth = int(double(extraWidth[column]) * adjustmentRatio + 0.5); width = minimumWidth[column] + adjustedExtraWidth; } else width = weightedWidth[column]; setColumnWidth(column, width); usedWidth += width; } // Fill the remaining gap for a clean fit into the available space. int remainingWidth = viewport()->width() - usedWidth; setColumnWidth(visibleColumns.back(), columnWidth(visibleColumns.back()) + remainingWidth); m_widthsDirty = false; } void Playlist::slotAddToUpcoming() { m_collection->setUpcomingPlaylistEnabled(true); m_collection->upcomingPlaylist()->appendItems(selectedItems()); } void Playlist::slotShowRMBMenu(const QPoint &point) { QTreeWidgetItem *item = itemAt(point); int column = columnAt(point.x()); if(!item) return; // Create the RMB menu on demand. if(!m_rmbMenu) { // Probably more of these actions should be ported over to using KActions. m_rmbMenu = new QMenu(this); - m_rmbMenu->addAction(SmallIcon("go-jump-today"), + m_rmbMenu->addAction(QIcon::fromTheme("go-jump-today"), i18n("Add to Play Queue"), this, SLOT(slotAddToUpcoming())); m_rmbMenu->addSeparator(); if(!readOnly()) { m_rmbMenu->addAction( action("edit_cut") ); m_rmbMenu->addAction( action("edit_copy") ); m_rmbMenu->addAction( action("edit_paste") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("removeFromPlaylist") ); } else m_rmbMenu->addAction( action("edit_copy") ); m_rmbEdit = m_rmbMenu->addAction(i18n("Edit")); m_rmbMenu->addAction( action("refresh") ); m_rmbMenu->addAction( action("removeItem") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( action("guessTag") ); m_rmbMenu->addAction( action("renameFile") ); m_rmbMenu->addAction( action("coverManager") ); m_rmbMenu->addSeparator(); m_rmbMenu->addAction( - SmallIcon("folder-new"), i18n("Create Playlist From Selected Items..."), this, SLOT(slotCreateGroup())); + QIcon::fromTheme("folder-new"), + i18n("Create Playlist From Selected Items..."), + this, SLOT(slotCreateGroup())); } // Ignore any columns added by subclasses. const int adjColumn = column - columnOffset(); bool showEdit = (adjColumn == PlaylistItem::TrackColumn) || (adjColumn == PlaylistItem::ArtistColumn) || (adjColumn == PlaylistItem::AlbumColumn) || (adjColumn == PlaylistItem::TrackNumberColumn) || (adjColumn == PlaylistItem::GenreColumn) || (adjColumn == PlaylistItem::YearColumn); if(showEdit) { m_rmbEdit->setText(i18n("Edit '%1'", item->text(column))); m_rmbEdit->disconnect(this); connect(m_rmbEdit, &QAction::triggered, this, [this, item, column]() { this->editItem(item, column); }); } m_rmbEdit->setVisible(showEdit); // Disable edit menu if only one file is selected, and it's read-only FileHandle file = static_cast(item)->file(); m_rmbEdit->setEnabled(file.fileInfo().isWritable() || selectedItems().count() > 1); // View cover is based on if there is a cover to see. We should only have // the remove cover option if the cover is in our database (and not directly // embedded in the file, for instance). action("viewCover")->setEnabled(file.coverInfo()->hasCover()); action("removeCover")->setEnabled(file.coverInfo()->coverId() != CoverManager::NoMatch); m_rmbMenu->popup(mapToGlobal(point)); } bool Playlist::editTag(PlaylistItem *item, const QString &text, int column) { Tag *newTag = TagTransactionManager::duplicateTag(item->file().tag()); switch(column - columnOffset()) { case PlaylistItem::TrackColumn: newTag->setTitle(text); break; case PlaylistItem::ArtistColumn: newTag->setArtist(text); break; case PlaylistItem::AlbumColumn: newTag->setAlbum(text); break; case PlaylistItem::TrackNumberColumn: { bool ok; int value = text.toInt(&ok); if(ok) newTag->setTrack(value); break; } case PlaylistItem::GenreColumn: newTag->setGenre(text); break; case PlaylistItem::YearColumn: { bool ok; int value = text.toInt(&ok); if(ok) newTag->setYear(value); break; } } TagTransactionManager::instance()->changeTagOnItem(item, newTag); return true; } void Playlist::slotInlineEditDone(QTreeWidgetItem *item, int column) { // The column we get is as passed from QTreeWidget so it does not need // adjustment to get the right text from the QTreeWidgetItem QString text = item->text(column); const PlaylistItemList l = selectedItems(); // See if any of the files have a tag different from the input. const int adjColumn = column - columnOffset(); bool changed = std::any_of(l.cbegin(), l.cend(), [text, adjColumn] (const PlaylistItem *item) { return item->text(adjColumn) != text; } ); if(!changed || (l.count() > 1 && KMessageBox::warningContinueCancel( 0, i18n("This will edit multiple files. Are you sure?"), QString(), KGuiItem(i18n("Edit")), KStandardGuiItem::cancel(), "DontWarnMultipleTags") == KMessageBox::Cancel)) { return; } for(auto &item : l) { editTag(item, text, column); } TagTransactionManager::instance()->commit(); CollectionList::instance()->playlistItemsChanged(); playlistItemsChanged(); } void Playlist::slotColumnOrderChanged(int, int from, int to) { if(from == 0 || to == 0) { updatePlaying(); m_leftColumn = header()->sectionPosition(0); } SharedSettings::instance()->setColumnOrder(this); } void Playlist::slotToggleColumnVisible(QAction *action) { int column = action->data().toInt(); if(isColumnHidden(column)) { int fileNameColumn = PlaylistItem::FileNameColumn + columnOffset(); int fullPathColumn = PlaylistItem::FullPathColumn + columnOffset(); if(column == fileNameColumn && !isColumnHidden(fullPathColumn)) { hideColumn(fullPathColumn, false); SharedSettings::instance()->toggleColumnVisible(fullPathColumn); } if(column == fullPathColumn && !isColumnHidden(fileNameColumn)) { hideColumn(fileNameColumn, false); SharedSettings::instance()->toggleColumnVisible(fileNameColumn); } } if(!isColumnHidden(column)) hideColumn(column); else showColumn(column); if(column >= columnOffset()) { SharedSettings::instance()->toggleColumnVisible(column - columnOffset()); } } void Playlist::slotCreateGroup() { QString name = m_collection->playlistNameDialog(i18n("Create New Playlist")); if(!name.isEmpty()) new Playlist(m_collection, selectedItems(), name); } void Playlist::notifyUserColumnWidthModeChanged() { KMessageBox::information(this, i18n("Manual column widths have been enabled. You can " "switch back to automatic column sizes in the view " "menu."), i18n("Manual Column Widths Enabled"), "ShowManualColumnWidthInformation"); } void Playlist::columnResized(int column, int, int newSize) { m_widthsDirty = true; m_columnFixedWidths[column] = newSize; } void Playlist::slotInlineCompletionModeChanged(KCompletion::CompletionMode mode) { SharedSettings::instance()->setInlineCompletionMode(mode); } void Playlist::slotPlayCurrent() { QTreeWidgetItemIterator it(this, QTreeWidgetItemIterator::Selected); PlaylistItem *next = static_cast(*it); TrackSequenceManager::instance()->setNextItem(next); action("forward")->trigger(); } //////////////////////////////////////////////////////////////////////////////// // helper functions //////////////////////////////////////////////////////////////////////////////// QDataStream &operator<<(QDataStream &s, const Playlist &p) { s << p.name(); s << p.fileName(); s << p.files(); return s; } QDataStream &operator>>(QDataStream &s, Playlist &p) { p.read(s); return s; } bool processEvents() { static QTime time = QTime::currentTime(); if(time.elapsed() > 100) { time.restart(); qApp->processEvents(); return true; } return false; } // vim: set et sw=4 tw=0 sta: diff --git a/playlistbox.cpp b/playlistbox.cpp index cc3cbb6a..cc07fdc2 100644 --- a/playlistbox.cpp +++ b/playlistbox.cpp @@ -1,774 +1,768 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * * 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 "playlistbox.h" -#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "playlist.h" #include "collectionlist.h" #include "dynamicplaylist.h" #include "upcomingplaylist.h" #include "historyplaylist.h" #include "viewmode.h" #include "searchplaylist.h" #include "treeviewitemplaylist.h" #include "actioncollection.h" #include "cache.h" #include "tracksequencemanager.h" #include "tagtransactionmanager.h" #include "playermanager.h" #include "dbuscollectionproxy.h" #include "juk_debug.h" using namespace ActionCollection; //////////////////////////////////////////////////////////////////////////////// // PlaylistBox public methods //////////////////////////////////////////////////////////////////////////////// PlaylistBox::PlaylistBox(PlayerManager *player, QWidget *parent, QStackedWidget *playlistStack) : QTreeWidget(parent), PlaylistCollection(player, playlistStack), m_viewModeIndex(0), m_hasSelection(false), m_doingMultiSelect(false), m_dropItem(0), m_showTimer(0) { readConfig(); setHeaderLabel("Playlists"); setRootIsDecorated(false); setContextMenuPolicy(Qt::CustomContextMenu); setDropIndicatorShown(true); header()->blockSignals(true); header()->hide(); header()->blockSignals(false); sortByColumn(0); viewport()->setAcceptDrops(true); setDropIndicatorShown(true); setSelectionMode(QAbstractItemView::ExtendedSelection); m_contextMenu = new QMenu(this); m_contextMenu->addAction( action("file_new") ); m_contextMenu->addAction( action("renamePlaylist") ); m_contextMenu->addAction( action("editSearch") ); m_contextMenu->addAction( action("duplicatePlaylist") ); m_contextMenu->addAction( action("reloadPlaylist") ); m_contextMenu->addAction( action("deleteItemPlaylist") ); m_contextMenu->addAction( action("file_save") ); m_contextMenu->addAction( action("file_save_as") ); m_contextMenu->addSeparator(); // add the view modes stuff KSelectAction *viewModeAction = new KSelectAction( QIcon::fromTheme(QStringLiteral("view-choose")), i18n("View Modes"), ActionCollection::actions()); ActionCollection::actions()->addAction("viewModeMenu", viewModeAction); ViewMode* viewmode = new ViewMode(this); m_viewModes.append(viewmode); viewModeAction->addAction(QIcon::fromTheme(QStringLiteral("view-list-details")), viewmode->name()); CompactViewMode* compactviewmode = new CompactViewMode(this); m_viewModes.append(compactviewmode); viewModeAction->addAction(QIcon::fromTheme(QStringLiteral("view-list-text")), compactviewmode->name()); // TODO: Fix the broken tree view mode #if 0 TreeViewMode* treeviewmode = new TreeViewMode(this); m_viewModes.append(treeviewmode); viewModeAction->addAction(QIcon::fromTheme(QStringLiteral("view-list-tree")), treeviewmode->name()); #endif CollectionList::initialize(this); viewModeAction->setCurrentItem(m_viewModeIndex); m_viewModes[m_viewModeIndex]->setShown(true); TrackSequenceManager::instance()->setCurrentPlaylist(CollectionList::instance()); raise(CollectionList::instance()); m_contextMenu->addAction( viewModeAction ); connect(viewModeAction, SIGNAL(triggered(int)), this, SLOT(slotSetViewMode(int))); connect(this, SIGNAL(itemSelectionChanged()), this, SLOT(slotPlaylistChanged())); connect(this, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotDoubleClicked(QTreeWidgetItem*))); connect(this, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(slotShowContextMenu(QPoint))); TagTransactionManager *tagManager = TagTransactionManager::instance(); connect(tagManager, SIGNAL(signalAboutToModifyTags()), SLOT(slotFreezePlaylists())); connect(tagManager, SIGNAL(signalDoneModifyingTags()), SLOT(slotUnfreezePlaylists())); setupUpcomingPlaylist(); connect(CollectionList::instance(), SIGNAL(signalNewTag(QString,uint)), this, SLOT(slotAddItem(QString,uint))); connect(CollectionList::instance(), SIGNAL(signalRemovedTag(QString,uint)), this, SLOT(slotRemoveItem(QString,uint))); connect(CollectionList::instance(), SIGNAL(cachedItemsLoaded()), this, SLOT(slotLoadCachedPlaylists())); m_savePlaylistTimer = 0; KToggleAction *historyAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("view-history")), i18n("Show &History"), ActionCollection::actions()); ActionCollection::actions()->addAction("showHistory", historyAction); connect(historyAction, SIGNAL(triggered(bool)), this, SLOT(slotSetHistoryPlaylistEnabled(bool))); m_showTimer = new QTimer(this); connect(m_showTimer, SIGNAL(timeout()), SLOT(slotShowDropTarget())); // hook up to the D-Bus (void) new DBusCollectionProxy(this, this); } PlaylistBox::~PlaylistBox() { PlaylistList l; CollectionList *collection = CollectionList::instance(); for(QTreeWidgetItemIterator it(topLevelItem(0)); *it; ++it) { Item *item = static_cast(*it); if(item->playlist() && item->playlist() != collection) l.append(item->playlist()); } Cache::savePlaylists(l); saveConfig(); } void PlaylistBox::raise(Playlist *playlist) { if(!playlist) return; Item *i = m_playlistDict.value(playlist, 0); if(i) { clearSelection(); setCurrentItem(i); setSingleItem(i); scrollToItem(currentItem()); } else PlaylistCollection::raise(playlist); slotPlaylistChanged(); } void PlaylistBox::duplicate() { Item *item = static_cast(currentItem()); if(!item || !item->playlist()) return; QString name = playlistNameDialog(i18nc("verb, copy the playlist", "Duplicate"), item->text(0)); if(name.isNull()) return; Playlist *p = new Playlist(this, name); p->createItems(item->playlist()->items()); } void PlaylistBox::scanFolders() { qCDebug(JUK_LOG) << "Starting folder scan"; QTime stopwatch; stopwatch.start(); PlaylistCollection::scanFolders(); qCDebug(JUK_LOG) << "Folder scan complete, took" << stopwatch.elapsed() << "ms"; qCDebug(JUK_LOG) << "Startup complete!"; emit startupComplete(); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox public slots //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::paste() { // TODO: Reimplement } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox protected methods //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::slotFreezePlaylists() { setDynamicListsFrozen(true); } void PlaylistBox::slotUnfreezePlaylists() { setDynamicListsFrozen(false); } void PlaylistBox::slotPlaylistDataChanged() { if(m_savePlaylistTimer) m_savePlaylistTimer->start(); // Restarts the timer if it's already running. } void PlaylistBox::slotSetHistoryPlaylistEnabled(bool enable) { setHistoryPlaylistEnabled(enable); } void PlaylistBox::setupPlaylist(Playlist *playlist, const QString &iconName) { setupPlaylist(playlist, iconName, 0); } void PlaylistBox::setupPlaylist(Playlist *playlist, const QString &iconName, Item *parentItem) { connect(playlist, SIGNAL(signalPlaylistItemsDropped(Playlist*)), SLOT(slotPlaylistItemsDropped(Playlist*))); PlaylistCollection::setupPlaylist(playlist, iconName); if(parentItem) new Item(parentItem, iconName, playlist->name(), playlist); else new Item(this, iconName, playlist->name(), playlist); } void PlaylistBox::removePlaylist(Playlist *playlist) { // Could be false if setup() wasn't run yet. if(m_playlistDict.contains(playlist)) { removeNameFromDict(m_playlistDict[playlist]->text(0)); delete m_playlistDict[playlist]; // Delete the Item* } removeFileFromDict(playlist->fileName()); m_playlistDict.remove(playlist); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox private methods //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::readConfig() { KConfigGroup config(KSharedConfig::openConfig(), "PlaylistBox"); m_viewModeIndex = config.readEntry("ViewMode", 0); // TODO Restore ability to use Tree View once fixed. if(m_viewModeIndex == 2) { m_viewModeIndex = 0; } } void PlaylistBox::saveConfig() { KConfigGroup config(KSharedConfig::openConfig(), "PlaylistBox"); config.writeEntry("ViewMode", action("viewModeMenu")->currentItem()); KSharedConfig::openConfig()->sync(); } void PlaylistBox::remove() { ItemList items = selectedBoxItems(); if(items.isEmpty()) return; QStringList files; QStringList names; foreach(Item *item, items) { if(item && item->playlist()) { if (!item->playlist()->fileName().isEmpty() && QFileInfo(item->playlist()->fileName()).exists()) { files.append(item->playlist()->fileName()); } names.append(item->playlist()->name()); } } if(!files.isEmpty()) { int remove = KMessageBox::warningYesNoCancelList( this, i18n("Do you want to delete these files from the disk as well?"), files, QString(), KStandardGuiItem::del(), KGuiItem(i18n("Keep"))); if(remove == KMessageBox::Yes) { QStringList couldNotDelete; for(QStringList::ConstIterator it = files.constBegin(); it != files.constEnd(); ++it) { if(!QFile::remove(*it)) couldNotDelete.append(*it); } if(!couldNotDelete.isEmpty()) KMessageBox::errorList(this, i18n("Could not delete these files."), couldNotDelete); } else if(remove == KMessageBox::Cancel) return; } else if(items.count() > 1 || items.front()->playlist() != upcomingPlaylist()) { if(KMessageBox::warningContinueCancelList(this, i18n("Are you sure you want to remove these " "playlists from your collection?"), names, i18n("Remove Items?"), KGuiItem(i18n("&Remove"), "user-trash")) == KMessageBox::Cancel) { return; } } PlaylistList removeQueue; for(ItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { if(*it != Item::collectionItem() && (*it)->playlist() && (!(*it)->playlist()->readOnly())) { removeQueue.append((*it)->playlist()); } } // FIXME removing items /*if(items.back()->nextSibling() && static_cast(items.back()->nextSibling())->playlist()) setSingleItem(items.back()->nextSibling()); else { Item *i = static_cast(items.front()->itemAbove()); while(i && !i->playlist()) i = static_cast(i->itemAbove()); if(!i) i = Item::collectionItem(); setSingleItem(i); }*/ for(PlaylistList::ConstIterator it = removeQueue.constBegin(); it != removeQueue.constEnd(); ++it) { if(*it != upcomingPlaylist()) delete *it; else { action("showUpcoming")->setChecked(false); setUpcomingPlaylistEnabled(false); } } } void PlaylistBox::setDynamicListsFrozen(bool frozen) { for(QList::Iterator it = m_viewModes.begin(); it != m_viewModes.end(); ++it) { (*it)->setDynamicListsFrozen(frozen); } } void PlaylistBox::slotSavePlaylists() { qCDebug(JUK_LOG) << "Auto-saving playlists.\n"; PlaylistList l; CollectionList *collection = CollectionList::instance(); for(QTreeWidgetItemIterator it(topLevelItem(0)); *it; ++it) { Item *item = static_cast(*it); if(item->playlist() && item->playlist() != collection) l.append(item->playlist()); } Cache::savePlaylists(l); } void PlaylistBox::slotShowDropTarget() { if(!m_dropItem) { qCCritical(JUK_LOG) << "Trying to show the playlist of a null item!\n"; return; } raise(m_dropItem->playlist()); } void PlaylistBox::slotAddItem(const QString &tag, unsigned column) { for(QList::Iterator it = m_viewModes.begin(); it != m_viewModes.end(); ++it) (*it)->addItems(QStringList(tag), column); } void PlaylistBox::slotRemoveItem(const QString &tag, unsigned column) { for(QList::Iterator it = m_viewModes.begin(); it != m_viewModes.end(); ++it) (*it)->removeItem(tag, column); } void PlaylistBox::mousePressEvent(QMouseEvent *e) { if(e->button() == Qt::LeftButton) m_doingMultiSelect = true; QTreeWidget::mousePressEvent(e); } void PlaylistBox::mouseReleaseEvent(QMouseEvent *e) { if(e->button() == Qt::LeftButton) { m_doingMultiSelect = false; slotPlaylistChanged(); } QTreeWidget::mouseReleaseEvent(e); } void PlaylistBox::keyPressEvent(QKeyEvent *e) { if((e->key() == Qt::Key_Up || e->key() == Qt::Key_Down) && e->modifiers() == Qt::ShiftModifier) m_doingMultiSelect = true; QTreeWidget::keyPressEvent(e); } void PlaylistBox::keyReleaseEvent(QKeyEvent *e) { if(m_doingMultiSelect && e->key() == Qt::Key_Shift) { m_doingMultiSelect = false; slotPlaylistChanged(); } QTreeWidget::keyReleaseEvent(e); } PlaylistBox::ItemList PlaylistBox::selectedBoxItems() const { ItemList l; for(QTreeWidgetItemIterator it(const_cast(this), QTreeWidgetItemIterator::Selected); *it; ++it) l.append(static_cast(*it)); return l; } void PlaylistBox::setSingleItem(QTreeWidgetItem *item) { setSelectionMode(QAbstractItemView::SingleSelection); setCurrentItem(item); setSelectionMode(QAbstractItemView::ExtendedSelection); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox private slots //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::slotPlaylistChanged() { // Don't update while the mouse is pressed down. if(m_doingMultiSelect) return; ItemList items = selectedBoxItems(); m_hasSelection = !items.isEmpty(); bool allowReload = false; PlaylistList playlists; for(ItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { Playlist *p = (*it)->playlist(); if(p) { if(p->canReload()) allowReload = true; playlists.append(p); } } bool singlePlaylist = playlists.count() == 1; if(playlists.isEmpty() || (singlePlaylist && (playlists.front() == CollectionList::instance() || playlists.front()->readOnly()))) { action("file_save")->setEnabled(false); action("file_save_as")->setEnabled(false); action("renamePlaylist")->setEnabled(false); action("deleteItemPlaylist")->setEnabled(false); } else { action("file_save")->setEnabled(true); action("file_save_as")->setEnabled(true); action("renamePlaylist")->setEnabled(playlists.count() == 1); action("deleteItemPlaylist")->setEnabled(true); } action("reloadPlaylist")->setEnabled(allowReload); action("duplicatePlaylist")->setEnabled(!playlists.isEmpty()); action("editSearch")->setEnabled(singlePlaylist && playlists.front()->searchIsEditable()); if(singlePlaylist) { PlaylistCollection::raise(playlists.front()); if(playlists.front() == upcomingPlaylist()) action("deleteItemPlaylist")->setText(i18n("Hid&e")); else action("deleteItemPlaylist")->setText(i18n("R&emove")); } else if(!playlists.isEmpty()) createDynamicPlaylist(playlists); } void PlaylistBox::slotDoubleClicked(QTreeWidgetItem *item) { if(!item) return; TrackSequenceManager *manager = TrackSequenceManager::instance(); Item *playlistItem = static_cast(item); manager->setCurrentPlaylist(playlistItem->playlist()); manager->setCurrent(0); // Reset playback PlaylistItem *next = manager->nextItem(); // Allow manager to choose if(next) { emit startFilePlayback(next->file()); playlistItem->playlist()->setPlaying(next); } else action("stop")->trigger(); } void PlaylistBox::slotShowContextMenu(const QPoint &point) { m_contextMenu->popup(mapToGlobal(point)); } void PlaylistBox::slotPlaylistItemsDropped(Playlist *p) { raise(p); } void PlaylistBox::slotSetViewMode(int index) { if(index == m_viewModeIndex) return; viewMode()->setShown(false); m_viewModeIndex = index; viewMode()->setShown(true); } void PlaylistBox::setupItem(Item *item) { m_playlistDict.insert(item->playlist(), item); viewMode()->queueRefresh(); } void PlaylistBox::setupUpcomingPlaylist() { KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); bool enable = config.readEntry("showUpcoming", false); setUpcomingPlaylistEnabled(enable); action("showUpcoming")->setChecked(enable); } void PlaylistBox::slotLoadCachedPlaylists() { qCDebug(JUK_LOG) << "Loading cached playlists."; QTime stopwatch; stopwatch.start(); Cache::loadPlaylists(this); qCDebug(JUK_LOG) << "Cached playlists loaded, took" << stopwatch.elapsed() << "ms"; // Auto-save playlists after they change. m_savePlaylistTimer = new QTimer(this); m_savePlaylistTimer->setInterval(3000); // 3 seconds with no change? -> commit m_savePlaylistTimer->setSingleShot(true); connect(m_savePlaylistTimer, SIGNAL(timeout()), SLOT(slotSavePlaylists())); clearSelection(); setCurrentItem(m_playlistDict[CollectionList::instance()]); QTimer::singleShot(0, CollectionList::instance(), SLOT(slotCheckCache())); QTimer::singleShot(0, object(), SLOT(slotScanFolders())); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox::Item protected methods //////////////////////////////////////////////////////////////////////////////// PlaylistBox::Item *PlaylistBox::Item::m_collectionItem = 0; PlaylistBox::Item::Item(PlaylistBox *listBox, const QString &icon, const QString &text, Playlist *l) : QObject(listBox), QTreeWidgetItem(listBox, QStringList(text)), PlaylistObserver(l), m_playlist(l), m_text(text), m_iconName(icon), m_sortedFirst(false) { init(); } PlaylistBox::Item::Item(Item *parent, const QString &icon, const QString &text, Playlist *l) : QObject(parent->listView()), QTreeWidgetItem(parent, QStringList(text)), PlaylistObserver(l), m_playlist(l), m_text(text), m_iconName(icon), m_sortedFirst(false) { init(); } PlaylistBox::Item::~Item() { } int PlaylistBox::Item::compare(QTreeWidgetItem *i, int col, bool) const { Item *otherItem = static_cast(i); PlaylistBox *playlistBox = static_cast(treeWidget()); if(m_playlist == playlistBox->upcomingPlaylist() && otherItem->m_playlist != CollectionList::instance()) return -1; if(otherItem->m_playlist == playlistBox->upcomingPlaylist() && m_playlist != CollectionList::instance()) return 1; if(m_sortedFirst && !otherItem->m_sortedFirst) return -1; else if(otherItem->m_sortedFirst && !m_sortedFirst) return 1; return text(col).toLower().localeAwareCompare(i->text(col).toLower()); } // FIXME paintcell /*void PlaylistBox::Item::paintCell(QPainter *painter, const QColorGroup &colorGroup, int column, int width, int align) { PlaylistBox *playlistBox = static_cast(listView()); playlistBox->viewMode()->paintCell(this, painter, colorGroup, column, width, align); }*/ void PlaylistBox::Item::setText(int column, const QString &text) { m_text = text; QTreeWidgetItem::setText(column, text); } void PlaylistBox::Item::setup() { listView()->viewMode()->setupItem(this); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox::Item protected slots //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::Item::slotSetName(const QString &name) { if(listView()) { setText(0, name); setSelected(true); treeWidget()->sortItems(0, Qt::AscendingOrder); treeWidget()->scrollToItem(treeWidget()->currentItem()); //FIXME viewmode //listView()->viewMode()->queueRefresh(); } } void PlaylistBox::Item::playingItemHasChanged() { } void PlaylistBox::Item::playlistItemDataHasChanged() { // This avoids spuriously re-saving all playlists just because play queue // changes. if(m_playlist != listView()->upcomingPlaylist()) listView()->slotPlaylistDataChanged(); } //////////////////////////////////////////////////////////////////////////////// // PlaylistBox::Item private methods //////////////////////////////////////////////////////////////////////////////// void PlaylistBox::Item::init() { PlaylistBox *list = listView(); list->setupItem(this); - const auto iconLoader = KIconLoader::global(); - const KIconLoader::Group iconGroup = - list->viewModeIndex() == 0 - ? KIconLoader::Desktop - : KIconLoader::Toolbar; - setIcon(0, iconLoader->loadIcon(m_iconName, iconGroup)); + setIcon(0, QIcon::fromTheme(m_iconName)); list->addNameToDict(m_text); if(m_playlist) { connect(m_playlist, SIGNAL(signalNameChanged(QString)), this, SLOT(slotSetName(QString))); connect(m_playlist, SIGNAL(signalEnableDirWatch(bool)), list->object(), SLOT(slotEnableDirWatch(bool))); } if(m_playlist == CollectionList::instance()) { m_sortedFirst = true; m_collectionItem = this; list->viewMode()->setupDynamicPlaylists(); } if(m_playlist == list->historyPlaylist() || m_playlist == list->upcomingPlaylist()) m_sortedFirst = true; } // vim: set et sw=4 tw=0 sta: diff --git a/playlistcollection.cpp b/playlistcollection.cpp index e4b6ac2f..73e925af 100644 --- a/playlistcollection.cpp +++ b/playlistcollection.cpp @@ -1,1019 +1,1019 @@ /** * 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 "playlistcollection.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "collectionlist.h" #include "actioncollection.h" #include "advancedsearchdialog.h" #include "coverinfo.h" #include "searchplaylist.h" #include "folderplaylist.h" #include "historyplaylist.h" #include "upcomingplaylist.h" #include "directorylist.h" #include "mediafiles.h" #include "playermanager.h" #include "tracksequencemanager.h" #include "juk.h" //Laurent: readd it //#include "collectionadaptor.h" using namespace ActionCollection; //////////////////////////////////////////////////////////////////////////////// // static methods //////////////////////////////////////////////////////////////////////////////// PlaylistCollection *PlaylistCollection::m_instance = 0; // Returns all folders in input list with their canonical path, if available, or // unchanged if not. static QStringList canonicalizeFolderPaths(const QStringList &folders) { QStringList result; foreach(const QString &folder, folders) { QString canonicalFolder = QDir(folder).canonicalPath(); result << (!canonicalFolder.isEmpty() ? canonicalFolder : folder); } return result; } //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// PlaylistCollection::PlaylistCollection(PlayerManager *player, QStackedWidget *playlistStack) : m_playlistStack(playlistStack), m_historyPlaylist(0), m_upcomingPlaylist(0), m_playerManager(player), m_importPlaylists(true), m_searchEnabled(true), m_playing(false), m_showMorePlaylist(0), m_belowShowMorePlaylist(0), m_dynamicPlaylist(0), m_belowDistraction(0), m_distraction(0) { //new CollectionAdaptor( this ); //QDBus::sessionBus().registerObject("/Collection",this ); m_instance = this; m_actionHandler = new ActionHandler(this); // KDirLister's auto error handling seems to crash JuK during startup in // readConfig(). m_dirLister.setAutoErrorHandlingEnabled(false, playlistStack); readConfig(); } PlaylistCollection::~PlaylistCollection() { saveConfig(); CollectionList::instance()->saveItemsToCache(); delete m_actionHandler; Playlist::setShuttingDown(); } QString PlaylistCollection::name() const { return currentPlaylist()->name(); } FileHandle PlaylistCollection::currentFile() const { return currentPlaylist()->currentFile(); } int PlaylistCollection::count() const { return currentPlaylist()->count(); } int PlaylistCollection::time() const { return currentPlaylist()->time(); } void PlaylistCollection::playFirst() { m_playing = true; currentPlaylist()->playFirst(); currentPlayingItemChanged(); } void PlaylistCollection::playNextAlbum() { m_playing = true; currentPlaylist()->playNextAlbum(); currentPlayingItemChanged(); } void PlaylistCollection::playPrevious() { m_playing = true; currentPlaylist()->playPrevious(); currentPlayingItemChanged(); } void PlaylistCollection::playNext() { m_playing = true; currentPlaylist()->playNext(); currentPlayingItemChanged(); } void PlaylistCollection::stop() { m_playing = false; currentPlaylist()->stop(); playlistItemsChanged(); } bool PlaylistCollection::playing() const { return m_playing; } QStringList PlaylistCollection::playlists() const { QStringList l; //(or qFindChildren() if you need MSVC 6 compatibility) const QList childList = m_playlistStack->findChildren("Playlist"); foreach(Playlist *p, childList) { l.append(p->name()); } return l; } void PlaylistCollection::createPlaylist(const QString &name) { raise(new Playlist(this, name)); } void PlaylistCollection::createDynamicPlaylist(const PlaylistList &playlists) { if(m_dynamicPlaylist) m_dynamicPlaylist->setPlaylists(playlists); else { m_dynamicPlaylist = new DynamicPlaylist(playlists, this, i18n("Dynamic List"), "audio-midi", false, true); PlaylistCollection::setupPlaylist(m_dynamicPlaylist, QString()); } PlaylistCollection::raise(m_dynamicPlaylist); } void PlaylistCollection::showMore(const QString &artist, const QString &album) { PlaylistList playlists; PlaylistSearch::ComponentList components; if(currentPlaylist() != CollectionList::instance() && currentPlaylist() != m_showMorePlaylist) { playlists.append(currentPlaylist()); } playlists.append(CollectionList::instance()); if(!artist.isNull()) { // Just setting off the artist stuff in its own block. ColumnList columns; columns.append(PlaylistItem::ArtistColumn); PlaylistSearch::Component c(artist, false, columns, PlaylistSearch::Component::Exact); components.append(c); } if(!album.isNull()) { ColumnList columns; columns.append(PlaylistItem::AlbumColumn); PlaylistSearch::Component c(album, false, columns, PlaylistSearch::Component::Exact); components.append(c); } PlaylistSearch search(playlists, components, PlaylistSearch::MatchAll); if(m_showMorePlaylist) m_showMorePlaylist->setPlaylistSearch(search); else m_showMorePlaylist = new SearchPlaylist(this, search, i18n("Now Playing"), false, true); // The call to raise() below will end up clearing m_belowShowMorePlaylist, // so cache the value we want it to have now. Playlist *belowShowMore = visiblePlaylist(); PlaylistCollection::setupPlaylist(m_showMorePlaylist, QString()); PlaylistCollection::raise(m_showMorePlaylist); m_belowShowMorePlaylist = belowShowMore; } void PlaylistCollection::removeTrack(const QString &playlist, const QStringList &files) { Playlist *p = playlistByName(playlist); PlaylistItemList itemList; if(!p) return; QStringList::ConstIterator it; for(it = files.begin(); it != files.end(); ++it) { CollectionListItem *item = CollectionList::instance()->lookup(*it); if(item) { PlaylistItem *playlistItem = item->itemForPlaylist(p); if(playlistItem) itemList.append(playlistItem); } } p->clearItems(itemList); } QString PlaylistCollection::playlist() const { return visiblePlaylist() ? visiblePlaylist()->name() : QString(); } QString PlaylistCollection::playingPlaylist() const { return currentPlaylist() && m_playing ? currentPlaylist()->name() : QString(); } void PlaylistCollection::setPlaylist(const QString &playlist) { Playlist *p = playlistByName(playlist); if(p) raise(p); } QStringList PlaylistCollection::playlistTracks(const QString &playlist) const { Playlist *p = playlistByName(playlist); if(p) return p->files(); return QStringList(); } QString PlaylistCollection::trackProperty(const QString &file, const QString &property) const { CollectionList *l = CollectionList::instance(); CollectionListItem *item = l->lookup(file); return item ? item->file().property(property) : QString(); } QPixmap PlaylistCollection::trackCover(const QString &file, const QString &size) const { if(size.toLower() != "small" && size.toLower() != "large") return QPixmap(); CollectionList *l = CollectionList::instance(); CollectionListItem *item = l->lookup(file); if(!item) return QPixmap(); if(size.toLower() == "small") return item->file().coverInfo()->pixmap(CoverInfo::Thumbnail); else return item->file().coverInfo()->pixmap(CoverInfo::FullSize); } void PlaylistCollection::open(const QStringList &l) { QStringList files = l; if(files.isEmpty()) files = MediaFiles::openDialog(JuK::JuKInstance()); if(files.isEmpty()) return; bool justPlaylists = true; for(QStringList::ConstIterator it = files.constBegin(); it != files.constEnd() && justPlaylists; ++it) justPlaylists = !MediaFiles::isPlaylistFile(*it); if(visiblePlaylist() == CollectionList::instance() || justPlaylists || KMessageBox::questionYesNo( JuK::JuKInstance(), i18n("Do you want to add these items to the current list or to the collection list?"), QString(), KGuiItem(i18nc("current playlist", "Current")), KGuiItem(i18n("Collection"))) == KMessageBox::No) { CollectionList::instance()->addFiles(files); } else { visiblePlaylist()->addFiles(files); } playlistItemsChanged(); } void PlaylistCollection::open(const QString &playlist, const QStringList &files) { Playlist *p = playlistByName(playlist); if(p) p->addFiles(files); } void PlaylistCollection::addFolder() { DirectoryList l(m_folderList, m_excludedFolderList, m_importPlaylists, JuK::JuKInstance()); if(l.exec() == QDialog::Accepted) { m_dirLister.blockSignals(true); DirectoryList::Result result = l.dialogResult(); const bool reload = m_importPlaylists != result.addPlaylists; m_importPlaylists = result.addPlaylists; m_excludedFolderList = canonicalizeFolderPaths(result.excludedDirs); foreach(const QString &dir, result.addedDirs) { m_dirLister.openUrl(QUrl::fromLocalFile(dir), KDirLister::Keep); m_folderList.append(dir); } foreach(const QString &dir, result.removedDirs) { m_dirLister.stop(QUrl::fromLocalFile(dir)); m_folderList.removeAll(dir); } if(reload) { open(m_folderList); } else if(!result.addedDirs.isEmpty()) { open(result.addedDirs); } saveConfig(); m_dirLister.blockSignals(false); } } void PlaylistCollection::rename() { QString old = visiblePlaylist()->name(); QString name = playlistNameDialog(i18n("Rename"), old, false); m_playlistNames.remove(old); if(name.isEmpty()) return; visiblePlaylist()->setName(name); } void PlaylistCollection::duplicate() { QString name = playlistNameDialog(i18nc("verb, copy the playlist", "Duplicate"), visiblePlaylist()->name()); if(name.isEmpty()) return; raise(new Playlist(this, visiblePlaylist()->items(), name)); } void PlaylistCollection::save() { visiblePlaylist()->save(); } void PlaylistCollection::saveAs() { visiblePlaylist()->saveAs(); } void PlaylistCollection::reload() { if(visiblePlaylist() == CollectionList::instance()) CollectionList::instance()->addFiles(m_folderList); else visiblePlaylist()->slotReload(); } void PlaylistCollection::editSearch() { SearchPlaylist *p = dynamic_cast(visiblePlaylist()); if(!p) return; auto searchDialog = new AdvancedSearchDialog( p->name(), p->playlistSearch(), JuK::JuKInstance()); QObject::connect(searchDialog, &QDialog::finished, [searchDialog, p](int result) { if (result) { p->setPlaylistSearch(searchDialog->resultSearch()); p->setName(searchDialog->resultPlaylistName()); } searchDialog->deleteLater(); }); searchDialog->exec(); } void PlaylistCollection::removeItems() { visiblePlaylist()->slotRemoveSelectedItems(); } void PlaylistCollection::refreshItems() { visiblePlaylist()->slotRefresh(); } void PlaylistCollection::renameItems() { visiblePlaylist()->slotRenameFile(); } void PlaylistCollection::addCovers(bool fromFile) { visiblePlaylist()->slotAddCover(fromFile); playlistItemsChanged(); } void PlaylistCollection::removeCovers() { visiblePlaylist()->slotRemoveCover(); playlistItemsChanged(); } void PlaylistCollection::viewCovers() { visiblePlaylist()->slotViewCover(); } void PlaylistCollection::showCoverManager() { visiblePlaylist()->slotShowCoverManager(); } PlaylistItemList PlaylistCollection::selectedItems() { return visiblePlaylist()->selectedItems(); } void PlaylistCollection::scanFolders() { CollectionList::instance()->addFiles(m_folderList); if(CollectionList::instance()->count() == 0) addFolder(); enableDirWatch(true); } void PlaylistCollection::createPlaylist() { QString name = playlistNameDialog(); if(!name.isEmpty()) raise(new Playlist(this, name)); } void PlaylistCollection::createSearchPlaylist() { QString name = uniquePlaylistName(i18n("Search Playlist")); auto searchDialog = new AdvancedSearchDialog( name, PlaylistSearch(), JuK::JuKInstance()); QObject::connect(searchDialog, &QDialog::finished, [searchDialog, this](int result) { if (result) { raise(new SearchPlaylist( this, searchDialog->resultSearch(), searchDialog->resultPlaylistName())); } searchDialog->deleteLater(); }); searchDialog->exec(); } void PlaylistCollection::createFolderPlaylist() { QString folder = QFileDialog::getExistingDirectory(); if(folder.isEmpty()) return; QString name = uniquePlaylistName(folder.mid(folder.lastIndexOf('/') + 1)); name = playlistNameDialog(i18n("Create Folder Playlist"), name); if(!name.isEmpty()) raise(new FolderPlaylist(this, folder, name)); } void PlaylistCollection::guessTagFromFile() { visiblePlaylist()->slotGuessTagInfo(TagGuesser::FileName); } void PlaylistCollection::guessTagFromInternet() { visiblePlaylist()->slotGuessTagInfo(TagGuesser::MusicBrainz); } void PlaylistCollection::setSearchEnabled(bool enable) { if(enable == m_searchEnabled) return; m_searchEnabled = enable; visiblePlaylist()->setSearchEnabled(enable); } HistoryPlaylist *PlaylistCollection::historyPlaylist() const { return m_historyPlaylist; } void PlaylistCollection::setHistoryPlaylistEnabled(bool enable) { if((enable && m_historyPlaylist) || (!enable && !m_historyPlaylist)) return; if(enable) { action("showHistory")->setChecked(true); m_historyPlaylist = new HistoryPlaylist(this); m_historyPlaylist->setName(i18n("History")); setupPlaylist(m_historyPlaylist, "view-history"); QObject::connect(m_playerManager, SIGNAL(signalItemChanged(FileHandle)), historyPlaylist(), SLOT(appendProposedItem(FileHandle))); } else { delete m_historyPlaylist; m_historyPlaylist = 0; } } UpcomingPlaylist *PlaylistCollection::upcomingPlaylist() const { return m_upcomingPlaylist; } void PlaylistCollection::setUpcomingPlaylistEnabled(bool enable) { if((enable && m_upcomingPlaylist) || (!enable && !m_upcomingPlaylist)) return; if(enable) { action("showUpcoming")->setChecked(true); if(!m_upcomingPlaylist) m_upcomingPlaylist = new UpcomingPlaylist(this); setupPlaylist(m_upcomingPlaylist, "go-jump-today"); } else { action("showUpcoming")->setChecked(false); bool raiseCollection = visiblePlaylist() == m_upcomingPlaylist; if(raiseCollection) { raise(CollectionList::instance()); } m_upcomingPlaylist->deleteLater(); m_upcomingPlaylist = 0; } } QObject *PlaylistCollection::object() const { return m_actionHandler; } Playlist *PlaylistCollection::currentPlaylist() const { if(m_belowDistraction) return m_belowDistraction; if(m_upcomingPlaylist && m_upcomingPlaylist->active()) return m_upcomingPlaylist; if(Playlist::playingItem()) return Playlist::playingItem()->playlist(); else return visiblePlaylist(); } Playlist *PlaylistCollection::visiblePlaylist() const { return qobject_cast(m_playlistStack->currentWidget()); } void PlaylistCollection::raise(Playlist *playlist) { if(m_showMorePlaylist && currentPlaylist() == m_showMorePlaylist) m_showMorePlaylist->lower(playlist); if(m_dynamicPlaylist && currentPlaylist() == m_dynamicPlaylist) m_dynamicPlaylist->lower(playlist); TrackSequenceManager::instance()->setCurrentPlaylist(playlist); playlist->applySharedSettings(); playlist->setSearchEnabled(m_searchEnabled); m_playlistStack->setCurrentWidget(playlist); clearShowMore(false); playlistItemsChanged(); } void PlaylistCollection::raiseDistraction() { if(m_belowDistraction) return; m_belowDistraction = currentPlaylist(); if(!m_distraction) { m_distraction = new QWidget(m_playlistStack); m_playlistStack->addWidget(m_distraction); } m_playlistStack->setCurrentWidget(m_distraction); } void PlaylistCollection::lowerDistraction() { if(!m_distraction) return; if(m_belowDistraction) m_playlistStack->setCurrentWidget(m_belowDistraction); m_belowDistraction = 0; } //////////////////////////////////////////////////////////////////////////////// // protected methods //////////////////////////////////////////////////////////////////////////////// QStackedWidget *PlaylistCollection::playlistStack() const { return m_playlistStack; } void PlaylistCollection::setupPlaylist(Playlist *playlist, const QString &) { if(!playlist->fileName().isEmpty()) m_playlistFiles.insert(playlist->fileName()); if(!playlist->name().isEmpty()) m_playlistNames.insert(playlist->name()); m_playlistStack->addWidget(playlist); QObject::connect(playlist, SIGNAL(itemSelectionChanged()), object(), SIGNAL(signalSelectedItemsChanged())); } bool PlaylistCollection::importPlaylists() const { return m_importPlaylists; } bool PlaylistCollection::containsPlaylistFile(const QString &file) const { return m_playlistFiles.contains(file); } bool PlaylistCollection::showMoreActive() const { return visiblePlaylist() == m_showMorePlaylist; } void PlaylistCollection::clearShowMore(bool raisePlaylist) { if(!m_showMorePlaylist) return; if(raisePlaylist) { if(m_belowShowMorePlaylist) raise(m_belowShowMorePlaylist); else raise(CollectionList::instance()); } m_belowShowMorePlaylist = 0; } void PlaylistCollection::enableDirWatch(bool enable) { auto collection = CollectionList::instance(); m_dirLister.disconnect(object()); if(enable) { QObject::connect(&m_dirLister, &KDirLister::newItems, object(), [this](const KFileItemList &items) { this->newItems(items); }); QObject::connect(&m_dirLister, &KDirLister::refreshItems, collection, &CollectionList::slotRefreshItems); QObject::connect(&m_dirLister, &KDirLister::itemsDeleted, collection, &CollectionList::slotDeleteItems); } } QString PlaylistCollection::playlistNameDialog(const QString &caption, const QString &suggest, bool forceUnique) const { bool ok; QString name = QInputDialog::getText( m_playlistStack, caption, i18n("Please enter a name for this playlist:"), QLineEdit::Normal, forceUnique ? uniquePlaylistName(suggest) : suggest, &ok); return ok ? uniquePlaylistName(name) : QString(); } QString PlaylistCollection::uniquePlaylistName(const QString &suggest) const { if(suggest.isEmpty()) return uniquePlaylistName(); if(!m_playlistNames.contains(suggest)) return suggest; QString base = suggest; base.remove(QRegExp("\\s\\([0-9]+\\)$")); int count = 1; QString s = QString("%1 (%2)").arg(base).arg(count); while(m_playlistNames.contains(s)) { count++; s = QString("%1 (%2)").arg(base).arg(count); } return s; } void PlaylistCollection::addNameToDict(const QString &name) { m_playlistNames.insert(name); } void PlaylistCollection::addFileToDict(const QString &file) { m_playlistFiles.insert(file); } void PlaylistCollection::removeNameFromDict(const QString &name) { m_playlistNames.remove(name); } void PlaylistCollection::removeFileFromDict(const QString &file) { m_playlistFiles.remove(file); } void PlaylistCollection::dirChanged(const QString &path) { QString canonicalPath = QDir(path).canonicalPath(); if(canonicalPath.isEmpty()) return; foreach(const QString &excludedFolder, m_excludedFolderList) { if(canonicalPath.startsWith(excludedFolder)) return; } CollectionList::instance()->addFiles(QStringList(canonicalPath)); } Playlist *PlaylistCollection::playlistByName(const QString &name) const { for(int i = 0; i < m_playlistStack->count(); ++i) { Playlist *p = qobject_cast(m_playlistStack->widget(i)); if(p && p->name() == name) return p; } return 0; } void PlaylistCollection::newItems(const KFileItemList &list) const { // Make fast-path for the normal case if(m_excludedFolderList.isEmpty()) { CollectionList::instance()->slotNewItems(list); return; } // Slow case: Directories to exclude from consideration KFileItemList filteredList(list); foreach(const QString &excludedFolder, m_excludedFolderList) { QMutableListIterator filteredListIterator(filteredList); while(filteredListIterator.hasNext()) { const KFileItem fileItem = filteredListIterator.next(); if(fileItem.url().path().startsWith(excludedFolder)) filteredListIterator.remove(); } } CollectionList::instance()->slotNewItems(filteredList); } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// void PlaylistCollection::readConfig() { KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); m_importPlaylists = config.readEntry("ImportPlaylists", true); m_folderList = config.readEntry("DirectoryList", QStringList()); m_excludedFolderList = canonicalizeFolderPaths( config.readEntry("ExcludeDirectoryList", QStringList())); for(const auto &folder : m_folderList) { m_dirLister.openUrl(QUrl::fromUserInput(folder), KDirLister::Keep); } } void PlaylistCollection::saveConfig() { KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); config.writeEntry("ImportPlaylists", m_importPlaylists); config.writeEntry("showUpcoming", action("showUpcoming")->isChecked()); config.writePathEntry("DirectoryList", m_folderList); config.writePathEntry("ExcludeDirectoryList", m_excludedFolderList); config.sync(); } //////////////////////////////////////////////////////////////////////////////// // ActionHandler implementation //////////////////////////////////////////////////////////////////////////////// PlaylistCollection::ActionHandler::ActionHandler(PlaylistCollection *collection) : QObject(nullptr), m_collection(collection) { setObjectName( QLatin1String("ActionHandler" )); KActionMenu *menu; // "New" menu menu = new KActionMenu(QIcon::fromTheme(QStringLiteral("document-new")), i18nc("new playlist", "&New"), actions()); actions()->addAction("file_new", menu); menu->addAction(createAction(i18n("&Empty Playlist..."), SLOT(slotCreatePlaylist()), "newPlaylist", "window-new", QKeySequence(Qt::CTRL + Qt::Key_N))); menu->addAction(createAction(i18n("&Search Playlist..."), SLOT(slotCreateSearchPlaylist()), "newSearchPlaylist", "edit-find", QKeySequence(Qt::CTRL + Qt::Key_F))); menu->addAction(createAction(i18n("Playlist From &Folder..."), SLOT(slotCreateFolderPlaylist()), "newDirectoryPlaylist", "document-open", QKeySequence(Qt::CTRL + Qt::Key_D))); // Guess tag info menu #if HAVE_TUNEPIMP menu = new KActionMenu(i18n("&Guess Tag Information"), actions()); actions()->addAction("guessTag", menu); - /* menu->setIcon(SmallIcon("wizard")); */ + menu->setIcon(QIcon::fromTheme("wizard")); menu->addAction(createAction(i18n("From &File Name"), SLOT(slotGuessTagFromFile()), "guessTagFile", "document-import", QKeySequence(Qt::CTRL + Qt::Key_G))); menu->addAction(createAction(i18n("From &Internet"), SLOT(slotGuessTagFromInternet()), "guessTagInternet", "network-server", QKeySequence(Qt::CTRL + Qt::Key_I))); #else createAction(i18n("Guess Tag Information From &File Name"), SLOT(slotGuessTagFromFile()), "guessTag", "document-import", QKeySequence(Qt::CTRL + Qt::Key_G)); #endif createAction(i18n("Play First Track"),SLOT(slotPlayFirst()), "playFirst"); createAction(i18n("Play Next Album"), SLOT(slotPlayNextAlbum()), "forwardAlbum", "go-down-search"); KStandardAction::open(this, SLOT(slotOpen()), actions()); KStandardAction::save(this, SLOT(slotSave()), actions()); KStandardAction::saveAs(this, SLOT(slotSaveAs()), actions()); createAction(i18n("Manage &Folders..."), SLOT(slotManageFolders()), "openDirectory", "folder-new"); createAction(i18n("&Rename..."), SLOT(slotRename()), "renamePlaylist", "edit-rename"); createAction(i18nc("verb, copy the playlist", "D&uplicate..."), SLOT(slotDuplicate()), "duplicatePlaylist", "edit-copy"); createAction(i18n("R&emove"), SLOT(slotRemove()), "deleteItemPlaylist", "user-trash"); createAction(i18n("Reload"), SLOT(slotReload()), "reloadPlaylist", "view-refresh"); createAction(i18n("Edit Search..."), SLOT(slotEditSearch()), "editSearch"); createAction(i18n("&Delete"), SLOT(slotRemoveItems()), "removeItem", "edit-delete"); createAction(i18n("Refresh"), SLOT(slotRefreshItems()), "refresh", "view-refresh"); createAction(i18n("&Rename File"), SLOT(slotRenameItems()), "renameFile", "document-save-as", QKeySequence(Qt::CTRL + Qt::Key_R)); menu = new KActionMenu(i18n("Cover Manager"), actions()); actions()->addAction("coverManager", menu); - /* menu->setIcon(SmallIcon("image-x-generic")); */ + menu->setIcon(QIcon::fromTheme("image-x-generic")); menu->addAction(createAction(i18n("&View Cover"), SLOT(slotViewCovers()), "viewCover", "document-preview")); menu->addAction(createAction(i18n("Get Cover From &File..."), SLOT(slotAddLocalCover()), "addCover", "document-import", QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_F))); menu->addAction(createAction(i18n("Get Cover From &Internet..."), SLOT(slotAddInternetCover()), "webImageCover", "network-server", QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_G))); menu->addAction(createAction(i18n("&Delete Cover"), SLOT(slotRemoveCovers()), "removeCover", "edit-delete")); menu->addAction(createAction(i18n("Show Cover &Manager"), SLOT(slotShowCoverManager()), "showCoverManager")); KToggleAction *upcomingAction = new KToggleAction(QIcon::fromTheme(QStringLiteral("go-jump-today")), i18n("Show &Play Queue"), actions()); actions()->addAction("showUpcoming", upcomingAction); connect(upcomingAction, SIGNAL(triggered(bool)), this, SLOT(slotSetUpcomingPlaylistEnabled(bool))); connect(m_collection->m_playerManager, &PlayerManager::signalStop, this, [this]() { m_collection->stop(); }); } QAction *PlaylistCollection::ActionHandler::createAction(const QString &text, const char *slot, const char *name, const QString &icon, const QKeySequence &shortcut) { auto actionCollection = actions(); QAction *action = new QAction(text, actions()); if(!icon.isEmpty()) { action->setIcon(QIcon::fromTheme(icon)); } connect(action, SIGNAL(triggered(bool)), slot); actionCollection->addAction(name, action); if (!shortcut.isEmpty()) { actionCollection->setDefaultShortcut(action, shortcut); } return action; } // vim: set et sw=4 tw=0 sta: diff --git a/playlistitem.cpp b/playlistitem.cpp index 0e264054..6902b2fc 100644 --- a/playlistitem.cpp +++ b/playlistitem.cpp @@ -1,478 +1,451 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * * 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 "playlistitem.h" #include #include #include #include #include #include #include #include "collectionlist.h" #include "musicbrainzquery.h" #include "tag.h" #include "coverinfo.h" #include "covermanager.h" #include "tagtransactionmanager.h" #include "juk_debug.h" PlaylistItemList PlaylistItem::m_playingItems; // static static void startMusicBrainzQuery(const FileHandle &file) { #if HAVE_TUNEPIMP // This deletes itself when finished. new MusicBrainzLookup(file); #else Q_UNUSED(file) #endif } static int naturalCompare(const QString &first, const QString &second) { static QCollator collator; collator.setNumericMode(true); collator.setCaseSensitivity(Qt::CaseInsensitive); return collator.compare(first, second); } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem public methods //////////////////////////////////////////////////////////////////////////////// PlaylistItem::~PlaylistItem() { // Although this isn't the most efficient way to accomplish the task of // stopping playback when deleting the item being played, it has the // stark advantage of working reliably. I'll tell anyone who tries to // optimize this, the timing issues can be *hard*. -- mpyne m_collectionItem->removeChildItem(this); if(m_playingItems.contains(this)) { m_playingItems.removeAll(this); if(m_playingItems.isEmpty()) playlist()->setPlaying(0); } playlist()->updateDeletedItem(this); emit playlist()->signalAboutToRemove(this); if(m_watched) Pointer::clear(this); } void PlaylistItem::setFile(const FileHandle &file) { m_collectionItem->updateCollectionDict(d->fileHandle.absFilePath(), file.absFilePath()); d->fileHandle = file; refresh(); } void PlaylistItem::setFile(const QString &file) { QString oldPath = d->fileHandle.absFilePath(); d->fileHandle.setFile(file); m_collectionItem->updateCollectionDict(oldPath, d->fileHandle.absFilePath()); refresh(); } FileHandle PlaylistItem::file() const { return d->fileHandle; } -Q_GLOBAL_STATIC_WITH_ARGS(QPixmap, globalGenericImage, (SmallIcon("image-x-generic"))) -Q_GLOBAL_STATIC_WITH_ARGS(QPixmap, globalPlayingImage, (UserIcon("playing"))) - -const QPixmap *PlaylistItem::pixmap(int column) const -{ - int offset = playlist()->columnOffset(); - - // Don't use hasCover here because that may dig into the track itself. - // Besides, we really just want to know if the cover manager has a cover - // for the track. - - if((column - offset) == CoverColumn && - d->fileHandle.coverInfo()->coverId() != CoverManager::NoMatch) - { - return globalGenericImage; - } - - if(column == playlist()->leftColumn() && - m_playingItems.contains(const_cast(this))) - { - return globalPlayingImage; - } - - //return QTreeWidgetItem::pixmap(column); - return nullptr; -} - QString PlaylistItem::text(int column) const { if(!d->fileHandle.tag()) return QString(); int offset = playlist()->columnOffset(); switch(column - offset) { case TrackColumn: return d->fileHandle.tag()->title(); case ArtistColumn: return d->fileHandle.tag()->artist(); case AlbumColumn: return d->fileHandle.tag()->album(); case CoverColumn: return QString(); case TrackNumberColumn: return d->fileHandle.tag()->track() > 0 ? QString::number(d->fileHandle.tag()->track()) : QString(); case GenreColumn: return d->fileHandle.tag()->genre(); case YearColumn: return d->fileHandle.tag()->year() > 0 ? QString::number(d->fileHandle.tag()->year()) : QString(); case LengthColumn: return d->fileHandle.tag()->lengthString(); case BitrateColumn: return QString::number(d->fileHandle.tag()->bitrate()); case CommentColumn: return d->fileHandle.tag()->comment(); case FileNameColumn: return d->fileHandle.fileInfo().fileName(); case FullPathColumn: return d->fileHandle.fileInfo().absoluteFilePath(); default: return QTreeWidgetItem::text(column); } } void PlaylistItem::setText(int column, const QString &text) { QTreeWidgetItem::setText(column, text); playlist()->slotWeightDirty(column); } void PlaylistItem::setPlaying(bool playing, bool master) { m_playingItems.removeAll(this); if(playing) { if(master) m_playingItems.prepend(this); else m_playingItems.append(this); } else { // This is a tricky little recursion, but it // in fact does clear the list. if(!m_playingItems.isEmpty()) m_playingItems.front()->setPlaying(false); } treeWidget()->viewport()->update(); } void PlaylistItem::guessTagInfo(TagGuesser::Type type) { switch(type) { case TagGuesser::FileName: { TagGuesser guesser(d->fileHandle.absFilePath()); Tag *tag = TagTransactionManager::duplicateTag(d->fileHandle.tag()); if(!guesser.title().isNull()) tag->setTitle(guesser.title()); if(!guesser.artist().isNull()) tag->setArtist(guesser.artist()); if(!guesser.album().isNull()) tag->setAlbum(guesser.album()); if(!guesser.track().isNull()) tag->setTrack(guesser.track().toInt()); if(!guesser.comment().isNull()) tag->setComment(guesser.comment()); TagTransactionManager::instance()->changeTagOnItem(this, tag); break; } case TagGuesser::MusicBrainz: startMusicBrainzQuery(d->fileHandle); break; } } Playlist *PlaylistItem::playlist() const { return static_cast(treeWidget()); } QVector PlaylistItem::cachedWidths() const { return d->cachedWidths; } void PlaylistItem::refresh() { m_collectionItem->refresh(); } void PlaylistItem::refreshFromDisk() { d->fileHandle.refresh(); refresh(); } void PlaylistItem::clear() { playlist()->clearItem(this); } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem protected methods //////////////////////////////////////////////////////////////////////////////// PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent) : QTreeWidgetItem(parent), d(0), m_watched(0) { setup(item); } PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent, QTreeWidgetItem *after) : QTreeWidgetItem(parent, after), d(0), m_watched(0) { setup(item); } // This constructor should only be used by the CollectionList subclass. PlaylistItem::PlaylistItem(CollectionList *parent) : QTreeWidgetItem(parent), m_watched(0) { d = new Data; m_collectionItem = static_cast(this); setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsDragEnabled); } int PlaylistItem::compare(const QTreeWidgetItem *item, int column, bool ascending) const { // reimplemented from QListViewItem int offset = playlist()->columnOffset(); if(!item) return 0; const PlaylistItem *playlistItem = static_cast(item); // The following statments first check to see if you can sort based on the // specified column. If the values for the two PlaylistItems are the same // in that column it then tries to sort based on columns 1, 2, 3 and 0, // (artist, album, track number, track name) in that order. int c = compare(this, playlistItem, column, ascending); if(c != 0) return c; else { // Loop through the columns doing comparisons until something is differnt. // If all else is the same, compare the track name. int last = !playlist()->isColumnHidden(AlbumColumn + offset) ? TrackNumberColumn : ArtistColumn; for(int i = ArtistColumn; i <= last; i++) { if(!playlist()->isColumnHidden(i + offset)) { c = compare(this, playlistItem, i, ascending); if(c != 0) return c; } } return compare(this, playlistItem, TrackColumn + offset, ascending); } } int PlaylistItem::compare(const PlaylistItem *firstItem, const PlaylistItem *secondItem, int column, bool) const { int offset = playlist()->columnOffset(); if(column < 0 || column > lastColumn() + offset || !firstItem->d || !secondItem->d) return 0; if(column < offset) { QString first = firstItem->text(column); QString second = secondItem->text(column); return naturalCompare(first, second); } switch(column - offset) { case TrackNumberColumn: if(firstItem->d->fileHandle.tag()->track() > secondItem->d->fileHandle.tag()->track()) return 1; else if(firstItem->d->fileHandle.tag()->track() < secondItem->d->fileHandle.tag()->track()) return -1; else return 0; break; case LengthColumn: if(firstItem->d->fileHandle.tag()->seconds() > secondItem->d->fileHandle.tag()->seconds()) return 1; else if(firstItem->d->fileHandle.tag()->seconds() < secondItem->d->fileHandle.tag()->seconds()) return -1; else return 0; break; case BitrateColumn: if(firstItem->d->fileHandle.tag()->bitrate() > secondItem->d->fileHandle.tag()->bitrate()) return 1; else if(firstItem->d->fileHandle.tag()->bitrate() < secondItem->d->fileHandle.tag()->bitrate()) return -1; else return 0; break; case CoverColumn: if(firstItem->d->fileHandle.coverInfo()->coverId() == secondItem->d->fileHandle.coverInfo()->coverId()) return 0; else if (firstItem->d->fileHandle.coverInfo()->coverId() != CoverManager::NoMatch) return -1; else return 1; break; default: return naturalCompare(firstItem->d->metadata[column - offset], secondItem->d->metadata[column - offset]); } } bool PlaylistItem::operator<(const QTreeWidgetItem &other) const { bool ascending = playlist()->header()->sortIndicatorOrder() == Qt::AscendingOrder; return compare(&other, playlist()->sortColumn(), ascending) == -1; } bool PlaylistItem::isValid() const { return bool(d->fileHandle.tag()); } void PlaylistItem::setTrackId(quint32 id) { m_trackId = id; } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem private methods //////////////////////////////////////////////////////////////////////////////// void PlaylistItem::setup(CollectionListItem *item) { m_collectionItem = item; d = item->d; item->addChildItem(this); setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsDragEnabled); int offset = playlist()->columnOffset(); int columns = lastColumn() + offset + 1; for(int i = offset; i < columns; i++) { setText(i, text(i)); } } //////////////////////////////////////////////////////////////////////////////// // PlaylistItem::Pointer implementation //////////////////////////////////////////////////////////////////////////////// QMap > PlaylistItem::Pointer::m_map; // static PlaylistItem::Pointer::Pointer(PlaylistItem *item) : m_item(item) { if(!m_item) return; m_item->m_watched = true; m_map[m_item].append(this); } PlaylistItem::Pointer::Pointer(const Pointer &p) : m_item(p.m_item) { m_map[m_item].append(this); } PlaylistItem::Pointer::~Pointer() { if(!m_item) return; m_map[m_item].removeAll(this); if(m_map[m_item].isEmpty()) { m_map.remove(m_item); m_item->m_watched = false; } } PlaylistItem::Pointer &PlaylistItem::Pointer::operator=(PlaylistItem *item) { if(item == m_item) return *this; if(m_item) { m_map[m_item].removeAll(this); if(m_map[m_item].isEmpty()) { m_map.remove(m_item); m_item->m_watched = false; } } if(item) { m_map[item].append(this); item->m_watched = true; } m_item = item; return *this; } void PlaylistItem::Pointer::clear(PlaylistItem *item) // static { if(!item) return; QList l = m_map[item]; foreach(Pointer *pointer, l) pointer->m_item = 0; m_map.remove(item); item->m_watched = false; } // vim: set et sw=4 tw=0 sta: diff --git a/playlistitem.h b/playlistitem.h index 6076420a..36f072c5 100644 --- a/playlistitem.h +++ b/playlistitem.h @@ -1,221 +1,220 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * * 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 . */ #ifndef JUK_PLAYLISTITEM_H #define JUK_PLAYLISTITEM_H #include #include #include #include #include #include "tagguesser.h" #include "filehandle.h" #include "juk_debug.h" class Playlist; class PlaylistItem; class CollectionListItem; class CollectionList; typedef QList PlaylistItemList; /** * Items for the Playlist and the baseclass for CollectionListItem. * The constructors and destructor are protected and new items should be * created via Playlist::createItem(). Items should be removed by * Playlist::clear(), Playlist::deleteFromDisk(), Playlist::clearItem() or * Playlist::clearItem(). */ class PlaylistItem : public QTreeWidgetItem { friend class Playlist; friend class SearchPlaylist; friend class UpcomingPlaylist; friend class CollectionList; friend class CollectionListItem; friend class Pointer; public: enum ColumnType { TrackColumn = 0, ArtistColumn = 1, AlbumColumn = 2, CoverColumn = 3, TrackNumberColumn = 4, GenreColumn = 5, YearColumn = 6, LengthColumn = 7, BitrateColumn = 8, CommentColumn = 9, FileNameColumn = 10, FullPathColumn = 11 }; /** * A helper class to implement guarded pointer semantics. */ class Pointer { public: Pointer() : m_item(0) {} Pointer(PlaylistItem *item); Pointer(const Pointer &p); ~Pointer(); Pointer &operator=(PlaylistItem *item); bool operator==(const Pointer &p) const { return m_item == p.m_item; } bool operator!=(const Pointer &p) const { return m_item != p.m_item; } PlaylistItem *operator->() const { return m_item; } PlaylistItem &operator*() const { return *m_item; } operator PlaylistItem*() const { return m_item; } static void clear(PlaylistItem *item); private: PlaylistItem *m_item; static QMap > m_map; }; friend class Pointer; static int lastColumn() { return FullPathColumn; } void setFile(const FileHandle &file); void setFile(const QString &file); FileHandle file() const; - virtual const QPixmap *pixmap(int column) const; virtual QString text(int column) const; virtual void setText(int column, const QString &text); void setPlaying(bool playing = true, bool master = true); void guessTagInfo(TagGuesser::Type type); Playlist *playlist() const; virtual CollectionListItem *collectionItem() { return m_collectionItem; } /** * This is an identifier for the playlist item which will remain unique * throughout the process lifetime. It stays constant once the PlaylistItem * is created. */ quint32 trackId() const { return m_trackId; } /** * The widths of items are cached when they're updated for us in computations * in the "weighted" listview column width mode. */ QVector cachedWidths() const; /** * This just refreshes from the in memory data. This may seem pointless at * first, but this data is shared between all of the list view items that are * based on the same file, so if another one of those items changes its data * it is important to refresh the others. */ virtual void refresh(); /** * This rereads the tag from disk. This affects all PlaylistItems based on * the same file. */ virtual void refreshFromDisk(); /** * Asks the item's playlist to remove the item (which uses deleteLater()). */ virtual void clear(); /** * Returns properly casted item below this one. */ PlaylistItem *itemBelow() { return static_cast(treeWidget()->itemBelow(this)); } /** * Returns properly casted item above this one. */ PlaylistItem *itemAbove() { return static_cast(treeWidget()->itemAbove(this)); } /** * Returns a reference to the list of the currently playing items, with the * first being the "master" item (i.e. the item from which the next track is * chosen). */ static const PlaylistItemList &playingItems() { return m_playingItems; } protected: /** * Items should always be created using Playlist::createItem() or through a * subclass or friend class. */ PlaylistItem(CollectionListItem *item, Playlist *parent); PlaylistItem(CollectionListItem *item, Playlist *parent, QTreeWidgetItem *after); /** * This is the constructor that should be used by subclasses. */ PlaylistItem(CollectionList *parent); /** * See the class documentation for an explanation of construction and deletion * of PlaylistItems. */ virtual ~PlaylistItem(); virtual int compare(const QTreeWidgetItem *item, int column, bool ascending) const; int compare(const PlaylistItem *firstItem, const PlaylistItem *secondItem, int column, bool ascending) const; bool operator<(const QTreeWidgetItem &other) const; bool isValid() const; void setTrackId(quint32 id); /** * Shared data between all PlaylistItems from the same track (incl. the CollectionItem * representing said track. */ struct Data : public QSharedData { FileHandle fileHandle; // Set within CollectionList QVector metadata; ///< Artist, album, or genre tags. Other columns unfilled QVector cachedWidths; }; using DataPtr = QExplicitlySharedDataPointer; DataPtr sharedData() const { return d; } private: DataPtr d; void setup(CollectionListItem *item); CollectionListItem *m_collectionItem; quint32 m_trackId; bool m_watched; static PlaylistItemList m_playingItems; }; inline QDebug operator<<(QDebug s, const PlaylistItem &item) { s << item.text(PlaylistItem::TrackColumn); return s; } #endif // vim: set et sw=4 tw=0 sta: diff --git a/statuslabel.cpp b/statuslabel.cpp index c394d0e2..725765bf 100644 --- a/statuslabel.cpp +++ b/statuslabel.cpp @@ -1,157 +1,158 @@ /** * Copyright (C) 2002-2004 Scott Wheeler * * 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 "statuslabel.h" #include #include #include #include #include #include #include +#include #include #include #include #include #include "filehandle.h" #include "playlistinterface.h" #include "actioncollection.h" #include "playermanager.h" #include "tag.h" #include "juk_debug.h" using namespace ActionCollection; //////////////////////////////////////////////////////////////////////////////// // static helpers //////////////////////////////////////////////////////////////////////////////// static QString formatTime(qint64 milliseconds) { static const KFormat fmt; return fmt.formatDuration(milliseconds); } //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// StatusLabel::StatusLabel(const PlaylistInterface ¤tPlaylist, QStatusBar *parent) : QWidget(parent) { m_playlistLabel = new KSqueezedTextLabel(this); m_playlistLabel->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred, QSizePolicy::Label)); m_playlistLabel->setTextFormat(Qt::PlainText); m_playlistLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); parent->addWidget(m_playlistLabel, 1); m_trackLabel = new QLabel(this); m_trackLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); m_trackLabel->setTextFormat(Qt::PlainText); parent->addPermanentWidget(m_trackLabel); m_itemTimeLabel = new QLabel(this); QFontMetrics fontMetrics(font()); m_itemTimeLabel->setAlignment(Qt::AlignCenter); m_itemTimeLabel->setMinimumWidth(fontMetrics.boundingRect("000:00 / 000:00").width()); m_itemTimeLabel->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); m_itemTimeLabel->setFrameStyle(QFrame::Box | QFrame::Sunken); m_itemTimeLabel->installEventFilter(this); parent->addPermanentWidget(m_itemTimeLabel); setItemTotalTime(0); setItemCurrentTime(0); QPushButton *jumpButton = new QPushButton(this); - jumpButton->setIcon(SmallIcon("go-up")); + jumpButton->setIcon(QIcon::fromTheme("go-jump")); jumpButton->setFlat(true); jumpButton->setToolTip(i18n("Jump to the currently playing item")); connect(jumpButton, &QPushButton::clicked, action("showPlaying"), &QAction::trigger); parent->addPermanentWidget(jumpButton); installEventFilter(this); slotCurrentPlaylistHasChanged(currentPlaylist); } void StatusLabel::slotPlayingItemHasChanged(const FileHandle &file) { const Tag *tag = file.tag(); const QString mid = (tag->artist().isEmpty() || tag->title().isEmpty()) ? QString() : QStringLiteral(" - "); setItemTotalTime(tag->seconds()); setItemCurrentTime(0); m_trackLabel->setText(tag->artist() + mid + tag->title()); } void StatusLabel::slotCurrentPlaylistHasChanged(const PlaylistInterface ¤tPlaylist) { if(!currentPlaylist.playing()) { return; } m_playlistLabel->setText(currentPlaylist.name()); m_trackLabel->setText( i18np("1 item", "%1 items", currentPlaylist.count()) + QStringLiteral(" - ") + formatTime(qint64(1000) * currentPlaylist.time()) ); } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// void StatusLabel::updateTime() { const qint64 milliseconds = m_showTimeRemaining ? m_itemTotalTime - m_itemCurrentTime : m_itemCurrentTime; const QString timeString = formatTime(milliseconds) + QStringLiteral(" / ") + formatTime(m_itemTotalTime); m_itemTimeLabel->setText(timeString); } bool StatusLabel::eventFilter(QObject *o, QEvent *e) { if(!o || !e) return false; QMouseEvent *mouseEvent = static_cast(e); if(e->type() == QEvent::MouseButtonRelease && mouseEvent->button() == Qt::LeftButton) { if(o == m_itemTimeLabel) { m_showTimeRemaining = !m_showTimeRemaining; updateTime(); } else action("showPlaying")->trigger(); return true; } return false; } // vim: set et sw=4 tw=0 sta: diff --git a/systemtray.cpp b/systemtray.cpp index 2572b556..c97b8651 100644 --- a/systemtray.cpp +++ b/systemtray.cpp @@ -1,528 +1,528 @@ /** * Copyright (C) 2002 Daniel Molkentin * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2004-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 "systemtray.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tag.h" #include "actioncollection.h" #include "playermanager.h" #include "coverinfo.h" #include "juk_debug.h" using namespace ActionCollection; PassiveInfo::PassiveInfo() : QFrame(nullptr, Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint ), m_timer(new QTimer(this)), m_layout(new QVBoxLayout(this)), m_justDie(false) { connect(m_timer, SIGNAL(timeout()), SLOT(timerExpired())); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); // Workaround transparent background in Oxygen when (ab-)using Qt::ToolTip setAutoFillBackground(true); setFrameStyle(StyledPanel | Plain); setLineWidth(2); } void PassiveInfo::startTimer(int delay) { m_timer->start(delay); } void PassiveInfo::show() { m_timer->start(3500); setWindowOpacity(1.0); QFrame::show(); } void PassiveInfo::setView(QWidget *view) { m_layout->addWidget(view); view->show(); // We are still hidden though. adjustSize(); positionSelf(); } 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(QEvent *) { m_timer->stop(); emit mouseEntered(); } void PassiveInfo::leaveEvent(QEvent *) { m_justDie = true; m_timer->start(50); } void PassiveInfo::hideEvent(QHideEvent *) { } void PassiveInfo::wheelEvent(QWheelEvent *e) { if(e->delta() >= 0) { emit nextSong(); } else { emit previousSong(); } e->accept(); } void PassiveInfo::positionSelf() { // Start with a QRect of our size, move it to the right spot. QRect r(rect()); QRect curScreen(KWindowSystem::workArea()); // Try to position in lower right of the screen QPoint anchor(curScreen.right() * 7 / 8, curScreen.bottom()); // Now make our rect hit that anchor. r.moveBottomRight(anchor); move(r.topLeft()); } //////////////////////////////////////////////////////////////////////////////// // public methods //////////////////////////////////////////////////////////////////////////////// SystemTray::SystemTray(PlayerManager *player, QWidget *parent) : KStatusNotifierItem(parent), m_popup(0), m_player(player), m_fadeTimer(0), m_fade(true), m_hasCompositionManager(false) { using ActionCollection::action; // Override the KSNI::action call introduced in KF5 // This should be initialized to the number of labels that are used. m_labels.fill(0, 3); setIconByName("juk"); setCategory(ApplicationStatus); setStatus(Active); // We were told to dock in systray by user, force us visible - m_forwardPix = SmallIcon("media-skip-forward"); - m_backPix = SmallIcon("media-skip-backward"); + m_forwardPix = QIcon::fromTheme("media-skip-forward"); + m_backPix = QIcon::fromTheme("media-skip-backward"); // Just create this here so that it show up in the DBus interface and the // key bindings dialog. QAction *rpaction = new QAction(i18n("Redisplay Popup"), this); ActionCollection::actions()->addAction("showPopup", rpaction); connect(rpaction, SIGNAL(triggered(bool)), SLOT(slotPlay())); QMenu *cm = contextMenu(); connect(m_player, SIGNAL(signalPlay()), this, SLOT(slotPlay())); connect(m_player, SIGNAL(signalPause()), this, SLOT(slotPause())); connect(m_player, SIGNAL(signalStop()), this, SLOT(slotStop())); cm->addAction( action("play") ); cm->addAction( action("pause") ); cm->addAction( action("stop") ); cm->addAction( action("forward") ); cm->addAction( action("back") ); cm->addSeparator(); // Pity the actionCollection doesn't keep track of what sub-menus it has. KActionMenu *menu = new KActionMenu(i18n("&Random Play"), this); // FIXME //actionCollection()->addAction("randomplay", menu); menu->addAction(action("disableRandomPlay")); menu->addAction(action("randomPlay")); menu->addAction(action("albumRandomPlay")); cm->addAction( menu ); cm->addAction( action("togglePopups") ); m_fadeTimer = new QTimer(this); m_fadeTimer->setObjectName( QLatin1String("systrayFadeTimer" )); connect(m_fadeTimer, SIGNAL(timeout()), SLOT(slotNextStep())); // Handle wheel events connect(this, SIGNAL(scrollRequested(int,Qt::Orientation)), SLOT(scrollEvent(int,Qt::Orientation))); // Add a quick hook for play/pause toggle connect(this, SIGNAL(secondaryActivateRequested(QPoint)), action("playPause"), SLOT(trigger())); if(m_player->playing()) slotPlay(); else if(m_player->paused()) slotPause(); } //////////////////////////////////////////////////////////////////////////////// // public slots //////////////////////////////////////////////////////////////////////////////// void SystemTray::slotPlay() { if(!m_player->playing()) return; QPixmap cover = m_player->playingFile().coverInfo()->pixmap(CoverInfo::FullSize); setOverlayIconByName("media-playback-start"); setToolTip(m_player->playingString(), cover); createPopup(); } void SystemTray::slotPause() { setOverlayIconByName("media-playback-pause"); } void SystemTray::slotPopupLargeCover() { if(!m_player->playing()) return; FileHandle playingFile = m_player->playingFile(); playingFile.coverInfo()->popup(); } void SystemTray::slotStop() { setToolTip(); setOverlayIconByName(QString()); delete m_popup; m_popup = 0; m_fadeTimer->stop(); } void SystemTray::slotPopupDestroyed() { for(int i = 0; i < m_labels.size(); ++i) m_labels[i] = 0; } void SystemTray::slotNextStep() { // Could happen I guess if the timeout event were queued while we're deleting m_popup if(!m_popup) return; ++m_step; // If we're not fading, immediately stop the fadeout if(!m_fade || m_step == STEPS) { m_step = 0; m_fadeTimer->stop(); emit fadeDone(); return; } if(m_hasCompositionManager) { m_popup->setWindowOpacity((1.0 * STEPS - m_step) / STEPS); } else { QColor result = interpolateColor(m_step); for(int i = 0; i < m_labels.size() && m_labels[i]; ++i) { QPalette palette; palette.setColor(m_labels[i]->foregroundRole(), result); m_labels[i]->setPalette(palette); } } } void SystemTray::slotFadeOut() { m_startColor = m_labels[0]->palette().color( QPalette::Text ); //textColor(); m_endColor = m_labels[0]->palette().color( QPalette::Window ); //backgroundColor(); m_hasCompositionManager = KWindowSystem::compositingActive(); connect(this, SIGNAL(fadeDone()), m_popup, SLOT(hide())); connect(m_popup, SIGNAL(mouseEntered()), this, 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]->palette().color( QPalette::Text ); //textColor(); disconnect(SIGNAL(fadeDone())); if(m_hasCompositionManager) m_popup->setWindowOpacity(1.0); m_step = STEPS - 1; // Simulate end of fade to solid text slotNextStep(); } //////////////////////////////////////////////////////////////////////////////// // private methods //////////////////////////////////////////////////////////////////////////////// QWidget *SystemTray::createInfoBox(QBoxLayout *parentLayout, const FileHandle &file) { // We always show the popup on the right side of the current screen, so // this logic assumes that. Earlier revisions had logic for popup being // wherever the systray icon is, so if it's decided to go that route again, // dig into the source control history. --mpyne if(file.coverInfo()->hasCover()) { addCoverButton(parentLayout, file.coverInfo()->pixmap(CoverInfo::Thumbnail)); addSeparatorLine(parentLayout); } auto infoBox = new QWidget; auto infoBoxVLayout = new QVBoxLayout(infoBox); infoBoxVLayout->setSpacing(3); infoBoxVLayout->setMargin(3); parentLayout->addWidget(infoBox); addSeparatorLine(parentLayout); createButtonBox(parentLayout); return infoBox; } void SystemTray::createPopup() { FileHandle playingFile = m_player->playingFile(); Tag *playingInfo = playingFile.tag(); // If the action exists and it's checked, do our stuff if(!ActionCollection::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; connect(m_popup, SIGNAL(destroyed()), SLOT(slotPopupDestroyed())); connect(m_popup, SIGNAL(timeExpired()), SLOT(slotFadeOut())); connect(m_popup, SIGNAL(nextSong()), SLOT(slotForward())); connect(m_popup, SIGNAL(previousSong()), SLOT(slotBack())); auto box = new QWidget; auto boxHLayout = new QHBoxLayout(box); boxHLayout->setSpacing(15); // Add space between text and buttons QWidget *infoBox = createInfoBox(boxHLayout, playingFile); QLayout *infoBoxLayout = infoBox->layout(); for(int i = 0; i < m_labels.size(); ++i) { QLabel *l = new QLabel(" "); l->setAlignment(Qt::AlignRight | Qt::AlignVCenter); m_labels[i] = l; infoBoxLayout->addWidget(l); } // 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; QString title = playingInfo->title().toHtmlEscaped(); m_labels[labelCount++]->setText(QString("

%1

").arg(title)); if(!playingInfo->artist().isEmpty()) m_labels[labelCount++]->setText(playingInfo->artist()); if(!playingInfo->album().isEmpty()) { QString album = playingInfo->album().toHtmlEscaped(); QString s = playingInfo->year() > 0 ? QString("%1 (%2)").arg(album).arg(playingInfo->year()) : QString("%1").arg(album); m_labels[labelCount++]->setText(s); } m_popup->setView(box); m_popup->show(); } void SystemTray::createButtonBox(QBoxLayout *parentLayout) { auto buttonBox = new QWidget; auto buttonBoxVLayout = new QVBoxLayout(buttonBox); buttonBoxVLayout->setSpacing(3); QPushButton *forwardButton = new QPushButton(m_forwardPix, QString()); forwardButton->setObjectName(QLatin1String("popup_forward")); connect(forwardButton, SIGNAL(clicked()), SLOT(slotForward())); QPushButton *backButton = new QPushButton(m_backPix, QString()); backButton->setObjectName(QLatin1String("popup_back")); connect(backButton, SIGNAL(clicked()), SLOT(slotBack())); buttonBoxVLayout->addWidget(forwardButton); buttonBoxVLayout->addWidget(backButton); parentLayout->addWidget(buttonBox); } /** * What happens here is that the action->trigger() 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() { ActionCollection::action("back")->trigger(); m_fade = false; } void SystemTray::slotForward() { ActionCollection::action("forward")->trigger(); m_fade = false; } void SystemTray::addSeparatorLine(QBoxLayout *parentLayout) { QFrame *line = new QFrame; line->setFrameShape(QFrame::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); parentLayout->addWidget(line); } void SystemTray::addCoverButton(QBoxLayout *parentLayout, const QPixmap &cover) { QPushButton *coverButton = new QPushButton; coverButton->setIconSize(cover.size()); coverButton->setIcon(cover); coverButton->setFixedSize(cover.size()); coverButton->setFlat(true); connect(coverButton, SIGNAL(clicked()), this, SLOT(slotPopupLargeCover())); parentLayout->addWidget(coverButton); } QColor 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 QColor( (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 QString &tip, const QPixmap &cover) { if(tip.isEmpty()) KStatusNotifierItem::setToolTip("juk", i18n("JuK"), QString()); else { - QPixmap myCover; + QIcon myCover; if(cover.isNull()) { - myCover = DesktopIcon("juk"); + myCover = QIcon::fromTheme("juk"); } else { //Scale to proper icon size, otherwise KStatusNotifierItem will show an unknown icon - int iconSize = KIconLoader::global()->currentSize(KIconLoader::Desktop); - myCover = cover.scaled(iconSize, iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); + const int iconSize = KIconLoader::global()->currentSize(KIconLoader::Desktop); + myCover = QIcon(cover.scaled(iconSize, iconSize, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } - KStatusNotifierItem::setToolTip(QIcon(myCover), i18n("JuK"), tip); + KStatusNotifierItem::setToolTip(myCover, i18n("JuK"), tip); } } void SystemTray::scrollEvent(int delta, Qt::Orientation orientation) { if(orientation == Qt::Horizontal) return; switch(QApplication::queryKeyboardModifiers()) { case Qt::ShiftModifier: if(delta > 0) ActionCollection::action("volumeUp")->trigger(); else ActionCollection::action("volumeDown")->trigger(); break; default: if(delta > 0) ActionCollection::action("forward")->trigger(); else ActionCollection::action("back")->trigger(); break; } } // vim: set et sw=4 tw=0 sta: diff --git a/systemtray.h b/systemtray.h index 3066aa48..f18a1894 100644 --- a/systemtray.h +++ b/systemtray.h @@ -1,149 +1,149 @@ /** * Copyright (C) 2002 Daniel Molkentin * Copyright (C) 2002-2004 Scott Wheeler * Copyright (C) 2007, 2008, 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 . */ #ifndef JUK_SYSTEMTRAY_H #define JUK_SYSTEMTRAY_H #include #include #include #include #include #include #include class SystemTray; class PlayerManager; class QLabel; class QTimer; class FileHandle; /** * Workalike of KPassivePopup intended to more easily support JuK's particular * usage pattern, including things like staying open while under the mouse. * * @author Michael Pyne */ class PassiveInfo : public QFrame { Q_OBJECT public: PassiveInfo(); // Sets view as a child widget to show in the popup window. // This widget does not take ownership of the widget. If you want it auto-deleted, // either re-parent it or create it using this widget as its parent. void setView(QWidget *view); QWidget *view() const; public slots: // Starts a timer to show the popup. The popup will not automatically delete itself // once hidden. void startTimer(int delay); virtual void show(); signals: void mouseEntered(); void timeExpired(); void previousSong(); void nextSong(); protected: virtual void enterEvent(QEvent *); virtual void leaveEvent(QEvent *); virtual void hideEvent(QHideEvent *); virtual void wheelEvent(QWheelEvent *); private: // Move us near the required position. void positionSelf(); private slots: void timerExpired(); private: QTimer *m_timer; QVBoxLayout *m_layout; bool m_justDie; }; class SystemTray : public KStatusNotifierItem { Q_OBJECT public: explicit SystemTray(PlayerManager *player, QWidget *parent = 0); signals: // Emitted when the fade process is complete. void fadeDone(); private: static const int STEPS = 20; ///< Number of intermediate steps for fading. void createPopup(); void setToolTip(const QString &tip = QString(), const QPixmap &cover = QPixmap()); // Creates the widget layout for the popup, returning the QWidget that // holds the text labels. QWidget *createInfoBox(QBoxLayout *parentLayout, const FileHandle &file); void addSeparatorLine(QBoxLayout *parentLayout); void addCoverButton(QBoxLayout *parentLayout, const QPixmap &cover); void createButtonBox(QBoxLayout *parentLayout); // Interpolates from start color to end color. If @p step == 0, then // m_startColor is returned, while @p step == @steps returns // m_endColor. QColor interpolateColor(int step, int steps = STEPS); private slots: void slotPlay(); void slotPause(); void slotStop(); void slotPopupDestroyed(); void slotNextStep(); ///< This is the fading routine. void slotPopupLargeCover(); void slotForward(); void slotBack(); void slotFadeOut(); ///< Fades out the text void slotMouseInPopup(); ///< Forces the text back to its normal color. void scrollEvent(int delta, Qt::Orientation orientation); private: - QPixmap m_backPix; - QPixmap m_forwardPix; + QIcon m_backPix; + QIcon m_forwardPix; QColor m_startColor, m_endColor; PassiveInfo *m_popup; PlayerManager *m_player; QVector m_labels; QTimer *m_fadeTimer; int m_step; bool m_fade; /// Used to choose between manual fade and windowOpacity bool m_hasCompositionManager; }; #endif // JUK_SYSTEMTRAY_H // vim: set et sw=4 tw=0 sta: diff --git a/viewmode.cpp b/viewmode.cpp index e48c65b8..9dbf8fe6 100644 --- a/viewmode.cpp +++ b/viewmode.cpp @@ -1,472 +1,472 @@ /** * Copyright (C) 2003-2004 Scott Wheeler * * 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 "viewmode.h" #include #include #include #include #include "playlistbox.h" #include "searchplaylist.h" #include "treeviewitemplaylist.h" #include "collectionlist.h" #include "juk_debug.h" //////////////////////////////////////////////////////////////////////////////// // ViewMode //////////////////////////////////////////////////////////////////////////////// ViewMode::ViewMode(PlaylistBox *b) : QObject(b), m_playlistBox(b), m_visible(false), m_needsRefresh(false) { m_playlistBox->viewport()->installEventFilter(this); } ViewMode::~ViewMode() { } // FIXME /*void ViewMode::paintCell(PlaylistBox::Item *item, QPainter *painter, const QColorGroup &colorGroup, int column, int width, int) { if(width < item->pixmap(column)->width()) return; if(m_needsRefresh) updateHeights(); QFontMetrics fm = painter->fontMetrics(); int y = item->listView()->itemMargin() + border; const QPixmap *pm = item->pixmap(column); if(item->isSelected()) { painter->eraseRect(0, 0, width, item->height()); painter->setRenderHint(QPainter::Antialiasing); QPen oldPen = painter->pen(); QPen newPen = oldPen; painter->setPen(newPen); newPen.setJoinStyle(Qt::RoundJoin); newPen.setWidth(1); QColor background = m_playlistBox->palette().color(QPalette::Highlight); newPen.setColor(m_playlistBox->palette().color(QPalette::Text)); painter->setPen(newPen); painter->drawRoundedRect(border, border, width - border * 2, item->height() - border * 2, 2, 2); QRect inner(border + 1, border + 1, width - border * 2 - 2, item->height() - border * 2 - 2); painter->fillRect(inner, background); QPainterPath path(inner.bottomLeft()); path.lineTo(QPoint(inner.topLeft().x(), inner.topLeft().y() - 3)); const QPointF topLeft(inner.topLeft()); QRectF arc(topLeft, QSizeF(4, 4)); path.arcTo(arc, 180, -90); path.lineTo(inner.topRight()); path.lineTo(inner.bottomRight()); path.lineTo(inner.bottomLeft()); QColor window(item->listView()->palette().window().color()); const QColor base = background; window.setAlphaF(0.5); QLinearGradient decoGradient1; decoGradient1.setStart(inner.topLeft()); decoGradient1.setFinalStop(inner.bottomLeft()); decoGradient1.setColorAt(0, window); decoGradient1.setColorAt(1, Qt::transparent); QLinearGradient decoGradient2; decoGradient2.setStart(inner.topLeft()); decoGradient2.setFinalStop(inner.topRight()); decoGradient2.setColorAt(0, Qt::transparent); decoGradient2.setColorAt(1, base); painter->fillPath(path, decoGradient1); painter->fillPath(path, decoGradient2); painter->setPen(colorGroup.color(QPalette::HighlightedText)); } else painter->eraseRect(0, 0, width, item->height()); if(!pm->isNull()) { int x = (width - pm->width()) / 2; x = qMax(x, item->listView()->itemMargin()); painter->drawPixmap(x, y, *pm); } y += pm->height() + fm.height() - fm.descent(); foreach(const QString &line, m_lines[item]) { int x = (width - fm.width(line)) / 2; x = qMax(x, item->listView()->itemMargin()); painter->drawText(x, y, line); y += fm.height() - fm.descent(); } if(item == item->listView()->dropItem()) paintDropIndicator(painter, width, item->height()); }*/ bool ViewMode::eventFilter(QObject *watched, QEvent *e) { if(m_visible && watched == m_playlistBox->viewport() && e->type() == QEvent::Resize) { QResizeEvent *re = static_cast(e); if(re->size().width() != re->oldSize().width()) m_needsRefresh = true; } if(e->type() == QEvent::Hide) m_needsRefresh = true; return QObject::eventFilter(watched, e); } QString ViewMode::name() const { return i18nc("the normal viewing mode", "Default"); } void ViewMode::setShown(bool shown) { m_visible = shown; if(shown) { - updateIcons(32); + updateIcons(); m_needsRefresh = true; } } -void ViewMode::updateIcons(int size) +void ViewMode::updateIcons() { for(QTreeWidgetItemIterator it(m_playlistBox); *it; ++it) { PlaylistBox::Item *i = static_cast(*it); - i->setIcon(0, SmallIcon(i->iconName(), size)); + i->setIcon(0, QIcon::fromTheme(i->iconName())); } } void ViewMode::setupItem(PlaylistBox::Item *item) const { Q_UNUSED(item); // FIXME /*const PlaylistBox *box = item->listView(); const int width = box->width() - box->verticalScrollBar()->width() - border * 2; const int baseHeight = 2 * box->itemMargin() + 32 + border * 2; const QFontMetrics fm = box->fontMetrics(); item->setHeight(baseHeight + (fm.height() - fm.descent()) * lines(item, fm, width).count());*/ } void ViewMode::updateHeights() { // FIXME /*const int width = m_playlistBox->width() - m_playlistBox->verticalScrollBar()->width() - border * 2; const int baseHeight = 2 * m_playlistBox->itemMargin() + 32 + border * 2 + 4; const QFontMetrics fm = m_playlistBox->fontMetrics(); for(Q3ListViewItemIterator it(m_playlistBox); it.current(); ++it) { PlaylistBox::Item *i = static_cast(it.current()); m_lines[i] = lines(i, fm, width); const int height = baseHeight + (fm.height() - fm.descent()) * m_lines[i].count(); i->setHeight(height); } m_needsRefresh = false;*/ } void ViewMode::paintDropIndicator(QPainter *painter, int width, int height) // static { static const int border = 1; static const int lineWidth = 2; QPen oldPen = painter->pen(); QPen newPen = oldPen; newPen.setWidth(lineWidth); newPen.setStyle(Qt::DotLine); painter->setPen(newPen); painter->drawRect(border, border, width - border * 2, height - border * 2); painter->setPen(oldPen); } QStringList ViewMode::lines(const PlaylistBox::Item *item, const QFontMetrics &fm, int width) { // Here 32 is a bit arbitrary, but that's the width of the icons in this // mode and seems to a reasonable toLower bound. if(width < 32) return QStringList(); QString line = item->text(); QStringList l; Q_UNUSED(fm); while(!line.isEmpty()) { int textLength = line.length(); // FIXME /*while(textLength > 0 && fm.width(line.mid(0, textLength).trimmed()) + item->listView()->itemMargin() * 2 > width) { int i = line.lastIndexOf(QRegExp( "\\W"), textLength - 1); if(i > 0) textLength = i; else textLength--; }*/ l.append(line.mid(0, textLength).trimmed()); line = line.mid(textLength); } return l; } /////////////////////////////////////////////////////////////////////////////// // CompactViewMode //////////////////////////////////////////////////////////////////////////////// CompactViewMode::CompactViewMode(PlaylistBox *b) : ViewMode(b) { } CompactViewMode::~CompactViewMode() { } // FIXME /*void CompactViewMode::paintCell(PlaylistBox::Item *item, QPainter *painter, const QColorGroup &colorGroup, int column, int width, int align) { item->K3ListViewItem::paintCell(painter, colorGroup, column, width, align); if(item == item->listView()->dropItem()) paintDropIndicator(painter, width, item->height()); }*/ QString CompactViewMode::name() const { return i18nc("compact viewing mode", "Compact"); } void CompactViewMode::setShown(bool shown) { setVisible(shown); if(shown) { - updateIcons(16); + updateIcons(); updateHeights(); } } void CompactViewMode::updateHeights() { // FIXME /*for(Q3ListViewItemIterator it(playlistBox()); it.current(); ++it) it.current()->setup();*/ } //////////////////////////////////////////////////////////////////////////////// // TreeViewMode //////////////////////////////////////////////////////////////////////////////// TreeViewMode::TreeViewMode(PlaylistBox *b) : CompactViewMode(b), m_dynamicListsFrozen(false), m_setup(false) { } TreeViewMode::~TreeViewMode() { } QString TreeViewMode::name() const { return i18n("Tree"); } void TreeViewMode::setShown(bool show) { CompactViewMode::setShown(show); playlistBox()->setRootIsDecorated(show); // FIXME /*if(show) { PlaylistBox::Item *collectionItem = PlaylistBox::Item::collectionItem(); if(!collectionItem) return; if(collectionItem && m_searchCategories.isEmpty()) setupDynamicPlaylists(); else { foreach(PlaylistBox::Item *item, m_searchCategories) item->setVisible(true); } if(!m_setup) { m_setup = true; playlistBox()->setSorting(-1); CollectionList::instance()->setupTreeViewEntries(this); playlistBox()->setSorting(0); playlistBox()->sort(); } } else { foreach(PlaylistBox::Item *item, m_searchCategories) item->setVisible(false); }*/ } void TreeViewMode::removeItem(const QString &item, unsigned column) { if(!m_setup) return; QString itemKey; if(column == PlaylistItem::ArtistColumn) itemKey = "artists" + item; else if(column == PlaylistItem::GenreColumn) itemKey = "genres" + item; else if(column == PlaylistItem::AlbumColumn) itemKey = "albums" + item; else { qCWarning(JUK_LOG) << "Unhandled column type " << column; return; } if(!m_treeViewItems.contains(itemKey)) return; TreeViewItemPlaylist *itemPlaylist = m_treeViewItems.value(itemKey, 0); if(m_dynamicListsFrozen) { m_pendingItemsToRemove << itemKey; return; } m_treeViewItems.remove(itemKey); itemPlaylist->deleteLater(); emit signalPlaylistDestroyed(itemPlaylist); } void TreeViewMode::addItems(const QStringList &items, unsigned column) { if(!m_setup) return; QString searchCategory; if(column == PlaylistItem::ArtistColumn) searchCategory = "artists"; else if(column == PlaylistItem::GenreColumn) searchCategory = "genres"; else if(column == PlaylistItem::AlbumColumn) searchCategory = "albums"; else { qCWarning(JUK_LOG) << "Unhandled column type " << column; return; } ColumnList columns; columns.append(column); PlaylistSearch::Component::MatchMode mode = PlaylistSearch::Component::ContainsWord; if(column != PlaylistItem::ArtistColumn) mode = PlaylistSearch::Component::Exact; PlaylistSearch::ComponentList components; PlaylistList playlists; playlists.append(CollectionList::instance()); QString itemKey; PlaylistBox::Item *itemParent = m_searchCategories.value(searchCategory, 0); foreach(const QString &item, items) { itemKey = searchCategory + item; if(m_treeViewItems.contains(itemKey)) continue; components.clear(); components.append(PlaylistSearch::Component(item, false, columns, mode)); PlaylistSearch s(playlists, components, PlaylistSearch::MatchAny, false); TreeViewItemPlaylist *p = new TreeViewItemPlaylist(playlistBox(), s, item); playlistBox()->setupPlaylist(p, "audio-midi", itemParent); m_treeViewItems.insert(itemKey, p); } } void TreeViewMode::setDynamicListsFrozen(bool frozen) { m_dynamicListsFrozen = frozen; if(frozen) return; foreach(const QString &pendingItem, m_pendingItemsToRemove) { m_treeViewItems[pendingItem]->deleteLater(); m_treeViewItems.remove(pendingItem); } m_pendingItemsToRemove.clear(); } void TreeViewMode::setupDynamicPlaylists() { PlaylistBox::Item *i; PlaylistBox::Item *collectionItem = PlaylistBox::Item::collectionItem(); i = new PlaylistBox::Item(collectionItem, "media-optical-audio", i18n("Artists")); m_searchCategories.insert("artists", i); i = new PlaylistBox::Item(collectionItem, "media-optical-audio", i18n("Albums")); m_searchCategories.insert("albums", i); i = new PlaylistBox::Item(collectionItem, "media-optical-audio", i18n("Genres")); m_searchCategories.insert("genres", i); } // vim: set et sw=4 tw=0 sta: diff --git a/viewmode.h b/viewmode.h index 622d1e07..f2d0e566 100644 --- a/viewmode.h +++ b/viewmode.h @@ -1,152 +1,152 @@ /** * Copyright (C) 2003-2004 Scott Wheeler * Copyright (C) 2007 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 . */ -#ifndef VIEWMODE_H -#define VIEWMODE_H +#ifndef JUK_VIEWMODE_H +#define JUK_VIEWMODE_H #include #include #include #include "playlistbox.h" class QPainter; class QColorGroup; class ViewMode : public QObject { Q_OBJECT public: explicit ViewMode(PlaylistBox *b); virtual ~ViewMode(); virtual QString name() const; virtual void setShown(bool shown); /*virtual void paintCell(PlaylistBox::Item *item, QPainter *painter, const QColorGroup &colorGroup, int column, int width, int align);*/ virtual bool eventFilter(QObject *watched, QEvent *e); void queueRefresh() { m_needsRefresh = true; } virtual void setupItem(PlaylistBox::Item *item) const; virtual void setupDynamicPlaylists() {} /** * If the view mode has dynamic lists, this function is used to temporarily * freeze them to prevent them from deleting dynamic elements. */ virtual void setDynamicListsFrozen(bool /* frozen */) {} /** * Used for dynamic view modes. This function will be called when \p items * are added to \p column (even if the view mode hasn't been shown yet). */ virtual void addItems(const QStringList &items, unsigned column) { Q_UNUSED(items); Q_UNUSED(column); } /** * Used for dynamic view modes. This function will be called when \p item * is removed from \p column (even if the view mode hasn't been shown yet). */ virtual void removeItem(const QString &item, unsigned column) { Q_UNUSED(item); Q_UNUSED(column); } protected: PlaylistBox *playlistBox() const { return m_playlistBox; } bool visible() const { return m_visible; } void setVisible(bool v) { m_visible = v; } - void updateIcons(int size); + void updateIcons(); virtual void updateHeights(); static void paintDropIndicator(QPainter *painter, int width, int height); private: static QStringList lines(const PlaylistBox::Item *item, const QFontMetrics &fm, int width); PlaylistBox *m_playlistBox; bool m_visible; bool m_needsRefresh; QMap m_lines; static const int border = 4; }; //////////////////////////////////////////////////////////////////////////////// class CompactViewMode : public ViewMode { public: explicit CompactViewMode(PlaylistBox *b); virtual ~CompactViewMode(); virtual QString name() const; virtual void setShown(bool shown); /*virtual void paintCell(PlaylistBox::Item *item, QPainter *painter, const QColorGroup &colorGroup, int column, int width, int align);*/ virtual void setupItem(PlaylistBox::Item *) const { /*item->QTreeWidgetItem::setup();*/ } protected: virtual void updateHeights(); }; //////////////////////////////////////////////////////////////////////////////// class TreeViewItemPlaylist; class TreeViewMode : public CompactViewMode { Q_OBJECT public: explicit TreeViewMode(PlaylistBox *l); virtual ~TreeViewMode(); virtual QString name() const; virtual void setShown(bool shown); virtual void setupDynamicPlaylists(); virtual void setDynamicListsFrozen(bool frozen); virtual void removeItem(const QString &item, unsigned column); virtual void addItems(const QStringList &items, unsigned column); signals: void signalPlaylistDestroyed(Playlist*); private: QMap m_searchCategories; QMap m_treeViewItems; QStringList m_pendingItemsToRemove; bool m_dynamicListsFrozen; bool m_setup; }; #endif // vim: set et sw=4 tw=0 sta: diff --git a/webimagefetcher.cpp b/webimagefetcher.cpp index a52ca024..ff28775d 100644 --- a/webimagefetcher.cpp +++ b/webimagefetcher.cpp @@ -1,256 +1,258 @@ /** * Copyright (C) 2004 Nathan Toone * Copyright (C) 2007, 2017 Michael Pyne * Copyright (C) 2012 Martin Sandsmark * * 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 "webimagefetcher.h" #include #include #include #include #include "covermanager.h" #include "filehandle.h" #include "tag.h" #include "juk.h" #include "juk_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include class WebImageFetcher::Private { friend class WebImageFetcher; FileHandle file; QString artist; QString albumName; QPointer connection; QDialog *dialog = nullptr; QUrl url; }; WebImageFetcher::WebImageFetcher(QObject *parent) : QObject(parent) , d(new Private) { } WebImageFetcher::~WebImageFetcher() { delete d; } void WebImageFetcher::setFile(const FileHandle &file) { d->file = file; d->artist = file.tag()->artist(); d->albumName = file.tag()->album(); } void WebImageFetcher::abortSearch() { if (d->connection) d->connection->kill(); } void WebImageFetcher::searchCover() { QStatusBar *statusBar = JuK::JuKInstance()->statusBar(); statusBar->showMessage(i18n("Searching for cover. Please Wait...")); QUrlQuery urlQuery; urlQuery.addQueryItem("method", "album.getInfo"); urlQuery.addQueryItem("api_key", "3e6ecbd7284883089e8f2b5b53b0aecd"); urlQuery.addQueryItem("artist", d->artist); urlQuery.addQueryItem("album", d->albumName); QUrl url("http://ws.audioscrobbler.com/2.0/"); url.setQuery(urlQuery); qCDebug(JUK_LOG) << "Using request " << url.toDisplayString(); d->connection = KIO::storedGet(url, KIO::Reload /* reload always */, KIO::HideProgressInfo); connect(d->connection, SIGNAL(result(KJob*)), SLOT(slotWebRequestFinished(KJob*))); // Wait for the results... } void WebImageFetcher::slotWebRequestFinished(KJob *job) { if (job != d->connection) return; QStatusBar *statusBar = JuK::JuKInstance()->statusBar(); if (!job || job->error()) { qCCritical(JUK_LOG) << "Error reading image results from last.fm!\n"; qCCritical(JUK_LOG) << d->connection->errorString(); return; } if (d->connection->data().isEmpty()) { qCCritical(JUK_LOG) << "last.fm returned an empty result!\n"; return; } QDomDocument results("ResultSet"); QString errorStr; int errorCol, errorLine; if (!results.setContent(d->connection->data(), &errorStr, &errorLine, &errorCol)) { qCCritical(JUK_LOG) << "Unable to create XML document from results.\n"; qCCritical(JUK_LOG) << "Line " << errorLine << ", " << errorStr; return; } QDomElement n = results.documentElement(); if (n.isNull()) { qCDebug(JUK_LOG) << "No document root in XML results??\n"; return; } if (n.nodeName() != QLatin1String("lfm")) { qCDebug(JUK_LOG) << "Invalid resulting XML document, not "; return; } if (n.attribute(QStringLiteral("status")) != QLatin1String("ok")) { const QDomElement err = n.firstChildElement(QStringLiteral("error")); const int errCode = err.attribute(QStringLiteral("code")).toInt(); if (errCode == 6) { KMessageBox::information(nullptr, i18n("Album '%1' not found.", d->albumName), i18nc("@title:window", "Album not Found")); } else { KMessageBox::error(nullptr, i18n("Error %1 when searching for cover:\n%2", errCode, err.text())); } statusBar->clearMessage(); return; } n = n.firstChildElement("album"); //FIXME: We assume they have a sane sorting (smallest -> largest) const QString imageUrl = n.lastChildElement("image").text(); if (imageUrl.isEmpty()) { - KMessageBox::information(nullptr, i18n("No available cover for the album '%1'.", d->albumName), i18nc("@title:window", "Cover not Available")); + KMessageBox::information(nullptr, i18n("No available cover for the album '%1'.", d->albumName), + i18nc("@title:window", "Cover not Available")); statusBar->clearMessage(); return; } d->url = QUrl::fromEncoded(imageUrl.toLatin1()); //TODO: size attribute can have the values mega, extralarge, large, medium and small qCDebug(JUK_LOG) << "Got cover:" << d->url; statusBar->showMessage(i18n("Downloading cover. Please Wait...")); KIO::StoredTransferJob *newJob = KIO::storedGet(d->url, KIO::Reload /* reload always */, KIO::HideProgressInfo); connect(newJob, SIGNAL(result(KJob*)), SLOT(slotImageFetched(KJob*))); } void WebImageFetcher::slotImageFetched(KJob* j) { QStatusBar *statusBar = JuK::JuKInstance()->statusBar(); statusBar->clearMessage(); KIO::StoredTransferJob *job = qobject_cast(j); if (d->dialog) return; d->dialog = new QDialog; d->dialog->setWindowTitle(i18n("Cover found")); auto dlgVLayout = new QVBoxLayout(d->dialog); if(job->error()) { - qCCritical(JUK_LOG) << "Unable to grab image\n"; - d->dialog->setWindowIcon(DesktopIcon("dialog-error")); - // TODO: What kind of error announcement is this?? + qCCritical(JUK_LOG) << "Unable to grab image" << job->errorText(); + + KMessageBox::sorry(nullptr, i18n("Failed to download requested cover art: %1", job->errorString()), + i18nc("@title:window", "Could not download cover art")); return; } // TODO: 150x150 seems inconsistent with HiDPI, figure out something better QPixmap iconImage, realImage(150, 150); iconImage.loadFromData(job->data()); realImage.fill(Qt::transparent); if(iconImage.isNull()) { qCCritical(JUK_LOG) << "Thumbnail image is not of a supported format\n"; return; } // Scale down if necesssary if(iconImage.width() > 150 || iconImage.height() > 150) iconImage = iconImage.scaled(150, 150, Qt::KeepAspectRatio, Qt::SmoothTransformation); QLabel *cover = new QLabel(d->dialog); cover->setPixmap(iconImage); dlgVLayout->addWidget(cover); QLabel *infoLabel = new QLabel(i18n("Cover fetched from last.fm."), d->dialog); infoLabel->setOpenExternalLinks(true); infoLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); dlgVLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Expanding)); dlgVLayout->addWidget(infoLabel); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, d->dialog); dlgVLayout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, d->dialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, d->dialog, &QDialog::reject); connect(d->dialog, &QDialog::accepted, this, &WebImageFetcher::slotCoverChosen); connect(d->dialog, &QDialog::rejected, this, &WebImageFetcher::destroyDialog); d->dialog->setWindowIcon(realImage); d->dialog->show(); } void WebImageFetcher::slotCoverChosen() { qCDebug(JUK_LOG) << "Adding new cover for " << d->file.tag()->fileName() << "from URL" << d->url; coverKey newId = CoverManager::addCover(d->url, d->file.tag()->artist(), d->file.tag()->album()); if (newId != CoverManager::NoMatch) { emit signalCoverChanged(newId); destroyDialog(); } } void WebImageFetcher::destroyDialog() { if (!d->dialog) return; d->dialog->close(); d->dialog->deleteLater(); d->dialog = 0; } // vim: set et sw=4 tw=0 sta: