/* * Cantata * * Copyright (c) 2011-2022 Craig Drummond * * ---- * * 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; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "playlistrulesdialog.h" #include "playlistruledialog.h" #include "support/messagebox.h" #include "widgets/basicitemdelegate.h" #include "widgets/icons.h" #include #include #include #include #include #include #include #define REMOVE(w) \ w->setVisible(false); \ w->deleteLater(); \ w=0; class RulesSort : public QSortFilterProxyModel { public: RulesSort(QObject *parent) : QSortFilterProxyModel(parent) { setDynamicSortFilter(true); setFilterCaseSensitivity(Qt::CaseInsensitive); setSortCaseSensitivity(Qt::CaseInsensitive); setSortLocaleAware(true); sort(0); } ~RulesSort() override { } bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { bool lInc=left.data(Qt::UserRole+2).toBool(); bool rInc=right.data(Qt::UserRole+2).toBool(); if (lInc==rInc) { return left.data(Qt::DisplayRole).toString().localeAwareCompare(right.data(Qt::DisplayRole).toString())<0; } else { return lInc ? true : false; } } }; static QString translateStr(const QString &key) { if (RulesPlaylists::constArtistKey==key) { return QObject::tr("Artist"); } else if (RulesPlaylists::constSimilarArtistsKey==key) { return QObject::tr("SimilarArtists"); } else if (RulesPlaylists::constAlbumArtistKey==key) { return QObject::tr("AlbumArtist"); } else if (RulesPlaylists::constComposerKey==key) { return QObject::tr("Composer"); } else if (RulesPlaylists::constCommentKey==key) { return QObject::tr("Comment"); } else if (RulesPlaylists::constAlbumKey==key) { return QObject::tr("Album"); } else if (RulesPlaylists::constTitleKey==key) { return QObject::tr("Title"); } else if (RulesPlaylists::constGenreKey==key) { return QObject::tr("Genre"); } else if (RulesPlaylists::constDateKey==key) { return QObject::tr("Date"); } else if (RulesPlaylists::constFileKey==key) { return QObject::tr("File"); } else { return key; } } static void update(QStandardItem *i, const RulesPlaylists::Rule &rule) { RulesPlaylists::Rule::ConstIterator it(rule.constBegin()); RulesPlaylists::Rule::ConstIterator end(rule.constEnd()); QMap v; QString str; QString type=QObject::tr("Include"); bool exact=true; bool include=true; for (int count=0; it!=end; ++it, ++count) { if (RulesPlaylists::constExcludeKey==it.key()) { if (QLatin1String("true")==it.value()) { type=QObject::tr("Exclude"); include=false; } } else if (RulesPlaylists::constExactKey==it.key()) { if (QLatin1String("false")==it.value()) { exact=false; } } else { str+=QString("%1=%2").arg(translateStr(it.key()), it.value()); if (countsetText(str); i->setData(v); i->setData(include, Qt::UserRole+2); i->setFlags(Qt::ItemIsSelectable| Qt::ItemIsEnabled); } PlaylistRulesDialog::PlaylistRulesDialog(QWidget *parent, RulesPlaylists *m) : Dialog(parent, "PlaylistRulesDialog") , rules(m) , dlg(nullptr) { QWidget *mainWidet = new QWidget(this); setupUi(mainWidet); setMainWidget(mainWidet); setButtons(Ok|Cancel); enableButton(Ok, false); setCaption(rules->isDynamic() ? tr("Dynamic Playlist") : tr("Smart Playlist")); setAttribute(Qt::WA_DeleteOnClose); connect(addBtn, SIGNAL(clicked()), SLOT(add())); connect(editBtn, SIGNAL(clicked()), SLOT(edit())); connect(removeBtn, SIGNAL(clicked()), SLOT(remove())); connect(rulesList, SIGNAL(itemsSelected(bool)), SLOT(controlButtons())); connect(nameText, SIGNAL(textChanged(const QString &)), SLOT(enableOkButton())); connect(aboutLabel, SIGNAL(leftClickedUrl()), this, SLOT(showAbout())); connect(ratingFrom, SIGNAL(valueChanged(int)), SLOT(ratingChanged(int))); connect(ratingTo, SIGNAL(valueChanged(int)), SLOT(ratingChanged(int))); if (rules->isDynamic()) { connect(rules, SIGNAL(saved(bool)), SLOT(saved(bool))); } if (style()->styleHint(QStyle::SH_DialogButtonBox_ButtonsHaveIcons)) { addBtn->setIcon(Icons::self()->addIcon); editBtn->setIcon(Icons::self()->editIcon); removeBtn->setIcon(Icons::self()->removeIcon); } rulesList->setInfoText(tr("Each playlist requires a unique name, enter that above.\n\n" "Use the 'Add' button to add a 'rule' to control which artists, genres, etc you want included in the playlist. " "Any rules added will be listed here. " "A playlist with no rules will use songs from your whole music library.\n\n" "Use the settings below to restrict the range of songs added.")); messageWidget->setVisible(false); model=new QStandardItemModel(this); proxy=new RulesSort(this); proxy->setSourceModel(model); rulesList->setModel(proxy); rulesList->setItemDelegate(new BasicItemDelegate(rulesList)); rulesList->setAlternatingRowColors(false); minDuration->setSpecialValueText(tr("No Limit")); maxDuration->setSpecialValueText(tr("No Limit")); numTracks->setRange(rules->minTracks(), rules->maxTracks()); maxAge->setRange(0, 10*365); if (rules->isDynamic()) { REMOVE(orderLabel) REMOVE(order) REMOVE(orderAscending) orderLayout->deleteLater(); numTracks->setValue(qMax(qMin(10, rules->maxTracks()), rules->minTracks())); } else { numTracks->setValue(qMax(qMin(100, rules->maxTracks()), rules->minTracks())); for (int i=0; iaddItem(RulesPlaylists::orderName((RulesPlaylists::Order)i), i); } order->setCurrentIndex(RulesPlaylists::Order_Random); orderAscending->addItem(tr("Ascending")); orderAscending->addItem(tr("Descending")); orderAscending->setEnabled(false); connect(order, SIGNAL(currentIndexChanged(int)), this, SLOT(setOrder())); } controlButtons(); rulesList->setMinimumSize(Utils::scaleForDpi(400), Utils::scaleForDpi(160)); resize(Utils::scaleForDpi(500), Utils::scaleForDpi(240)); if (!rules->isDynamic()) { numberOfSongsLabel->setText(tr("Number of songs:")); } static bool registered=false; if (!registered) { qRegisterMetaType("RulesPlaylists::Rule"); registered=true; } } PlaylistRulesDialog::~PlaylistRulesDialog() { } void PlaylistRulesDialog::edit(const QString &name) { RulesPlaylists::Entry e=rules->entry(name); if (model->rowCount()) { model->removeRows(0, model->rowCount()); } nameText->setText(name); for (const RulesPlaylists::Rule &r: e.rules) { QStandardItem *item = new QStandardItem(); ::update(item, r); model->setItem(model->rowCount(), 0, item); } origName=name; ratingFrom->setValue(e.ratingFrom); ratingTo->setValue(e.ratingTo); includeUnrated->setChecked(e.includeUnrated); ratingChanged(0); minDuration->setValue(e.minDuration); maxDuration->setValue(e.maxDuration); numTracks->setValue(e.numTracks); if (order) { order->setCurrentIndex(e.order); setOrder(); orderAscending->setCurrentIndex(e.orderAscending ? 0 : 1); } maxAge->setValue(e.maxAge); show(); } void PlaylistRulesDialog::enableOkButton() { bool enable=!nameText->text().trimmed().isEmpty(); enableButton(Ok, enable); } void PlaylistRulesDialog::slotButtonClicked(int button) { switch (button) { case Ok: { if (save()) { accept(); } break; } case Cancel: reject(); // Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!! Dialog::slotButtonClicked(button); break; default: break; } } void PlaylistRulesDialog::controlButtons() { int numSel=rulesList->selectedIndexes().count(); removeBtn->setEnabled(numSel>0); editBtn->setEnabled(1==numSel); } void PlaylistRulesDialog::add() { if (!dlg) { dlg=new PlaylistRuleDialog(this, rules->isDynamic()); connect(dlg, SIGNAL(addRule(const RulesPlaylists::Rule&)), SLOT(addRule(const RulesPlaylists::Rule&))); } dlg->createNew(); } void PlaylistRulesDialog::addRule(const RulesPlaylists::Rule &rule) { QStandardItem *item = new QStandardItem(); ::update(item, rule); int index=indexOf(item); if (-1!=index) { delete item; } else { model->setItem(model->rowCount(), 0, item); } } void PlaylistRulesDialog::edit() { QModelIndexList items=rulesList->selectedIndexes(); if (1!=items.count()) { return; } if (!dlg) { dlg=new PlaylistRuleDialog(this, rules->isDynamic()); connect(dlg, SIGNAL(addRule(const RulesPlaylists::Rule&)), SLOT(addRule(const RulesPlaylists::Rule&))); } QModelIndex index=proxy->mapToSource(items.at(0)); QStandardItem *item=model->itemFromIndex(index); RulesPlaylists::Rule rule; QMap v=item->data().toMap(); QMap::ConstIterator it(v.constBegin()); QMap::ConstIterator end(v.constEnd()); for (; it!=end; ++it) { rule.insert(it.key(), it.value().toString()); } if (dlg->edit(rule)) { ::update(item, dlg->rule()); int idx=indexOf(item, true); if (-1!=idx && idx!=index.row()) { model->removeRow(index.row()); } } } void PlaylistRulesDialog::remove() { QModelIndexList items=rulesList->selectedIndexes(); QList rows; for (const QModelIndex &i: items) { rows.append(proxy->mapToSource(i).row()); } std::sort(rows.begin(), rows.end()); while (rows.count()) { model->removeRow(rows.takeLast()); } } void PlaylistRulesDialog::showAbout() { if (rules->isDynamic()) { MessageBox::information(this, #ifdef Q_OS_MAC tr("About dynamic rules")+QLatin1String("

")+ #endif tr("

Cantata will query your library using all of the rules listed. " "The list of Include rules will be used to build a set of songs that can be used. " "The list of Exclude rules will be used to build a set of songs that cannot be used. " "If there are no Include rules, Cantata will assume that all songs (bar those from Exclude) can be used.

" "

e.g. to have Cantata look for 'Rock songs by Wibble OR songs by Various Artists', you would need the following: " "

  • Include AlbumArtist=Wibble Genre=Rock
  • Include AlbumArtist=Various Artists
" "To have Cantata look for 'Songs by Wibble but not from album Abc', you would need the following: " "
  • Include AlbumArtist=Wibble
  • Exclude AlbumArtist=Wibble Album=Abc
" "After the set of usable songs has been created, Cantata will randomly select songs to " "keep the play queue filled with specified number of entries (10 by default). If a range of ratings has been specified, then " "only songs with a rating within this range will be used. Likewise, if a duration has been set.

") ); } else { MessageBox::information(this, #ifdef Q_OS_MAC tr("About smart rules")+QLatin1String("

")+ #endif tr("

Cantata will query your library using all of the rules listed. " "The list of Include rules will be used to build a set of songs that can be used. " "The list of Exclude rules will be used to build a set of songs that cannot be used. " "If there are no Include rules, Cantata will assume that all songs (bar those from Exclude) can be used.

" "

e.g. to have Cantata look for 'Rock songs by Wibble OR songs by Various Artists', you would need the following: " "

  • Include AlbumArtist=Wibble Genre=Rock
  • Include AlbumArtist=Various Artists
" "To have Cantata look for 'Songs by Wibble but not from album Abc', you would need the following: " "
  • Include AlbumArtist=Wibble
  • Exclude AlbumArtist=Wibble Album=Abc
" "After the set of usable songs has been created, Cantata will add the desired number of songs to " "the play queue. If a range of ratings has been specified, then " "only songs with a rating within this range will be used. Likewise, if a duration has been set.

") ); } } void PlaylistRulesDialog::setOrder() { orderAscending->setEnabled(RulesPlaylists::Order_Random!=order->currentData().toInt()); if (RulesPlaylists::Order_Rating==order->currentData().toInt()) { orderAscending->setCurrentIndex(1); } } void PlaylistRulesDialog::ratingChanged(int value) { Q_UNUSED(value); includeUnrated->setEnabled(ratingFrom->value()>0 || ratingTo->value()>0); } void PlaylistRulesDialog::saved(bool s) { if (s) { accept(); } else { messageWidget->setError(tr("Failed to save %1").arg(nameText->text().trimmed())); controls->setEnabled(true); } } bool PlaylistRulesDialog::save() { if (!controls->isEnabled()) { return false; } QString name=nameText->text().trimmed(); if (name.isEmpty()) { return false; } if (name!=origName && rules->exists(name) && MessageBox::No==MessageBox::warningYesNo(this, tr("A set of rules named '%1' already exists!\n\nOverwrite?").arg(name), tr("Overwrite Rules"), StdGuiItem::overwrite(), StdGuiItem::cancel())) { return false; } RulesPlaylists::Entry entry; entry.name=name; int from=ratingFrom->value(); int to=ratingTo->value(); entry.ratingFrom=qMin(from, to); entry.ratingTo=qMax(from, to); entry.includeUnrated=includeUnrated->isChecked(); if (order) { entry.order=(RulesPlaylists::Order)order->currentData().toInt(); entry.orderAscending=0==orderAscending->currentIndex(); } entry.maxAge=maxAge->value(); from=minDuration->value(); to=maxDuration->value(); if (to>0) { entry.minDuration=qMin(from, to); entry.maxDuration=qMax(from, to); } else { entry.minDuration=from; entry.maxDuration=to; } entry.numTracks=numTracks->value(); for (int i=0; irowCount(); ++i) { QStandardItem *itm=model->item(i); if (itm) { QMap v=itm->data().toMap(); QMap::ConstIterator it(v.constBegin()); QMap::ConstIterator end(v.constEnd()); RulesPlaylists::Rule rule; for (; it!=end; ++it) { rule.insert(it.key(), it.value().toString()); } entry.rules.append(rule); } } bool saved=rules->save(entry); if (rules->isRemote()) { if (saved) { messageWidget->setInformation(tr("Saving %1").arg(name)); controls->setEnabled(false); enableButton(Ok, false); } return false; } else { if (saved && !origName.isEmpty() && entry.name!=origName) { rules->del(origName); } return saved; } } int PlaylistRulesDialog::indexOf(QStandardItem *item, bool diff) { QMap v=item->data().toMap(); for (int i=0; irowCount(); ++i) { QStandardItem *itm=model->item(i); if (itm) { if (itm->data().toMap()==v && (!diff || itm!=item)) { return i; } } } return -1; } #include "moc_playlistrulesdialog.cpp"