Clementine-audio-player-Mac.../src/playlist/playlistlistcontainer.cpp

520 lines
16 KiB
C++

/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Clementine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "playlistlistcontainer.h"
#include <QContextMenuEvent>
#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <iostream>
#include "core/application.h"
#include "core/logging.h"
#include "core/player.h"
#include "playlist.h"
#include "playlistlistmodel.h"
#include "playlistmanager.h"
#include "ui/iconloader.h"
#include "ui_playlistlistcontainer.h"
/* This filter proxy will:
- Accept all ancestors if at least a single child matches
- Accept all children if at least a single ancestor matches
The tree is then expanded only to the level at which the match occurs
*/
class PlaylistListFilterProxyModel : public QSortFilterProxyModel {
public:
explicit PlaylistListFilterProxyModel(QObject* parent)
: QSortFilterProxyModel(parent) {}
bool lessThan(const QModelIndex& left, const QModelIndex& right) const {
// Compare the display text first.
const int ret =
left.data().toString().localeAwareCompare(right.data().toString());
if (ret < 0) return true;
if (ret > 0) return false;
// Now use the source model row order to ensure we always get a
// deterministic sorting even when two items are named the same.
return left.row() < right.row();
}
QList<QModelIndex> expandList;
void setFilterRegExp(const QRegExp& regExp) {
expandList.clear();
QSortFilterProxyModel::setFilterRegExp(regExp);
}
void refreshExpanded(QTreeView* tree) {
tree->collapseAll();
// The call to setExpanded causes expendList to be appended via
// the filterAcceptsRow overload. Since QList's implementation is an array,
// when the reserved space is exhausted, it must be reallocated. When this
// happens, iterators are likely to break. But because of this implemention,
// indexing is safe and efficient.
for (int i = 0; i < expandList.count(); i++) {
const QModelIndex& sourceIndex = expandList[i];
QModelIndex mappedIndex = mapFromSource(sourceIndex);
tree->setExpanded(mappedIndex, true);
}
}
// Depth first search of all the items
bool hasAcceptedChildren(int source_row,
const QModelIndex& source_parent) const {
QModelIndex item = sourceModel()->index(source_row, 0, source_parent);
if (!item.isValid()) {
return false;
}
// check if there are children
int childCount = item.model()->rowCount(item);
if (childCount == 0) return false;
for (int i = 0; i < childCount; ++i) {
if (filterAcceptsRowItself(i, item)) return true;
if (hasAcceptedChildren(i, item)) return true;
}
return false;
}
bool filterAcceptsRowItself(int source_row,
const QModelIndex& source_parent) const {
bool rv =
QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
if (rv) {
if (sourceModel()->hasIndex(source_row, 0, source_parent)) {
QModelIndex idx = sourceModel()->index(source_row, 0, source_parent);
// Bit of a hack to get around the const in this function
auto* me = const_cast<PlaylistListFilterProxyModel*>(this);
QModelIndex pidx = sourceModel()->parent(idx);
while (pidx.isValid()) {
me->expandList.append(pidx);
pidx = sourceModel()->parent(pidx);
}
}
}
return rv;
}
bool filterAcceptsRow(int source_row,
const QModelIndex& source_parent) const {
if (filterAcceptsRowItself(source_row, source_parent)) {
return true;
}
// accept if any of the parents is accepted on it's own merits
QModelIndex parent = source_parent;
while (parent.isValid()) {
if (filterAcceptsRowItself(parent.row(), parent.parent())) return true;
parent = parent.parent();
}
// accept if any of the children is accepted on it's own merits
return hasAcceptedChildren(source_row, source_parent);
}
};
PlaylistListContainer::PlaylistListContainer(QWidget* parent)
: QWidget(parent),
app_(nullptr),
ui_(new Ui_PlaylistListContainer),
menu_(nullptr),
action_new_folder_(new QAction(this)),
action_remove_(new QAction(this)),
action_save_playlist_(new QAction(this)),
model_(new PlaylistListModel(this)),
proxy_(new PlaylistListFilterProxyModel(this)),
loaded_icons_(false),
active_playlist_id_(-1) {
ui_->setupUi(this);
ui_->tree->setAttribute(Qt::WA_MacShowFocusRect, false);
ui_->tree->SetAutoOpen(false);
action_new_folder_->setText(tr("New folder"));
action_remove_->setText(tr("Delete"));
action_save_playlist_->setText(
tr("Save playlist", "Save playlist menu action."));
ui_->new_folder->setDefaultAction(action_new_folder_);
ui_->remove->setDefaultAction(action_remove_);
ui_->save_playlist->setDefaultAction(action_save_playlist_);
connect(action_new_folder_, SIGNAL(triggered()), SLOT(NewFolderClicked()));
connect(action_remove_, SIGNAL(triggered()), SLOT(DeleteClicked()));
connect(action_save_playlist_, SIGNAL(triggered()), SLOT(SavePlaylist()));
connect(model_, SIGNAL(PlaylistPathChanged(int, QString)),
SLOT(PlaylistPathChanged(int, QString)));
proxy_->setSourceModel(model_);
proxy_->setDynamicSortFilter(true);
proxy_->sort(0);
ui_->tree->setModel(proxy_);
connect(ui_->tree, SIGNAL(doubleClicked(QModelIndex)),
SLOT(ItemDoubleClicked(QModelIndex)));
model_->invisibleRootItem()->setData(PlaylistListModel::Type_Folder,
PlaylistListModel::Role_Type);
connect(ui_->search, SIGNAL(textChanged(QString)),
SLOT(SearchTextEdited(QString)));
}
PlaylistListContainer::~PlaylistListContainer() { delete ui_; }
void PlaylistListContainer::showEvent(QShowEvent* e) {
// Loading icons is expensive so only do it when the view is first opened
if (loaded_icons_) {
QWidget::showEvent(e);
return;
}
loaded_icons_ = true;
action_new_folder_->setIcon(IconLoader::Load("folder-new", IconLoader::Base));
action_remove_->setIcon(IconLoader::Load("edit-delete", IconLoader::Base));
action_save_playlist_->setIcon(
IconLoader::Load("document-save", IconLoader::Base));
model_->SetIcons(IconLoader::Load("view-media-playlist", IconLoader::Base),
IconLoader::Load("folder", IconLoader::Base));
// Apply these icons to items that have already been created.
RecursivelySetIcons(model_->invisibleRootItem());
QWidget::showEvent(e);
}
void PlaylistListContainer::RecursivelySetIcons(QStandardItem* parent) const {
for (int i = 0; i < parent->rowCount(); ++i) {
QStandardItem* child = parent->child(i);
switch (child->data(PlaylistListModel::Role_Type).toInt()) {
case PlaylistListModel::Type_Folder:
child->setIcon(model_->folder_icon());
RecursivelySetIcons(child);
break;
case PlaylistListModel::Type_Playlist:
child->setIcon(model_->playlist_icon());
break;
case PlaylistListModel::Type_Track:
child->setIcon(model_->track_icon());
break;
}
}
}
void PlaylistListContainer::SetApplication(Application* app) {
app_ = app;
PlaylistManager* manager = app_->playlist_manager();
Player* player = app_->player();
connect(manager, SIGNAL(PlaylistAdded(int, QString, bool)),
SLOT(AddPlaylist(int, QString, bool)));
connect(manager, SIGNAL(PlaylistFavorited(int, bool)),
SLOT(PlaylistFavoriteStateChanged(int, bool)));
connect(manager, SIGNAL(PlaylistRenamed(int, QString)),
SLOT(PlaylistRenamed(int, QString)));
connect(manager, SIGNAL(CurrentChanged(Playlist*)),
SLOT(CurrentChanged(Playlist*)));
connect(manager, SIGNAL(ActiveChanged(Playlist*)),
SLOT(ActiveChanged(Playlist*)));
connect(model_, SIGNAL(PlaylistRenamed(int, QString)), manager,
SLOT(Rename(int, QString)));
connect(player, SIGNAL(Paused()), SLOT(ActivePaused()));
connect(player, SIGNAL(Playing()), SLOT(ActivePlaying()));
connect(player, SIGNAL(Stopped()), SLOT(ActiveStopped()));
// Get all playlists, even ones that are hidden in the UI.
for (const PlaylistBackend::Playlist& p :
app->playlist_backend()->GetAllFavoritePlaylists()) {
AddPlaylist(p.id, p.name, true, &p.ui_path);
}
}
void PlaylistListContainer::NewFolderClicked() {
QString name = QInputDialog::getText(this, tr("New folder"),
tr("Enter the name of the folder"));
if (name.isEmpty()) {
return;
}
name.replace("/", " ");
model_->invisibleRootItem()->appendRow(model_->NewFolder(name));
}
void PlaylistListContainer::AddPlaylist(int id, const QString& name,
bool favorite, const QString* ui_path) {
if (!favorite) {
return;
}
if (model_->PlaylistById(id)) {
// We know about this playlist already - it was probably one of the open
// ones that was loaded on startup.
return;
}
if (ui_path == nullptr)
ui_path = &app_->playlist_manager()->playlist(id)->ui_path();
QStandardItem* playlist_item = model_->NewPlaylist(name, id);
QStandardItem* parent_folder = model_->FolderByPath(*ui_path);
parent_folder->appendRow(playlist_item);
for (const Song& s : app_->playlist_backend()->GetPlaylistSongs(id)) {
QStandardItem* track_item = model_->NewTrack(s);
track_item->setDragEnabled(false);
playlist_item->appendRow(track_item);
}
}
void PlaylistListContainer::PlaylistRenamed(int id, const QString& new_name) {
QStandardItem* item = model_->PlaylistById(id);
if (!item) {
return;
}
item->setText(new_name);
}
void PlaylistListContainer::RemovePlaylist(int id) {
QStandardItem* item = model_->PlaylistById(id);
if (item) {
QStandardItem* parent = item->parent();
if (!parent) {
parent = model_->invisibleRootItem();
}
parent->removeRow(item->row());
}
}
void PlaylistListContainer::SavePlaylist() {
const QModelIndex& current_index =
proxy_->mapToSource(ui_->tree->currentIndex());
// Is it a playlist?
if (current_index.data(PlaylistListModel::Role_Type).toInt() ==
PlaylistListModel::Type_Playlist) {
const int playlist_id =
current_index.data(PlaylistListModel::Role_PlaylistId).toInt();
QStandardItem* item = model_->PlaylistById(playlist_id);
QString playlist_name = item ? item->text() : tr("Playlist");
app_->playlist_manager()->SaveWithUI(playlist_id, playlist_name);
}
}
void PlaylistListContainer::PlaylistFavoriteStateChanged(int id,
bool favorite) {
if (favorite) {
const QString& name = app_->playlist_manager()->GetPlaylistName(id);
AddPlaylist(id, name, favorite);
} else {
RemovePlaylist(id);
}
}
void PlaylistListContainer::ActiveChanged(Playlist* new_playlist) {
const int new_id = new_playlist->id();
if (new_id != active_playlist_id_) {
UpdateActiveIcon(active_playlist_id_, QIcon());
}
active_playlist_id_ = new_id;
}
void PlaylistListContainer::CurrentChanged(Playlist* new_playlist) {
if (!new_playlist) {
return;
}
// Focus this playlist in the tree
QStandardItem* item = model_->PlaylistById(new_playlist->id());
if (!item) {
return;
}
QModelIndex index = proxy_->mapFromSource(item->index());
ui_->tree->selectionModel()->setCurrentIndex(
index, QItemSelectionModel::ClearAndSelect);
ui_->tree->scrollTo(index);
}
void PlaylistListContainer::SearchTextEdited(const QString& text) {
QRegExp regexp(text);
regexp.setCaseSensitivity(Qt::CaseInsensitive);
proxy_->setFilterRegExp(regexp);
if (regexp.isEmpty()) {
ui_->tree->collapseAll();
} else {
proxy_->refreshExpanded(ui_->tree);
}
}
void PlaylistListContainer::PlaylistPathChanged(int id,
const QString& new_path) {
// Update the path in the database
app_->playlist_backend()->SetPlaylistUiPath(id, new_path);
Playlist* playlist = app_->playlist_manager()->playlist(id);
// Check the playlist exists (if it's not opened it's not in the manager)
if (playlist) {
playlist->set_ui_path(new_path);
}
}
void PlaylistListContainer::ItemDoubleClicked(const QModelIndex& proxy_index) {
const QModelIndex& index = proxy_->mapToSource(proxy_index);
// Is it a playlist?
if (index.data(PlaylistListModel::Role_Type).toInt() ==
PlaylistListModel::Type_Playlist) {
app_->playlist_manager()->SetCurrentOrOpen(
index.data(PlaylistListModel::Role_PlaylistId).toInt());
}
}
void PlaylistListContainer::DeleteClicked() {
QSet<int> ids;
QList<QPersistentModelIndex> folders_to_delete;
for (const QModelIndex& proxy_index :
ui_->tree->selectionModel()->selectedRows(0)) {
const QModelIndex& index = proxy_->mapToSource(proxy_index);
// Is it a playlist?
switch (index.data(PlaylistListModel::Role_Type).toInt()) {
case PlaylistListModel::Type_Playlist:
ids << index.data(PlaylistListModel::Role_PlaylistId).toInt();
break;
case PlaylistListModel::Type_Folder:
// Find all the playlists inside.
RecursivelyFindPlaylists(index, &ids);
folders_to_delete << index;
break;
}
}
// Make sure the user really wants to unfavorite all these playlists.
if (ids.count() > 1) {
const int button = QMessageBox::question(
this, tr("Remove playlists"),
tr("You are about to remove %1 playlists from your favorites, are you "
"sure?")
.arg(ids.count()),
QMessageBox::Yes, QMessageBox::Cancel);
if (button != QMessageBox::Yes) {
return;
}
}
// Unfavorite the playlists
for (int id : ids) {
app_->playlist_manager()->Favorite(id, false);
}
// Delete the top-level folders.
for (const QPersistentModelIndex& index : folders_to_delete) {
if (index.isValid()) {
model_->removeRow(index.row(), index.parent());
}
}
}
void PlaylistListContainer::RecursivelyFindPlaylists(const QModelIndex& parent,
QSet<int>* ids) const {
switch (parent.data(PlaylistListModel::Role_Type).toInt()) {
case PlaylistListModel::Type_Playlist:
ids->insert(parent.data(PlaylistListModel::Role_PlaylistId).toInt());
break;
case PlaylistListModel::Type_Folder:
for (int i = 0; i < parent.model()->rowCount(parent); ++i) {
RecursivelyFindPlaylists(parent.model()->index(i, 0, parent), ids);
}
break;
}
}
void PlaylistListContainer::contextMenuEvent(QContextMenuEvent* e) {
if (!menu_) {
menu_ = new QMenu(this);
menu_->addAction(action_new_folder_);
menu_->addAction(action_remove_);
menu_->addSeparator();
menu_->addAction(action_save_playlist_);
}
menu_->popup(e->globalPos());
}
void PlaylistListContainer::ActivePlaying() {
if (padded_play_icon_.isNull()) {
QPixmap pixmap(":tiny-start.png");
QPixmap new_pixmap(QSize(pixmap.height(), pixmap.height()));
new_pixmap.fill(Qt::transparent);
QPainter p(&new_pixmap);
p.drawPixmap((new_pixmap.width() - pixmap.width()) / 2, 0, pixmap.width(),
pixmap.height(), pixmap);
p.end();
padded_play_icon_.addPixmap(new_pixmap);
}
UpdateActiveIcon(active_playlist_id_, padded_play_icon_);
}
void PlaylistListContainer::ActivePaused() {
UpdateActiveIcon(active_playlist_id_, QIcon(":tiny-pause.png"));
}
void PlaylistListContainer::ActiveStopped() {
UpdateActiveIcon(active_playlist_id_, QIcon());
}
void PlaylistListContainer::UpdateActiveIcon(int id, const QIcon& icon) {
if (id == -1) {
return;
}
QStandardItem* item = model_->PlaylistById(id);
if (!item) {
return;
}
if (icon.isNull()) {
item->setIcon(model_->playlist_icon());
} else {
item->setIcon(icon);
}
}