Initial commit.

This commit is contained in:
Jonas Kvinge
2018-02-27 18:06:05 +01:00
parent 85d9664df7
commit b2b1ba7abe
1393 changed files with 177311 additions and 1 deletions

1895
src/playlist/playlist.cpp Normal file

File diff suppressed because it is too large Load Diff

373
src/playlist/playlist.h Normal file
View File

@@ -0,0 +1,373 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLIST_H
#define PLAYLIST_H
#include "config.h"
#include <QAbstractItemModel>
#include <QList>
#include "playlistitem.h"
#include "playlistsequence.h"
#include "core/tagreaderclient.h"
#include "core/song.h"
class CollectionBackend;
class PlaylistBackend;
class PlaylistFilter;
class Queue;
class TaskManager;
class QSortFilterProxyModel;
class QUndoStack;
namespace PlaylistUndoCommands {
class InsertItems;
class RemoveItems;
class MoveItems;
class ReOrderItems;
class SortItems;
class ShuffleItems;
}
typedef QMap<int, Qt::Alignment> ColumnAlignmentMap;
Q_DECLARE_METATYPE(Qt::Alignment);
Q_DECLARE_METATYPE(ColumnAlignmentMap);
// Objects that may prevent a song being added to the playlist. When there
// is something about to be inserted into it, Playlist notifies all of it's
// listeners about the fact and every one of them picks 'invalid' songs.
class SongInsertVetoListener : public QObject {
Q_OBJECT
public:
// Listener returns a list of 'invalid' songs. 'old_songs' are songs that are
// currently in the playlist and 'new_songs' are the songs about to be added if
// nobody exercises a veto.
virtual SongList AboutToInsertSongs(const SongList& old_songs, const SongList& new_songs) = 0;
};
class Playlist : public QAbstractListModel {
Q_OBJECT
friend class PlaylistUndoCommands::InsertItems;
friend class PlaylistUndoCommands::RemoveItems;
friend class PlaylistUndoCommands::MoveItems;
friend class PlaylistUndoCommands::ReOrderItems;
public:
Playlist(PlaylistBackend *backend, TaskManager *task_manager, CollectionBackend *collection, int id, const QString& special_type = QString(), bool favorite = false, QObject *parent = nullptr);
~Playlist();
void SkipTracks(const QModelIndexList& source_indexes);
// Always add new columns to the end of this enum - the values are persisted
enum Column {
Column_Title = 0,
Column_Artist,
Column_Album,
Column_AlbumArtist,
Column_Performer,
Column_Composer,
Column_Year,
Column_OriginalYear,
Column_Track,
Column_Disc,
Column_Length,
Column_Genre,
Column_Samplerate,
Column_Bitdepth,
Column_SamplerateBitdepth,
Column_Bitrate,
Column_Filename,
Column_BaseFilename,
Column_Filesize,
Column_Filetype,
Column_DateCreated,
Column_DateModified,
Column_PlayCount,
Column_SkipCount,
Column_LastPlayed,
Column_Comment,
Column_Grouping,
ColumnCount
};
enum Role {
Role_IsCurrent = Qt::UserRole + 1,
Role_IsPaused,
Role_StopAfter,
Role_QueuePosition
};
enum Path {
Path_Automatic = 0, // Automatically select path type
Path_Absolute, // Always use absolute paths
Path_Relative, // Always use relative paths
Path_Ask_User, // Only used in preferences: to ask user which of the
// previous values he wants to use.
};
static const char *kCddaMimeType;
static const char *kRowsMimetype;
static const char *kPlayNowMimetype;
static const int kInvalidSongPriority;
static const QRgb kInvalidSongColor;
static const int kDynamicHistoryPriority;
static const QRgb kDynamicHistoryColor;
static const char *kSettingsGroup;
static const char *kPathType;
static const char *kWriteMetadata;
static const int kUndoStackSize;
static const int kUndoItemLimit;
static bool CompareItems(int column, Qt::SortOrder order, PlaylistItemPtr a, PlaylistItemPtr b);
static QString column_name(Column column);
static QString abbreviated_column_name(Column column);
static bool column_is_editable(Playlist::Column column);
static bool set_column_value(Song& song, Column column, const QVariant& value);
// Persistence
void Save() const;
void Restore();
// Accessors
QSortFilterProxyModel *proxy() const;
Queue *queue() const { return queue_; }
int id() const { return id_; }
const QString& ui_path() const { return ui_path_; }
void set_ui_path(const QString &path) { ui_path_ = path; }
bool is_favorite() const { return favorite_; }
void set_favorite(bool favorite) { favorite_ = favorite; }
int current_row() const;
int last_played_row() const;
int next_row(bool ignore_repeat_track = false) const;
int previous_row(bool ignore_repeat_track = false) const;
const QModelIndex current_index() const;
bool stop_after_current() const;
QString special_type() const { return special_type_; }
void set_special_type(const QString &v) { special_type_ = v; }
const PlaylistItemPtr &item_at(int index) const { return items_[index]; }
const bool has_item_at(int index) const { return index >= 0 && index < rowCount(); }
PlaylistItemPtr current_item() const;
PlaylistItem::Options current_item_options() const;
Song current_item_metadata() const;
PlaylistItemList collection_items_by_id(int id) const;
SongList GetAllSongs() const;
PlaylistItemList GetAllItems() const;
quint64 GetTotalLength() const; // in seconds
void set_sequence(PlaylistSequence *v);
PlaylistSequence *sequence() const { return playlist_sequence_; }
QUndoStack *undo_stack() const { return undo_stack_; }
// Changing the playlist
void InsertItems (const PlaylistItemList &items, int pos = -1, bool play_now = false, bool enqueue = false);
void InsertCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false);
void InsertSongs (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false);
void InsertSongsOrCollectionItems (const SongList &items, int pos = -1, bool play_now = false, bool enqueue = false);
void ReshuffleIndices();
// If this playlist contains the current item, this method will apply the "valid" flag on it.
// If the "valid" flag is false, the song will be greyed out. Otherwise the grey color will
// be undone.
// If the song is a local file and it's valid but non existent or invalid but exists, the
// song will be reloaded to even out the situation because obviously something has changed.
// This returns true if this playlist had current item when the method was invoked.
bool ApplyValidityOnCurrentSong(const QUrl &url, bool valid);
// Grays out and reloads all deleted songs in all playlists. Also, "ungreys" those songs
// which were once deleted but now got restored somehow.
void InvalidateDeletedSongs();
// Removes from the playlist all local files that don't exist anymore.
void RemoveDeletedSongs();
void StopAfter(int row);
void ReloadItems(const QList<int> &rows);
void InformOfCurrentSongChange();
// Registers an object which will get notifications when new songs
// are about to be inserted into this playlist.
void AddSongInsertVetoListener(SongInsertVetoListener *listener);
// Unregisters a SongInsertVetoListener object.
void RemoveSongInsertVetoListener(SongInsertVetoListener *listener);
// QAbstractListModel
int rowCount(const QModelIndex& = QModelIndex()) const { return items_.count(); }
int columnCount(const QModelIndex& = QModelIndex()) const { return ColumnCount; }
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
bool setData(const QModelIndex &index, const QVariant &value, int role);
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const;
Qt::ItemFlags flags(const QModelIndex &index) const;
QStringList mimeTypes() const;
Qt::DropActions supportedDropActions() const;
QMimeData *mimeData(const QModelIndexList &indexes) const;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
void sort(int column, Qt::SortOrder order);
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());
static bool ComparePathDepths(Qt::SortOrder, PlaylistItemPtr, PlaylistItemPtr);
public slots:
void set_current_row(int index, bool is_stopping = false);
void Paused();
void Playing();
void Stopped();
void IgnoreSorting(bool value) { ignore_sorting_ = value; }
void ClearStreamMetadata();
void SetStreamMetadata(const QUrl &url, const Song &song);
void ItemChanged(PlaylistItemPtr item);
void UpdateItems(const SongList &songs);
void Clear();
void RemoveDuplicateSongs();
void RemoveUnavailableSongs();
void Shuffle();
void ShuffleModeChanged(PlaylistSequence::ShuffleMode mode);
void SetColumnAlignment(const ColumnAlignmentMap &alignment);
void InsertUrls(const QList<QUrl> &urls, int pos = -1, bool play_now = false, bool enqueue = false);
// Removes items with given indices from the playlist. This operation is not undoable.
void RemoveItemsWithoutUndo(const QList<int> &indices);
signals:
void RestoreFinished();
void CurrentSongChanged(const Song &metadata);
void EditingFinished(const QModelIndex &index);
void PlayRequested(const QModelIndex &index);
// Signals that the underlying list of items was changed, meaning that
// something was added to it, removed from it or the ordering changed.
void PlaylistChanged();
void DynamicModeChanged(bool dynamic);
void Error(const QString &message);
// Signals that the queue has changed, meaning that the remaining queued
// items should update their position.
void QueueChanged();
private:
void SetCurrentIsPaused(bool paused);
int NextVirtualIndex(int i, bool ignore_repeat_track) const;
int PreviousVirtualIndex(int i, bool ignore_repeat_track) const;
bool FilterContainsVirtualIndex(int i) const;
template <typename T>
void InsertSongItems(const SongList &songs, int pos, bool play_now, bool enqueue);
// Modify the playlist without changing the undo stack. These are used by
// our friends in PlaylistUndoCommands
void InsertItemsWithoutUndo(const PlaylistItemList &items, int pos, bool enqueue = false);
PlaylistItemList RemoveItemsWithoutUndo(int pos, int count);
void MoveItemsWithoutUndo(const QList<int> &source_rows, int pos);
void MoveItemWithoutUndo(int source, int dest);
void MoveItemsWithoutUndo(int start, const QList<int> &dest_rows);
void ReOrderWithoutUndo(const PlaylistItemList &new_items);
void RemoveItemsNotInQueue();
// Removes rows with given indices from this playlist.
bool removeRows(QList<int> &rows);
private slots:
void TracksAboutToBeDequeued(const QModelIndex&, int begin, int end);
void TracksDequeued();
void TracksEnqueued(const QModelIndex&, int begin, int end);
void QueueLayoutChanged();
void SongSaveComplete(TagReaderReply *reply, const QPersistentModelIndex &index);
void ItemReloadComplete(const QPersistentModelIndex &index);
void ItemsLoaded(QFuture<PlaylistItemList> future);
void SongInsertVetoListenerDestroyed();
private:
bool is_loading_;
PlaylistFilter *proxy_;
Queue *queue_;
QList<QModelIndex> temp_dequeue_change_indexes_;
PlaylistBackend *backend_;
TaskManager *task_manager_;
CollectionBackend *collection_;
int id_;
QString ui_path_;
bool favorite_;
PlaylistItemList items_;
QList<int> virtual_items_; // Contains the indices into items_ in the order
// that they will be played.
// A map of collection ID to playlist item - for fast lookups when collection
// items change.
QMultiMap<int, PlaylistItemPtr> collection_items_by_id_;
QPersistentModelIndex current_item_index_;
QPersistentModelIndex last_played_item_index_;
QPersistentModelIndex stop_after_;
bool current_is_paused_;
int current_virtual_index_;
bool is_shuffled_;
PlaylistSequence *playlist_sequence_;
// Hack to stop QTreeView::setModel sorting the playlist
bool ignore_sorting_;
QUndoStack *undo_stack_;
ColumnAlignmentMap column_alignments_;
QList<SongInsertVetoListener*> veto_listeners_;
QString special_type_;
// Cancel async restore if songs are already replaced
bool cancel_restore_;
};
// QDataStream& operator <<(QDataStream&, const Playlist*);
// QDataStream& operator >>(QDataStream&, Playlist*&);
#endif // PLAYLIST_H

View File

@@ -0,0 +1,449 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistbackend.h"
#include <memory>
#include <functional>
#include <QFile>
#include <QHash>
#include <QMutexLocker>
#include <QSqlQuery>
#include <QtDebug>
#include "core/application.h"
#include "core/database.h"
#include "core/logging.h"
#include "core/scopedtransaction.h"
#include "core/song.h"
#include "collection/collectionbackend.h"
#include "collection/sqlrow.h"
#include "playlist/songplaylistitem.h"
#include "playlistparsers/cueparser.h"
using std::placeholders::_1;
using std::shared_ptr;
const int PlaylistBackend::kSongTableJoins = 2;
PlaylistBackend::PlaylistBackend(Application* app, QObject* parent)
: QObject(parent), app_(app), db_(app_->database()) {}
PlaylistBackend::PlaylistList PlaylistBackend::GetAllPlaylists() {
return GetPlaylists(GetPlaylists_All);
}
PlaylistBackend::PlaylistList PlaylistBackend::GetAllOpenPlaylists() {
return GetPlaylists(GetPlaylists_OpenInUi);
}
PlaylistBackend::PlaylistList PlaylistBackend::GetAllFavoritePlaylists() {
return GetPlaylists(GetPlaylists_Favorite);
}
PlaylistBackend::PlaylistList PlaylistBackend::GetPlaylists(GetPlaylistsFlags flags) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
PlaylistList ret;
QStringList condition_list;
if (flags & GetPlaylists_OpenInUi) {
condition_list << "ui_order != -1";
}
if (flags & GetPlaylists_Favorite) {
condition_list << "is_favorite != 0";
}
QString condition;
if (!condition_list.isEmpty()) {
condition = " WHERE " + condition_list.join(" OR ");
}
QSqlQuery q(db);
q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite FROM playlists " + condition + " ORDER BY ui_order");
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
Playlist p;
p.id = q.value(0).toInt();
p.name = q.value(1).toString();
p.last_played = q.value(2).toInt();
p.special_type = q.value(3).toString();
p.ui_path = q.value(4).toString();
p.favorite = q.value(5).toBool();
ret << p;
}
return ret;
}
PlaylistBackend::Playlist PlaylistBackend::GetPlaylist(int id) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, name, last_played, special_type, ui_path, is_favorite FROM playlists WHERE ROWID=:id");
q.bindValue(":id", id);
q.exec();
if (db_->CheckErrors(q)) return Playlist();
q.next();
Playlist p;
p.id = q.value(0).toInt();
p.name = q.value(1).toString();
p.last_played = q.value(2).toInt();
p.special_type = q.value(3).toString();
p.ui_path = q.value(4).toString();
p.favorite = q.value(5).toBool();
return p;
}
QSqlQuery PlaylistBackend::GetPlaylistRows(int playlist) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QString query = "SELECT songs.ROWID, " + Song::JoinSpec("songs") +
","
" p.ROWID, " +
Song::JoinSpec("p") +
","
" p.type"
" FROM playlist_items AS p"
" LEFT JOIN songs"
" ON p.collection_id = songs.ROWID"
" WHERE p.playlist = :playlist";
QSqlQuery q(db);
// Forward iterations only may be faster
q.setForwardOnly(true);
q.prepare(query);
q.bindValue(":playlist", playlist);
q.exec();
return q;
}
QList<PlaylistItemPtr> PlaylistBackend::GetPlaylistItems(int playlist) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QSqlQuery q = GetPlaylistRows(playlist);
// Note that as this only accesses the query, not the db, we don't need the
// mutex.
if (db_->CheckErrors(q)) return QList<PlaylistItemPtr>();
// it's probable that we'll have a few songs associated with the
// same CUE so we're caching results of parsing CUEs
std::shared_ptr<NewSongFromQueryState> state_ptr(new NewSongFromQueryState());
QList<PlaylistItemPtr> playlistitems;
while (q.next()) {
playlistitems << NewPlaylistItemFromQuery(SqlRow(q), state_ptr);
}
return playlistitems;
}
QList<Song> PlaylistBackend::GetPlaylistSongs(int playlist) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QSqlQuery q = GetPlaylistRows(playlist);
// Note that as this only accesses the query, not the db, we don't need the
// mutex.
if (db_->CheckErrors(q)) return QList<Song>();
// it's probable that we'll have a few songs associated with the
// same CUE so we're caching results of parsing CUEs
std::shared_ptr<NewSongFromQueryState> state_ptr(new NewSongFromQueryState());
QList<Song> songs;
while (q.next()) {
songs << NewSongFromQuery(SqlRow(q), state_ptr);
}
return songs;
}
PlaylistItemPtr PlaylistBackend::NewPlaylistItemFromQuery(const SqlRow &row, std::shared_ptr<NewSongFromQueryState> state) {
//qLog(Debug) << __PRETTY_FUNCTION__;
// The song tables get joined first, plus one each for the song ROWIDs
const int playlist_row = (Song::kColumns.count() + 1) * kSongTableJoins;
//qLog(Debug) << row.value(playlist_row).toString();
PlaylistItemPtr item(PlaylistItem::NewFromType(row.value(playlist_row).toString()));
if (item) {
item->InitFromQuery(row);
return RestoreCueData(item, state);
}
else {
return item;
}
}
Song PlaylistBackend::NewSongFromQuery(const SqlRow &row, std::shared_ptr<NewSongFromQueryState> state) {
//qLog(Debug) << __PRETTY_FUNCTION__;
return NewPlaylistItemFromQuery(row, state)->Metadata();
}
// If song had a CUE and the CUE still exists, the metadata from it will be applied here.
PlaylistItemPtr PlaylistBackend::RestoreCueData(PlaylistItemPtr item, std::shared_ptr<NewSongFromQueryState> state) {
//qLog(Debug) << __PRETTY_FUNCTION__;
// we need collection to run a CueParser; also, this method applies only to file-type PlaylistItems
if (item->type() != "File") return item;
CueParser cue_parser(app_->collection_backend());
Song song = item->Metadata();
// we're only interested in .cue songs here
if (!song.has_cue()) return item;
QString cue_path = song.cue_path();
// if .cue was deleted - reload the song
if (!QFile::exists(cue_path)) {
item->Reload();
return item;
}
SongList song_list;
{
QMutexLocker locker(&state->mutex_);
if (!state->cached_cues_.contains(cue_path)) {
QFile cue(cue_path);
cue.open(QIODevice::ReadOnly);
song_list = cue_parser.Load(&cue, cue_path, QDir(cue_path.section('/', 0, -2)));
state->cached_cues_[cue_path] = song_list;
} else {
song_list = state->cached_cues_[cue_path];
}
}
for (const Song& from_list : song_list) {
if (from_list.url().toEncoded() == song.url().toEncoded() &&
from_list.beginning_nanosec() == song.beginning_nanosec()) {
// we found a matching section; replace the input
// item with a new one containing CUE metadata
return PlaylistItemPtr(new SongPlaylistItem(from_list));
}
}
// there's no such section in the related .cue -> reload the song
item->Reload();
return item;
}
void PlaylistBackend::SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played) {
//qLog(Debug) << __PRETTY_FUNCTION__;
metaObject()->invokeMethod(this, "SavePlaylist", Qt::QueuedConnection, Q_ARG(int, playlist), Q_ARG(PlaylistItemList, items), Q_ARG(int, last_played));
}
void PlaylistBackend::SavePlaylist(int playlist, const PlaylistItemList& items, int last_played) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
qLog(Debug) << "Saving playlist" << playlist;
QSqlQuery clear(db);
clear.prepare("DELETE FROM playlist_items WHERE playlist = :playlist");
QSqlQuery insert(db);
insert.prepare("INSERT INTO playlist_items (playlist, type, collection_id, " + Song::kColumnSpec + ") VALUES (:playlist, :type, :collection_id, " + Song::kBindSpec + ")");
QSqlQuery update(db);
update.prepare("UPDATE playlists SET last_played=:last_played WHERE ROWID=:playlist");
ScopedTransaction transaction(&db);
// Clear the existing items in the playlist
clear.bindValue(":playlist", playlist);
clear.exec();
if (db_->CheckErrors(clear)) return;
// Save the new ones
for (PlaylistItemPtr item : items) {
insert.bindValue(":playlist", playlist);
item->BindToQuery(&insert);
insert.exec();
db_->CheckErrors(insert);
}
// Update the last played track number
update.bindValue(":last_played", last_played);
update.bindValue(":playlist", playlist);
update.exec();
if (db_->CheckErrors(update)) return;
transaction.Commit();
}
int PlaylistBackend::CreatePlaylist(const QString& name, const QString& special_type) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("INSERT INTO playlists (name, special_type) VALUES (:name, :special_type)");
q.bindValue(":name", name);
q.bindValue(":special_type", special_type);
q.exec();
if (db_->CheckErrors(q)) return -1;
return q.lastInsertId().toInt();
}
void PlaylistBackend::RemovePlaylist(int id) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery delete_playlist(db);
delete_playlist.prepare("DELETE FROM playlists WHERE ROWID=:id");
QSqlQuery delete_items(db);
delete_items.prepare("DELETE FROM playlist_items WHERE playlist=:id");
delete_playlist.bindValue(":id", id);
delete_items.bindValue(":id", id);
ScopedTransaction transaction(&db);
delete_playlist.exec();
if (db_->CheckErrors(delete_playlist)) return;
delete_items.exec();
if (db_->CheckErrors(delete_items)) return;
transaction.Commit();
}
void PlaylistBackend::RenamePlaylist(int id, const QString &new_name) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("UPDATE playlists SET name=:name WHERE ROWID=:id");
q.bindValue(":name", new_name);
q.bindValue(":id", id);
q.exec();
db_->CheckErrors(q);
}
void PlaylistBackend::FavoritePlaylist(int id, bool is_favorite) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("UPDATE playlists SET is_favorite=:is_favorite WHERE ROWID=:id");
q.bindValue(":is_favorite", is_favorite ? 1 : 0);
q.bindValue(":id", id);
q.exec();
db_->CheckErrors(q);
}
void PlaylistBackend::SetPlaylistOrder(const QList<int> &ids) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction transaction(&db);
QSqlQuery q(db);
q.prepare("UPDATE playlists SET ui_order=-1");
q.exec();
if (db_->CheckErrors(q)) return;
q.prepare("UPDATE playlists SET ui_order=:index WHERE ROWID=:id");
for (int i = 0; i < ids.count(); ++i) {
q.bindValue(":index", i);
q.bindValue(":id", ids[i]);
q.exec();
if (db_->CheckErrors(q)) return;
}
transaction.Commit();
}
void PlaylistBackend::SetPlaylistUiPath(int id, const QString &path) {
//qLog(Debug) << __PRETTY_FUNCTION__;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("UPDATE playlists SET ui_path=:path WHERE ROWID=:id");
ScopedTransaction transaction(&db);
q.bindValue(":path", path);
q.bindValue(":id", id);
q.exec();
if (db_->CheckErrors(q)) return;
transaction.Commit();
}

View File

@@ -0,0 +1,104 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTBACKEND_H
#define PLAYLISTBACKEND_H
#include "config.h"
#include <QHash>
#include <QList>
#include <QMutex>
#include <QObject>
#include "playlistitem.h"
class Application;
class Database;
class PlaylistBackend : public QObject {
Q_OBJECT
public:
Q_INVOKABLE PlaylistBackend(Application *app, QObject *parent = nullptr);
struct Playlist {
Playlist() : id(-1), favorite(false), last_played(0) {}
int id;
QString name;
QString ui_path;
bool favorite;
int last_played;
// Special playlists have different behaviour, eg. the "spotify-search"
// type has a spotify search box at the top, replacing the ordinary filter.
QString special_type;
};
typedef QList<Playlist> PlaylistList;
static const int kSongTableJoins;
PlaylistList GetAllPlaylists();
PlaylistList GetAllOpenPlaylists();
PlaylistList GetAllFavoritePlaylists();
PlaylistBackend::Playlist GetPlaylist(int id);
QList<PlaylistItemPtr> GetPlaylistItems(int playlist);
QList<Song> GetPlaylistSongs(int playlist);
void SetPlaylistOrder(const QList<int> &ids);
void SetPlaylistUiPath(int id, const QString &path);
int CreatePlaylist(const QString &name, const QString &special_type);
void SavePlaylistAsync(int playlist, const PlaylistItemList &items, int last_played);
void RenamePlaylist(int id, const QString &new_name);
void FavoritePlaylist(int id, bool is_favorite);
void RemovePlaylist(int id);
Application *app() const { return app_; }
public slots:
void SavePlaylist(int playlist, const PlaylistItemList &items, int last_played);
private:
struct NewSongFromQueryState {
QHash<QString, SongList> cached_cues_;
QMutex mutex_;
};
QSqlQuery GetPlaylistRows(int playlist);
Song NewSongFromQuery(const SqlRow &row, std::shared_ptr<NewSongFromQueryState> state);
PlaylistItemPtr NewPlaylistItemFromQuery(const SqlRow &row, std::shared_ptr<NewSongFromQueryState> state);
PlaylistItemPtr RestoreCueData(PlaylistItemPtr item, std::shared_ptr<NewSongFromQueryState> state);
enum GetPlaylistsFlags {
GetPlaylists_OpenInUi = 1,
GetPlaylists_Favorite = 2,
GetPlaylists_All = GetPlaylists_OpenInUi | GetPlaylists_Favorite
};
PlaylistList GetPlaylists(GetPlaylistsFlags flags);
Application *app_;
Database *db_;
};
#endif // PLAYLISTBACKEND_H

View File

@@ -0,0 +1,448 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistcontainer.h"
#include "playlistmanager.h"
#include "ui_playlistcontainer.h"
#include "core/logging.h"
#include "core/iconloader.h"
#include "playlistparsers/playlistparser.h"
#include <QFileDialog>
#include <QInputDialog>
#include <QKeyEvent>
#include <QLabel>
#include <QMessageBox>
#include <QSettings>
#include <QSortFilterProxyModel>
#include <QTimeLine>
#include <QTimer>
#include <QUndoStack>
const char *PlaylistContainer::kSettingsGroup = "Playlist";
const int PlaylistContainer::kFilterDelayMs = 100;
const int PlaylistContainer::kFilterDelayPlaylistSizeThreshold = 5000;
PlaylistContainer::PlaylistContainer(QWidget *parent)
: QWidget(parent),
ui_(new Ui_PlaylistContainer),
manager_(nullptr),
undo_(nullptr),
redo_(nullptr),
playlist_(nullptr),
starting_up_(true),
tab_bar_visible_(false),
tab_bar_animation_(new QTimeLine(500, this)),
no_matches_label_(nullptr),
filter_timer_(new QTimer(this)) {
ui_->setupUi(this);
no_matches_label_ = new QLabel(ui_->playlist);
no_matches_label_->setAlignment(Qt::AlignTop | Qt::AlignHCenter);
no_matches_label_->setAttribute(Qt::WA_TransparentForMouseEvents);
no_matches_label_->setWordWrap(true);
no_matches_label_->raise();
no_matches_label_->hide();
// Set the colour of the no matches label to the disabled text colour
QPalette no_matches_palette = no_matches_label_->palette();
const QColor no_matches_color = no_matches_palette.color(QPalette::Disabled, QPalette::Text);
no_matches_palette.setColor(QPalette::Normal, QPalette::WindowText, no_matches_color);
no_matches_palette.setColor(QPalette::Inactive, QPalette::WindowText, no_matches_color);
no_matches_label_->setPalette(no_matches_palette);
// Remove QFrame border
ui_->toolbar->setStyleSheet("QFrame { border: 0px; }");
// Make it bold
QFont no_matches_font = no_matches_label_->font();
no_matches_font.setBold(true);
no_matches_label_->setFont(no_matches_font);
settings_.beginGroup(kSettingsGroup);
// Tab bar
ui_->tab_bar->setExpanding(false);
ui_->tab_bar->setMovable(true);
connect(tab_bar_animation_, SIGNAL(frameChanged(int)), SLOT(SetTabBarHeight(int)));
ui_->tab_bar->setMaximumHeight(0);
// Connections
connect(ui_->tab_bar, SIGNAL(currentChanged(int)), SLOT(Save()));
connect(ui_->tab_bar, SIGNAL(Save(int)), SLOT(SavePlaylist(int)));
// set up timer for delayed filter updates
filter_timer_->setSingleShot(true);
filter_timer_->setInterval(kFilterDelayMs);
connect(filter_timer_, SIGNAL(timeout()), this, SLOT(UpdateFilter()));
// Replace playlist search filter with native search box.
connect(ui_->filter, SIGNAL(textChanged(QString)), SLOT(MaybeUpdateFilter()));
connect(ui_->playlist, SIGNAL(FocusOnFilterSignal(QKeyEvent*)), SLOT(FocusOnFilter(QKeyEvent*)));
ui_->filter->installEventFilter(this);
}
PlaylistContainer::~PlaylistContainer() { delete ui_; }
PlaylistView *PlaylistContainer::view() const { return ui_->playlist; }
void PlaylistContainer::SetActions(QAction *new_playlist, QAction *load_playlist, QAction *save_playlist, QAction *clear_playlist, QAction *next_playlist, QAction *previous_playlist) {
ui_->create_new->setDefaultAction(new_playlist);
ui_->load->setDefaultAction(load_playlist);
ui_->save->setDefaultAction(save_playlist);
ui_->clear->setDefaultAction(clear_playlist);
ui_->tab_bar->SetActions(new_playlist, load_playlist);
connect(new_playlist, SIGNAL(triggered()), SLOT(NewPlaylist()));
connect(save_playlist, SIGNAL(triggered()), SLOT(SavePlaylist()));
connect(load_playlist, SIGNAL(triggered()), SLOT(LoadPlaylist()));
connect(clear_playlist, SIGNAL(triggered()), SLOT(ClearPlaylist()));
connect(next_playlist, SIGNAL(triggered()), SLOT(GoToNextPlaylistTab()));
connect(previous_playlist, SIGNAL(triggered()), SLOT(GoToPreviousPlaylistTab()));
connect(clear_playlist, SIGNAL(triggered()), SLOT(ClearPlaylist()));
}
void PlaylistContainer::SetManager(PlaylistManager *manager) {
manager_ = manager;
ui_->tab_bar->SetManager(manager);
connect(ui_->tab_bar, SIGNAL(CurrentIdChanged(int)), manager, SLOT(SetCurrentPlaylist(int)));
connect(ui_->tab_bar, SIGNAL(Rename(int, QString)), manager, SLOT(Rename(int, QString)));
connect(ui_->tab_bar, SIGNAL(Close(int)), manager, SLOT(Close(int)));
connect(ui_->tab_bar, SIGNAL(PlaylistFavorited(int, bool)), manager, SLOT(Favorite(int, bool)));
connect(ui_->tab_bar, SIGNAL(PlaylistOrderChanged(QList<int>)), manager, SLOT(ChangePlaylistOrder(QList<int>)));
connect(manager, SIGNAL(CurrentChanged(Playlist*)), SLOT(SetViewModel(Playlist*)));
connect(manager, SIGNAL(PlaylistAdded(int, QString, bool)), SLOT(PlaylistAdded(int, QString, bool)));
connect(manager, SIGNAL(PlaylistManagerInitialized()), SLOT(Started()));
connect(manager, SIGNAL(PlaylistClosed(int)), SLOT(PlaylistClosed(int)));
connect(manager, SIGNAL(PlaylistRenamed(int, QString)), SLOT(PlaylistRenamed(int, QString)));
}
void PlaylistContainer::SetViewModel(Playlist *playlist) {
if (view()->selectionModel()) {
disconnect(view()->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(SelectionChanged()));
}
if (playlist_ && playlist_->proxy()) {
disconnect(playlist_->proxy(), SIGNAL(modelReset()), this, SLOT(UpdateNoMatchesLabel()));
disconnect(playlist_->proxy(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(UpdateNoMatchesLabel()));
disconnect(playlist_->proxy(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(UpdateNoMatchesLabel()));
}
if (playlist_) {
disconnect(playlist_, SIGNAL(modelReset()), this, SLOT(UpdateNoMatchesLabel()));
disconnect(playlist_, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(UpdateNoMatchesLabel()));
disconnect(playlist_, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(UpdateNoMatchesLabel()));
}
playlist_ = playlist;
// Set the view
playlist->IgnoreSorting(true);
view()->setModel(playlist->proxy());
view()->SetItemDelegates(manager_->collection_backend());
view()->SetPlaylist(playlist);
view()->selectionModel()->select(manager_->current_selection(), QItemSelectionModel::ClearAndSelect);
playlist->IgnoreSorting(false);
connect(view()->selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), this, SLOT(SelectionChanged()));
emit ViewSelectionModelChanged();
// Update filter
ui_->filter->setText(playlist->proxy()->filterRegExp().pattern());
// Update the no matches label
connect(playlist_->proxy(), SIGNAL(modelReset()), SLOT(UpdateNoMatchesLabel()));
connect(playlist_->proxy(), SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(UpdateNoMatchesLabel()));
connect(playlist_->proxy(), SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(UpdateNoMatchesLabel()));
connect(playlist_, SIGNAL(modelReset()), SLOT(UpdateNoMatchesLabel()));
connect(playlist_, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(UpdateNoMatchesLabel()));
connect(playlist_, SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(UpdateNoMatchesLabel()));
UpdateNoMatchesLabel();
// Ensure that tab is current
if (ui_->tab_bar->current_id() != manager_->current_id())
ui_->tab_bar->set_current_id(manager_->current_id());
// Sort out the undo/redo actions
delete undo_;
delete redo_;
undo_ = playlist->undo_stack()->createUndoAction(this);
redo_ = playlist->undo_stack()->createRedoAction(this);
undo_->setIcon(IconLoader::Load("edit-undo"));
undo_->setShortcut(QKeySequence::Undo);
redo_->setIcon(IconLoader::Load("edit-redo"));
redo_->setShortcut(QKeySequence::Redo);
ui_->undo->setDefaultAction(undo_);
ui_->redo->setDefaultAction(redo_);
emit UndoRedoActionsChanged(undo_, redo_);
}
void PlaylistContainer::ActivePlaying() {
UpdateActiveIcon(QIcon(":/pictures/tiny-play.png"));
}
void PlaylistContainer::ActivePaused() {
UpdateActiveIcon(QIcon(":/pictures/tiny-pause.png"));
}
void PlaylistContainer::ActiveStopped() { UpdateActiveIcon(QIcon()); }
void PlaylistContainer::UpdateActiveIcon(const QIcon &icon) {
// Unset all existing icons
for (int i = 0; i < ui_->tab_bar->count(); ++i) {
ui_->tab_bar->setTabIcon(i, QIcon());
}
// Set our icon
if (!icon.isNull()) ui_->tab_bar->set_icon_by_id(manager_->active_id(), icon);
}
void PlaylistContainer::PlaylistAdded(int id, const QString &name, bool favorite) {
const int index = ui_->tab_bar->count();
ui_->tab_bar->InsertTab(id, index, name, favorite);
// Are we startup up, should we select this tab?
if (starting_up_ && settings_.value("current_playlist", 1).toInt() == id) {
starting_up_ = false;
ui_->tab_bar->set_current_id(id);
}
if (ui_->tab_bar->count() > 1) {
// Have to do this here because sizeHint() is only valid when there's a
// tab in the bar.
tab_bar_animation_->setFrameRange(0, ui_->tab_bar->sizeHint().height());
if (!isVisible()) {
// Skip the animation since the window is hidden (eg. if we're still
// loading the UI).
tab_bar_visible_ = true;
ui_->tab_bar->setMaximumHeight(tab_bar_animation_->endFrame());
} else {
SetTabBarVisible(true);
}
}
}
void PlaylistContainer::Started() { starting_up_ = false; }
void PlaylistContainer::PlaylistClosed(int id) {
ui_->tab_bar->RemoveTab(id);
if (ui_->tab_bar->count() <= 1) SetTabBarVisible(false);
}
void PlaylistContainer::PlaylistRenamed(int id, const QString &new_name) {
ui_->tab_bar->set_text_by_id(id, new_name);
}
void PlaylistContainer::NewPlaylist() { manager_->New(tr("Playlist")); }
void PlaylistContainer::LoadPlaylist() {
//qLog(Debug) << __PRETTY_FUNCTION__;
QString filename = settings_.value("last_load_playlist").toString();
filename = QFileDialog::getOpenFileName(this, tr("Load playlist"), filename, manager_->parser()->filters());
if (filename.isNull()) return;
settings_.setValue("last_load_playlist", filename);
manager_->Load(filename);
}
void PlaylistContainer::SavePlaylist(int id = -1) {
// Use the tab name as the suggested name
QString suggested_name = ui_->tab_bar->tabText(ui_->tab_bar->currentIndex());
manager_->SaveWithUI(id, suggested_name);
}
void PlaylistContainer::ClearPlaylist() {
}
void PlaylistContainer::GoToNextPlaylistTab() {
// Get the next tab' id
int id_next = ui_->tab_bar->id_of((ui_->tab_bar->currentIndex() + 1) %
ui_->tab_bar->count());
// Switch to next tab
manager_->SetCurrentPlaylist(id_next);
}
void PlaylistContainer::GoToPreviousPlaylistTab() {
// Get the next tab' id
int id_previous = ui_->tab_bar->id_of((ui_->tab_bar->currentIndex()+ui_->tab_bar->count()-1) % ui_->tab_bar->count());
// Switch to next tab
manager_->SetCurrentPlaylist(id_previous);
}
void PlaylistContainer::Save() {
if (starting_up_) return;
settings_.setValue("current_playlist", ui_->tab_bar->current_id());
}
void PlaylistContainer::SetTabBarVisible(bool visible) {
if (tab_bar_visible_ == visible) return;
tab_bar_visible_ = visible;
tab_bar_animation_->setDirection(visible ? QTimeLine::Forward : QTimeLine::Backward);
tab_bar_animation_->start();
}
void PlaylistContainer::SetTabBarHeight(int height) {
ui_->tab_bar->setMaximumHeight(height);
}
void PlaylistContainer::MaybeUpdateFilter() {
// delaying the filter update on small playlists is undesirable
// and an empty filter applies very quickly, too
if (manager_->current()->rowCount() < kFilterDelayPlaylistSizeThreshold || ui_->filter->text().isEmpty()) {
UpdateFilter();
}
else {
filter_timer_->start();
}
}
void PlaylistContainer::UpdateFilter() {
manager_->current()->proxy()->setFilterFixedString(ui_->filter->text());
ui_->playlist->JumpToCurrentlyPlayingTrack();
UpdateNoMatchesLabel();
}
void PlaylistContainer::UpdateNoMatchesLabel() {
Playlist *playlist = manager_->current();
const bool has_rows = playlist->rowCount() != 0;
const bool has_results = playlist->proxy()->rowCount() != 0;
QString text;
if (has_rows && !has_results) {
if (ui_->filter->text().trimmed().compare("the answer to life the universe and everything", Qt::CaseInsensitive) == 0) {
text = "42";
}
else {
text = tr("No matches found. Clear the search box to show the whole playlist again.");
}
}
if (!text.isEmpty()) {
no_matches_label_->setText(text);
RepositionNoMatchesLabel(true);
no_matches_label_->show();
}
else {
no_matches_label_->hide();
}
}
void PlaylistContainer::resizeEvent(QResizeEvent *e) {
QWidget::resizeEvent(e);
RepositionNoMatchesLabel();
}
void PlaylistContainer::FocusOnFilter(QKeyEvent *event) {
ui_->filter->setFocus();
switch (event->key()) {
case Qt::Key_Backspace:
break;
case Qt::Key_Escape:
ui_->filter->clear();
break;
default:
ui_->filter->setText(ui_->filter->text() + event->text());
break;
}
}
void PlaylistContainer::RepositionNoMatchesLabel(bool force) {
if (!force && !no_matches_label_->isVisible()) return;
const int kBorder = 10;
QPoint pos = ui_->playlist->viewport()->mapTo(ui_->playlist, QPoint(kBorder, kBorder));
QSize size = ui_->playlist->viewport()->size();
size.setWidth(size.width() - kBorder * 2);
size.setHeight(size.height() - kBorder * 2);
no_matches_label_->move(pos);
no_matches_label_->resize(size);
}
void PlaylistContainer::SelectionChanged() {
manager_->SelectionChanged(view()->selectionModel()->selection());
}
bool PlaylistContainer::eventFilter(QObject *objectWatched, QEvent *event) {
if (objectWatched == ui_->filter) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *e = static_cast<QKeyEvent*>(event);
switch (e->key()) {
//case Qt::Key_Up:
case Qt::Key_Down:
case Qt::Key_PageUp:
case Qt::Key_PageDown:
case Qt::Key_Return:
case Qt::Key_Enter:
view()->setFocus(Qt::OtherFocusReason);
QApplication::sendEvent(ui_->playlist, event);
return true;
case Qt::Key_Escape:
ui_->filter->clear();
return true;
default:
break;
}
}
}
return QWidget::eventFilter(objectWatched, event);
}

View File

@@ -0,0 +1,125 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTCONTAINER_H
#define PLAYLISTCONTAINER_H
#include "config.h"
#include <QWidget>
#include <QSettings>
class Ui_PlaylistContainer;
class LineEditInterface;
class Playlist;
class PlaylistManager;
class PlaylistView;
class QTimeLine;
class QTimer;
class QLabel;
class PlaylistContainer : public QWidget {
Q_OBJECT
public:
PlaylistContainer(QWidget *parent = nullptr);
~PlaylistContainer();
static const char *kSettingsGroup;
void SetActions(QAction *new_playlist, QAction *load_playlist, QAction *save_playlist, QAction *clear_playlist, QAction *next_playlist, QAction *previous_playlist);
void SetManager(PlaylistManager *manager);
PlaylistView *view() const;
bool eventFilter(QObject *objectWatched, QEvent *event);
signals:
void TabChanged(int id);
void Rename(int id, const QString &new_name);
void UndoRedoActionsChanged(QAction *undo, QAction *redo);
void ViewSelectionModelChanged();
protected:
// QWidget
void resizeEvent(QResizeEvent*);
private slots:
void NewPlaylist();
void LoadPlaylist();
void SavePlaylist() { SavePlaylist(-1); }
void SavePlaylist(int id);
void ClearPlaylist();
void GoToNextPlaylistTab();
void GoToPreviousPlaylistTab();
void SetViewModel(Playlist *playlist);
void PlaylistAdded(int id, const QString &name, bool favorite);
void PlaylistClosed(int id);
void PlaylistRenamed(int id, const QString &new_name);
void Started();
void ActivePlaying();
void ActivePaused();
void ActiveStopped();
void Save();
void SetTabBarVisible(bool visible);
void SetTabBarHeight(int height);
void SelectionChanged();
void MaybeUpdateFilter();
void UpdateFilter();
void FocusOnFilter(QKeyEvent *event);
void UpdateNoMatchesLabel();
private:
void UpdateActiveIcon(const QIcon &icon);
void RepositionNoMatchesLabel(bool force = false);
private:
static const int kFilterDelayMs;
static const int kFilterDelayPlaylistSizeThreshold;
Ui_PlaylistContainer *ui_;
PlaylistManager *manager_;
QAction *undo_;
QAction *redo_;
Playlist *playlist_;
QSettings settings_;
bool starting_up_;
bool tab_bar_visible_;
QTimeLine *tab_bar_animation_;
QLabel *no_matches_label_;
QTimer *filter_timer_;
};
#endif // PLAYLISTCONTAINER_H

View File

@@ -0,0 +1,189 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlaylistContainer</class>
<widget class="QWidget" name="PlaylistContainer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>987</width>
<height>707</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">#toolbar {
border-color: palette(dark);
border-style: solid;
border-width: 0px 1px 0px 1px;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="PlaylistTabBar" name="tab_bar" native="true"/>
</item>
<item>
<widget class="QFrame" name="toolbar">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="create_new">
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="load">
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="save">
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="clear">
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="undo">
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="redo">
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QSearchField" name="filter" native="true"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="PlaylistView" name="playlist">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="editTriggers">
<set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<property name="allColumnsShowFocus">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QSearchField</class>
<extends>QWidget</extends>
<header>3rdparty/qocoa/qsearchfield.h</header>
</customwidget>
<customwidget>
<class>PlaylistView</class>
<extends>QTreeView</extends>
<header>playlist/playlistview.h</header>
</customwidget>
<customwidget>
<class>PlaylistTabBar</class>
<extends>QWidget</extends>
<header>playlist/playlisttabbar.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,475 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QDateTime>
#include <QDir>
#include <QFuture>
#include <QHeaderView>
#include <QHelpEvent>
#include <QLinearGradient>
#include <QLineEdit>
#include <QPainter>
#include <QScrollBar>
#include <QTextDocument>
#include <QToolTip>
#include <QWhatsThis>
#include <QtConcurrentRun>
#include "playlistdelegates.h"
#include "queue.h"
#include "core/logging.h"
#include "core/player.h"
#include "core/utilities.h"
#include "core/iconloader.h"
#include "collection/collectionbackend.h"
#include "widgets/trackslider.h"
#ifdef Q_OS_DARWIN
#include "core/mac_utilities.h"
#endif // Q_OS_DARWIN
const int QueuedItemDelegate::kQueueBoxBorder = 1;
const int QueuedItemDelegate::kQueueBoxCornerRadius = 3;
const int QueuedItemDelegate::kQueueBoxLength = 30;
const QRgb QueuedItemDelegate::kQueueBoxGradientColor1 = qRgb(102, 150, 227);
const QRgb QueuedItemDelegate::kQueueBoxGradientColor2 = qRgb(77, 121, 200);
const int QueuedItemDelegate::kQueueOpacitySteps = 10;
const float QueuedItemDelegate::kQueueOpacityLowerBound = 0.4;
const int PlaylistDelegateBase::kMinHeight = 19;
QueuedItemDelegate::QueuedItemDelegate(QObject *parent, int indicator_column)
: QStyledItemDelegate(parent), indicator_column_(indicator_column) {}
void QueuedItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
QStyledItemDelegate::paint(painter, option, index);
if (index.column() == indicator_column_) {
bool ok = false;
const int queue_pos = index.data(Playlist::Role_QueuePosition).toInt(&ok);
if (ok && queue_pos != -1) {
float opacity = kQueueOpacitySteps - qMin(kQueueOpacitySteps, queue_pos);
opacity /= kQueueOpacitySteps;
opacity *= 1.0 - kQueueOpacityLowerBound;
opacity += kQueueOpacityLowerBound;
painter->setOpacity(opacity);
DrawBox(painter, option.rect, option.font, QString::number(queue_pos + 1), kQueueBoxLength);
painter->setOpacity(1.0);
}
}
}
void QueuedItemDelegate::DrawBox(QPainter *painter, const QRect &line_rect, const QFont &font, const QString &text, int width) const {
QFont smaller = font;
smaller.setPointSize(smaller.pointSize() - 1);
smaller.setBold(true);
if (width == -1) width = QFontMetrics(font).width(text + " ");
QRect rect(line_rect);
rect.setLeft(rect.right() - width - kQueueBoxBorder);
rect.setWidth(width);
rect.setTop(rect.top() + kQueueBoxBorder);
rect.setBottom(rect.bottom() - kQueueBoxBorder - 1);
QRect text_rect(rect);
text_rect.setBottom(text_rect.bottom() + 1);
QLinearGradient gradient(rect.topLeft(), rect.bottomLeft());
gradient.setColorAt(0.0, kQueueBoxGradientColor1);
gradient.setColorAt(1.0, kQueueBoxGradientColor2);
// Turn on antialiasing
painter->setRenderHint(QPainter::Antialiasing);
// Draw the box
painter->translate(0.5, 0.5);
painter->setPen(QPen(Qt::white, 1));
painter->setBrush(gradient);
painter->drawRoundedRect(rect, kQueueBoxCornerRadius, kQueueBoxCornerRadius);
// Draw the text
painter->setFont(smaller);
painter->drawText(rect, Qt::AlignCenter, text);
painter->translate(-0.5, -0.5);
}
int QueuedItemDelegate::queue_indicator_size(const QModelIndex &index) const {
if (index.column() == indicator_column_) {
const int queue_pos = index.data(Playlist::Role_QueuePosition).toInt();
if (queue_pos != -1) {
return kQueueBoxLength + kQueueBoxBorder * 2;
}
}
return 0;
}
PlaylistDelegateBase::PlaylistDelegateBase(QObject *parent, const QString &suffix)
: QueuedItemDelegate(parent), view_(qobject_cast<QTreeView*>(parent)), suffix_(suffix)
{
}
QString PlaylistDelegateBase::displayText(const QVariant &value, const QLocale&) const {
QString text;
switch (static_cast<QMetaType::Type>(value.type())) {
case QMetaType::Int: {
int v = value.toInt();
if (v > 0) text = QString::number(v);
break;
}
case QMetaType::Float:
case QMetaType::Double: {
double v = value.toDouble();
if (v > 0) text = QString::number(v);
break;
}
default:
text = value.toString();
break;
}
if (!text.isNull() && !suffix_.isNull()) text += " " + suffix_;
return text;
}
QSize PlaylistDelegateBase::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const {
QSize size = QueuedItemDelegate::sizeHint(option, index);
if (size.height() < kMinHeight) size.setHeight(kMinHeight);
return size;
}
void PlaylistDelegateBase::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
QueuedItemDelegate::paint(painter, Adjusted(option, index), index);
// Stop after indicator
if (index.column() == Playlist::Column_Title) {
if (index.data(Playlist::Role_StopAfter).toBool()) {
QRect rect(option.rect);
rect.setRight(rect.right() - queue_indicator_size(index));
DrawBox(painter, rect, option.font, tr("stop"));
}
}
}
QStyleOptionViewItemV4 PlaylistDelegateBase::Adjusted(const QStyleOptionViewItem &option, const QModelIndex &index) const {
if (!view_) return option;
QPoint top_left(-view_->horizontalScrollBar()->value(), -view_->verticalScrollBar()->value());
if (view_->header()->logicalIndexAt(top_left) != index.column())
return option;
QStyleOptionViewItemV4 ret(option);
if (index.data(Playlist::Role_IsCurrent).toBool()) {
// Move the text in a bit on the first column for the song that's currently
// playing
ret.rect.setLeft(ret.rect.left() + 20);
}
return ret;
}
bool PlaylistDelegateBase::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) {
// This function is copied from QAbstractItemDelegate, and changed to show
// displayText() in the tooltip, rather than the index's naked
// Qt::ToolTipRole text.
Q_UNUSED(option);
if (!event || !view) return false;
QHelpEvent *he = static_cast<QHelpEvent*>(event);
QString text = displayText(index.data(), QLocale::system());
// Special case: we want newlines in the comment tooltip
if (index.column() == Playlist::Column_Comment) {
text = index.data(Qt::ToolTipRole).toString().toHtmlEscaped();
text.replace("\\r\\n", "<br />");
text.replace("\\n", "<br />");
text.replace("\r\n", "<br />");
text.replace("\n", "<br />");
}
if (text.isEmpty() || !he) return false;
switch (event->type()) {
case QEvent::ToolTip: {
QRect displayed_text;
QSize real_text;
bool is_elided = false;
real_text = sizeHint(option, index);
displayed_text = view->visualRect(index);
is_elided = displayed_text.width() < real_text.width();
if (is_elided) {
QToolTip::showText(he->globalPos(), text, view);
} else { // in case that another text was previously displayed
QToolTip::hideText();
}
return true;
}
case QEvent::QueryWhatsThis:
return true;
case QEvent::WhatsThis:
QWhatsThis::showText(he->globalPos(), text, view);
return true;
default:
break;
}
return false;
}
QString LengthItemDelegate::displayText(const QVariant &value, const QLocale&) const {
bool ok = false;
qint64 nanoseconds = value.toLongLong(&ok);
if (ok && nanoseconds > 0) return Utilities::PrettyTimeNanosec(nanoseconds);
return QString::null;
}
QString SizeItemDelegate::displayText(const QVariant &value, const QLocale&) const {
bool ok = false;
int bytes = value.toInt(&ok);
if (ok) return Utilities::PrettySize(bytes);
return QString();
}
QString DateItemDelegate::displayText(const QVariant &value, const QLocale &locale) const {
bool ok = false;
int time = value.toInt(&ok);
if (!ok || time == -1)
return QString::null;
return QDateTime::fromTime_t(time).toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat));
}
QString LastPlayedItemDelegate::displayText(const QVariant &value, const QLocale &locale) const {
bool ok = false;
const int time = value.toInt(&ok);
if (!ok || time == -1)
return tr("Never");
return Utilities::Ago(time, locale);
}
QString FileTypeItemDelegate::displayText(const QVariant &value, const QLocale &locale) const {
bool ok = false;
Song::FileType type = Song::FileType(value.toInt(&ok));
if (!ok) return tr("Unknown");
return Song::TextForFiletype(type);
}
QString SamplerateBitdepthItemDelegate::displayText(const QVariant &value, const QLocale &locale) const {
return value.toString();
}
QWidget *TextItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
return new QLineEdit(parent);
}
TagCompletionModel::TagCompletionModel(CollectionBackend *backend, Playlist::Column column) : QStringListModel() {
QString col = database_column(column);
if (!col.isEmpty()) {
setStringList(backend->GetAll(col));
}
}
QString TagCompletionModel::database_column(Playlist::Column column) {
switch (column) {
case Playlist::Column_Artist: return "artist";
case Playlist::Column_Album: return "album";
case Playlist::Column_AlbumArtist: return "albumartist";
case Playlist::Column_Composer: return "composer";
case Playlist::Column_Performer: return "performer";
case Playlist::Column_Grouping: return "grouping";
case Playlist::Column_Genre: return "genre";
default:
qLog(Warning) << "Unknown column" << column;
return QString();
}
}
static TagCompletionModel *InitCompletionModel(CollectionBackend *backend, Playlist::Column column) {
return new TagCompletionModel(backend, column);
}
TagCompleter::TagCompleter(CollectionBackend *backend, Playlist::Column column, QLineEdit *editor) : QCompleter(editor), editor_(editor) {
QFuture<TagCompletionModel*> future = QtConcurrent::run(&InitCompletionModel, backend, column);
NewClosure(future, this, SLOT(ModelReady(QFuture<TagCompletionModel*>)), future);
}
void TagCompleter::ModelReady(QFuture<TagCompletionModel*> future) {
TagCompletionModel *model = future.result();
setModel(model);
setCaseSensitivity(Qt::CaseInsensitive);
editor_->setCompleter(this);
}
QWidget *TagCompletionItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem&, const QModelIndex&) const {
QLineEdit *editor = new QLineEdit(parent);
new TagCompleter(backend_, column_, editor);
return editor;
}
QString NativeSeparatorsDelegate::displayText(const QVariant &value, const QLocale&) const {
const QString string_value = value.toString();
QUrl url;
if (value.type() == QVariant::Url) {
url = value.toUrl();
}
else if (string_value.contains("://")) {
url = QUrl::fromEncoded(string_value.toLatin1());
}
else {
return QDir::toNativeSeparators(string_value);
}
if (url.scheme() == "file") {
return QDir::toNativeSeparators(url.toLocalFile());
}
return string_value;
}
SongSourceDelegate::SongSourceDelegate(QObject *parent, Player *player) : PlaylistDelegateBase(parent), player_(player) {}
QString SongSourceDelegate::displayText(const QVariant &value, const QLocale&) const {
return QString();
}
QPixmap SongSourceDelegate::LookupPixmap(const QUrl &url, const QSize &size) const {
QPixmap pixmap;
if (cache_.find(url.scheme(), &pixmap)) {
return pixmap;
}
QIcon icon;
const UrlHandler *handler = player_->HandlerForUrl(url);
if (handler) {
icon = handler->icon();
}
else {
if (url.scheme() == "file") {
icon = IconLoader::Load("folder-sound");
}
else if (url.scheme() == "cdda") {
icon = IconLoader::Load("cd");
}
else {
icon = IconLoader::Load("folder-sound");
}
}
pixmap = icon.pixmap(size.height());
cache_.insert(url.scheme(), pixmap);
return pixmap;
}
void SongSourceDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const {
// Draw the background
PlaylistDelegateBase::paint(painter, option, index);
QStyleOptionViewItem option_copy(option);
initStyleOption(&option_copy, index);
// Find the pixmap to use for this URL
const QUrl &url = index.data().toUrl();
QPixmap pixmap = LookupPixmap(url, option_copy.decorationSize);
float device_pixel_ratio = 1.0f;
#ifdef Q_OS_DARWIN
QWidget *parent_widget = reinterpret_cast<QWidget*>(parent());
device_pixel_ratio = mac::GetDevicePixelRatio(parent_widget);
#endif
// Draw the pixmap in the middle of the rectangle
QRect draw_rect(QPoint(0, 0), option_copy.decorationSize / device_pixel_ratio);
draw_rect.moveCenter(option_copy.rect.center());
painter->drawPixmap(draw_rect, pixmap);
}

View File

@@ -0,0 +1,172 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTDELEGATES_H
#define PLAYLISTDELEGATES_H
#include "config.h"
#include <QCompleter>
#include <QPixmapCache>
#include <QStringListModel>
#include <QStyledItemDelegate>
#include <QTreeView>
#include "playlist.h"
#include "collection/collection.h"
#include "widgets/ratingwidget.h"
class Player;
class QueuedItemDelegate : public QStyledItemDelegate {
public:
QueuedItemDelegate(QObject *parent, int indicator_column = Playlist::Column_Title);
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
void DrawBox(QPainter *painter, const QRect &line_rect, const QFont &font, const QString &text, int width = -1) const;
int queue_indicator_size(const QModelIndex &index) const;
private:
static const int kQueueBoxBorder;
static const int kQueueBoxCornerRadius;
static const int kQueueBoxLength;
static const QRgb kQueueBoxGradientColor1;
static const QRgb kQueueBoxGradientColor2;
static const int kQueueOpacitySteps;
static const float kQueueOpacityLowerBound;
int indicator_column_;
};
class PlaylistDelegateBase : public QueuedItemDelegate {
Q_OBJECT
public:
PlaylistDelegateBase(QObject *parent, const QString &suffix = QString());
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
QString displayText(const QVariant &value, const QLocale &locale) const;
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
QStyleOptionViewItemV4 Adjusted(const QStyleOptionViewItem &option, const QModelIndex &index) const;
static const int kMinHeight;
public slots:
bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index);
protected:
QTreeView *view_;
QString suffix_;
};
class LengthItemDelegate : public PlaylistDelegateBase {
public:
LengthItemDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
QString displayText(const QVariant &value, const QLocale &locale) const;
};
class SizeItemDelegate : public PlaylistDelegateBase {
public:
SizeItemDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
QString displayText(const QVariant &value, const QLocale &locale) const;
};
class DateItemDelegate : public PlaylistDelegateBase {
public:
DateItemDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
QString displayText(const QVariant &value, const QLocale &locale) const;
};
class LastPlayedItemDelegate : public PlaylistDelegateBase {
public:
LastPlayedItemDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
QString displayText(const QVariant &value, const QLocale &locale) const;
};
class FileTypeItemDelegate : public PlaylistDelegateBase {
public:
FileTypeItemDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
QString displayText(const QVariant &value, const QLocale &locale) const;
};
class SamplerateBitdepthItemDelegate : public PlaylistDelegateBase {
public:
SamplerateBitdepthItemDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
QString displayText(const QVariant &value, const QLocale &locale) const;
};
class TextItemDelegate : public PlaylistDelegateBase {
public:
TextItemDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const;
};
class TagCompletionModel : public QStringListModel {
public:
TagCompletionModel(CollectionBackend *backend, Playlist::Column column);
private:
static QString database_column(Playlist::Column column);
};
class TagCompleter : public QCompleter {
Q_OBJECT
public:
TagCompleter(CollectionBackend *backend, Playlist::Column column, QLineEdit *editor);
private slots:
void ModelReady(QFuture<TagCompletionModel*> future);
private:
QLineEdit *editor_;
};
class TagCompletionItemDelegate : public PlaylistDelegateBase {
public:
TagCompletionItemDelegate(QObject *parent, CollectionBackend *backend, Playlist::Column column) : PlaylistDelegateBase(parent), backend_(backend), column_(column) {};
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const;
private:
CollectionBackend *backend_;
Playlist::Column column_;
};
class NativeSeparatorsDelegate : public PlaylistDelegateBase {
public:
NativeSeparatorsDelegate(QObject *parent) : PlaylistDelegateBase(parent) {}
QString displayText(const QVariant &value, const QLocale &locale) const;
};
class SongSourceDelegate : public PlaylistDelegateBase {
public:
SongSourceDelegate(QObject *parent, Player *player);
QString displayText(const QVariant &value, const QLocale &locale) const;
void paint(QPainter *paint, const QStyleOptionViewItem &option, const QModelIndex &index) const;
private:
QPixmap LookupPixmap(const QUrl &url, const QSize &size) const;
Player *player_;
mutable QPixmapCache cache_;
};
#endif // PLAYLISTDELEGATES_H

View File

@@ -0,0 +1,85 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistfilter.h"
#include "playlistfilterparser.h"
#include <QtDebug>
PlaylistFilter::PlaylistFilter(QObject *parent)
: QSortFilterProxyModel(parent),
filter_tree_(new NopFilter),
query_hash_(0)
{
setDynamicSortFilter(true);
column_names_["title"] = Playlist::Column_Title;
column_names_["name"] = Playlist::Column_Title;
column_names_["artist"] = Playlist::Column_Artist;
column_names_["album"] = Playlist::Column_Album;
column_names_["albumartist"] = Playlist::Column_AlbumArtist;
column_names_["composer"] = Playlist::Column_Composer;
column_names_["performer"] = Playlist::Column_Performer;
column_names_["grouping"] = Playlist::Column_Grouping;
column_names_["length"] = Playlist::Column_Length;
column_names_["track"] = Playlist::Column_Track;
column_names_["disc"] = Playlist::Column_Disc;
column_names_["year"] = Playlist::Column_Year;
column_names_["originalyear"] = Playlist::Column_OriginalYear;
column_names_["genre"] = Playlist::Column_Genre;
column_names_["comment"] = Playlist::Column_Comment;
column_names_["bitrate"] = Playlist::Column_Bitrate;
column_names_["filename"] = Playlist::Column_Filename;
numerical_columns_ << Playlist::Column_Length
<< Playlist::Column_Track
<< Playlist::Column_Disc
<< Playlist::Column_Year
<< Playlist::Column_Bitrate;
}
PlaylistFilter::~PlaylistFilter() {
}
void PlaylistFilter::sort(int column, Qt::SortOrder order) {
// Pass this through to the Playlist, it does sorting itself
sourceModel()->sort(column, order);
}
bool PlaylistFilter::filterAcceptsRow(int row, const QModelIndex &parent) const {
QString filter = filterRegExp().pattern();
uint hash = qHash(filter);
if (hash != query_hash_) {
// Parse the query
FilterParser p(filter, column_names_, numerical_columns_);
filter_tree_.reset(p.parse());
query_hash_ = hash;
}
// Test the row
return filter_tree_->accept(row, parent, sourceModel());
}

View File

@@ -0,0 +1,58 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTFILTER_H
#define PLAYLISTFILTER_H
#include "config.h"
#include <QScopedPointer>
#include <QSortFilterProxyModel>
#include "playlist.h"
#include <QSet>
class FilterTree;
class PlaylistFilter : public QSortFilterProxyModel {
Q_OBJECT
public:
PlaylistFilter(QObject *parent = nullptr);
~PlaylistFilter();
// QAbstractItemModel
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder);
// QSortFilterProxyModel
// public so Playlist::NextVirtualIndex and friends can get at it
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const;
private:
// Mutable because they're modified from filterAcceptsRow() const
mutable QScopedPointer<FilterTree> filter_tree_;
mutable uint query_hash_;
QMap<QString, int> column_names_;
QSet<int> numerical_columns_;
};
#endif // PLAYLISTFILTER_H

View File

@@ -0,0 +1,535 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistfilterparser.h"
#include "playlist.h"
#include "core/logging.h"
#include <QAbstractItemModel>
class SearchTermComparator {
public:
virtual ~SearchTermComparator() {}
virtual bool Matches(const QString &element) const = 0;
};
// "compares" by checking if the field contains the search term
class DefaultComparator : public SearchTermComparator {
public:
explicit DefaultComparator(const QString &value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return element.contains(search_term_);
}
private:
QString search_term_;
};
class EqComparator : public SearchTermComparator {
public:
explicit EqComparator(const QString &value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return search_term_ == element;
}
private:
QString search_term_;
};
class NeComparator : public SearchTermComparator {
public:
explicit NeComparator(const QString &value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return search_term_ != element;
}
private:
QString search_term_;
};
class LexicalGtComparator : public SearchTermComparator {
public:
explicit LexicalGtComparator(const QString &value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return element > search_term_;
}
private:
QString search_term_;
};
class LexicalGeComparator : public SearchTermComparator {
public:
explicit LexicalGeComparator(const QString &value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return element >= search_term_;
}
private:
QString search_term_;
};
class LexicalLtComparator : public SearchTermComparator {
public:
explicit LexicalLtComparator(const QString &value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return element < search_term_;
}
private:
QString search_term_;
};
class LexicalLeComparator : public SearchTermComparator {
public:
explicit LexicalLeComparator(const QString &value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return element <= search_term_;
}
private:
QString search_term_;
};
class GtComparator : public SearchTermComparator {
public:
explicit GtComparator(int value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return element.toInt() > search_term_;
}
private:
int search_term_;
};
class GeComparator : public SearchTermComparator {
public:
explicit GeComparator(int value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return element.toInt() >= search_term_;
}
private:
int search_term_;
};
class LtComparator : public SearchTermComparator {
public:
explicit LtComparator(int value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return element.toInt() < search_term_;
}
private:
int search_term_;
};
class LeComparator : public SearchTermComparator {
public:
explicit LeComparator(int value) : search_term_(value) {}
virtual bool Matches(const QString &element) const {
return element.toInt() <= search_term_;
}
private:
int search_term_;
};
// The length field of the playlist (entries) contains a
// song's running time in nano seconds. However, We don't
// really care about nano seconds, just seconds. Thus, with
// this decorator we drop the last 9 digits, if that many
// are present.
class DropTailComparatorDecorator : public SearchTermComparator {
public:
explicit DropTailComparatorDecorator(SearchTermComparator *cmp) : cmp_(cmp) {}
virtual bool Matches(const QString &element) const {
if (element.length() > 9)
return cmp_->Matches(element.left(element.length() - 9));
else
return cmp_->Matches(element);
}
private:
QScopedPointer<SearchTermComparator> cmp_;
};
class RatingComparatorDecorator : public SearchTermComparator {
public:
explicit RatingComparatorDecorator(SearchTermComparator *cmp) : cmp_(cmp) {}
virtual bool Matches(const QString &element) const {
return cmp_->Matches(
QString::number(static_cast<int>(element.toDouble() * 10.0 + 0.5)));
}
private:
QScopedPointer<SearchTermComparator> cmp_;
};
// filter that applies a SearchTermComparator to all fields of a playlist entry
class FilterTerm : public FilterTree {
public:
explicit FilterTerm(SearchTermComparator *comparator, const QList<int> &columns) : cmp_(comparator), columns_(columns) {}
virtual bool accept(int row, const QModelIndex &parent,
const QAbstractItemModel *const model) const {
for (int i : columns_) {
QModelIndex idx(model->index(row, i, parent));
if (cmp_->Matches(idx.data().toString().toLower())) return true;
}
return false;
}
virtual FilterType type() { return Term; }
private:
QScopedPointer<SearchTermComparator> cmp_;
QList<int> columns_;
};
// filter that applies a SearchTermComparator to one specific field of a playlist entry
class FilterColumnTerm : public FilterTree {
public:
FilterColumnTerm(int column, SearchTermComparator *comparator) : col(column), cmp_(comparator) {}
virtual bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const {
QModelIndex idx(model->index(row, col, parent));
return cmp_->Matches(idx.data().toString().toLower());
}
virtual FilterType type() { return Column; }
private:
int col;
QScopedPointer<SearchTermComparator> cmp_;
};
class NotFilter : public FilterTree {
public:
explicit NotFilter(const FilterTree *inv) : child_(inv) {}
virtual bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const {
return !child_->accept(row, parent, model);
}
virtual FilterType type() { return Not; }
private:
QScopedPointer<const FilterTree> child_;
};
class OrFilter : public FilterTree {
public:
~OrFilter() { qDeleteAll(children_); }
virtual void add(FilterTree *child) { children_.append(child); }
virtual bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const {
for (FilterTree *child : children_) {
if (child->accept(row, parent, model)) return true;
}
return false;
}
FilterType type() { return Or; }
private:
QList<FilterTree*> children_;
};
class AndFilter : public FilterTree {
public:
virtual ~AndFilter() { qDeleteAll(children_); }
virtual void add(FilterTree *child) { children_.append(child); }
virtual bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const {
for (FilterTree *child : children_) {
if (!child->accept(row, parent, model)) return false;
}
return true;
}
FilterType type() { return And; }
private:
QList<FilterTree*> children_;
};
FilterParser::FilterParser(const QString &filter, const QMap<QString, int> &columns, const QSet<int> &numerical_cols) : filterstring_(filter), columns_(columns), numerical_columns_(numerical_cols) {}
FilterTree *FilterParser::parse() {
iter_ = filterstring_.constBegin();
end_ = filterstring_.constEnd();
return parseOrGroup();
}
void FilterParser::advance() {
while (iter_ != end_ && iter_->isSpace()) {
++iter_;
}
}
FilterTree *FilterParser::parseOrGroup() {
advance();
if (iter_ == end_) return new NopFilter;
OrFilter *group = new OrFilter;
group->add(parseAndGroup());
advance();
while (checkOr()) {
group->add(parseAndGroup());
advance();
}
return group;
}
FilterTree *FilterParser::parseAndGroup() {
advance();
if (iter_ == end_) return new NopFilter;
AndFilter *group = new AndFilter();
do {
group->add(parseSearchExpression());
advance();
if (iter_ != end_ && *iter_ == QChar(')')) break;
if (checkOr(false)) {
break;
}
checkAnd(); // if there's no 'AND', we'll add the term anyway...
} while (iter_ != end_);
return group;
}
bool FilterParser::checkAnd() {
if (iter_ != end_) {
if (*iter_ == QChar('A')) {
buf_ += *iter_;
iter_++;
if (iter_ != end_ && *iter_ == QChar('N')) {
buf_ += *iter_;
iter_++;
if (iter_ != end_ && *iter_ == QChar('D')) {
buf_ += *iter_;
iter_++;
if (iter_ != end_ && (iter_->isSpace() || *iter_ == QChar('-') || *iter_ == '(')) {
advance();
buf_.clear();
return true;
}
}
}
}
}
return false;
}
bool FilterParser::checkOr(bool step_over) {
if (!buf_.isEmpty()) {
if (buf_ == "OR") {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
}
else {
if (iter_ != end_) {
if (*iter_ == 'O') {
buf_ += *iter_;
iter_++;
if (iter_ != end_ && *iter_ == 'R') {
buf_ += *iter_;
iter_++;
if (iter_ != end_ && (iter_->isSpace() || *iter_ == '-' || *iter_ == '(')) {
if (step_over) {
buf_.clear();
advance();
}
return true;
}
}
}
}
}
return false;
}
FilterTree *FilterParser::parseSearchExpression() {
advance();
if (iter_ == end_) return new NopFilter;
if (*iter_ == '(') {
iter_++;
advance();
FilterTree *tree = parseOrGroup();
advance();
if (iter_ != end_) {
if (*iter_ == ')') {
++iter_;
}
}
return tree;
}
else if (*iter_ == '-') {
++iter_;
FilterTree *tree = parseSearchExpression();
if (tree->type() != FilterTree::Nop) return new NotFilter(tree);
return tree;
} else {
return parseSearchTerm();
}
}
FilterTree *FilterParser::parseSearchTerm() {
QString col;
QString search;
QString prefix;
bool inQuotes = false;
for (; iter_ != end_; ++iter_) {
if (inQuotes) {
if (*iter_ == '"')
inQuotes = false;
else
buf_ += *iter_;
}
else {
if (*iter_ == '"') {
inQuotes = true;
}
else if (col.isEmpty() && *iter_ == ':') {
col = buf_.toLower();
buf_.clear();
prefix.clear(); // prefix isn't allowed here - let's ignore it
}
else if (iter_->isSpace() || *iter_ == '(' || *iter_ == ')' ||
*iter_ == '-') {
break;
} else if (buf_.isEmpty()) {
// we don't know whether there is a column part in this search term
// thus we assume the latter and just try and read a prefix
if (prefix.isEmpty() && (*iter_ == '>' || *iter_ == '<' || *iter_ == '=' || *iter_ == '!')) {
prefix += *iter_;
}
else if (prefix != "=" && *iter_ == '=') {
prefix += *iter_;
}
else {
buf_ += *iter_;
}
}
else {
buf_ += *iter_;
}
}
}
search = buf_.toLower();
buf_.clear();
return createSearchTermTreeNode(col, prefix, search);
}
FilterTree *FilterParser::createSearchTermTreeNode(
const QString &col, const QString &prefix, const QString &search) const {
if (search.isEmpty() && prefix != "=") {
return new NopFilter;
}
// here comes a mess :/
// well, not that much of a mess, but so many options -_-
SearchTermComparator *cmp = nullptr;
if (prefix == "!=" || prefix == "<>") {
cmp = new NeComparator(search);
}
else if (!col.isEmpty() && columns_.contains(col) && numerical_columns_.contains(columns_[col])) {
// the length column contains the time in seconds (nano seconds, actually -
// the "nano" part is handled by the DropTailComparatorDecorator, though).
int search_value;
if (columns_[col] == Playlist::Column_Length) {
search_value = parseTime(search);
}
//else if (columns_[col] == Playlist::Column_Rating) {
//search_value = static_cast<int>(search.toDouble() * 2.0 + 0.5);
//}
else {
search_value = search.toInt();
}
// alright, back to deciding which comparator we'll use
if (prefix == ">") {
cmp = new GtComparator(search_value);
}
else if (prefix == ">=") {
cmp = new GeComparator(search_value);
}
else if (prefix == "<") {
cmp = new LtComparator(search_value);
}
else if (prefix == "<=") {
cmp = new LeComparator(search_value);
}
else {
// convert back because for time/rating
cmp = new EqComparator(QString::number(search_value));
}
}
else {
if (prefix == "=") {
cmp = new EqComparator(search);
}
else if (prefix == ">") {
cmp = new LexicalGtComparator(search);
}
else if (prefix == ">=") {
cmp = new LexicalGeComparator(search);
}
else if (prefix == "<") {
cmp = new LexicalLtComparator(search);
}
else if (prefix == "<=") {
cmp = new LexicalLeComparator(search);
}
else {
cmp = new DefaultComparator(search);
}
}
if (columns_.contains(col)) {
if (columns_[col] == Playlist::Column_Length) {
cmp = new DropTailComparatorDecorator(cmp);
}
return new FilterColumnTerm(columns_[col], cmp);
}
else {
return new FilterTerm(cmp, columns_.values());
}
}
// Try and parse the string as '[[h:]m:]s' (ignoring all spaces),
// and return the number of seconds if it parses correctly.
// If not, the original string is returned.
// The 'h', 'm' and 's' components can have any length (including 0).
//
// A few examples:
// "::" is parsed to "0"
// "1::" is parsed to "3600"
// "3:45" is parsed to "225"
// "1:165" is parsed to "225"
// "225" is parsed to "225" (srsly! ^.^)
// "2:3:4:5" is parsed to "2:3:4:5"
// "25m" is parsed to "25m"
int FilterParser::parseTime(const QString &time_str) const {
int seconds = 0;
int accum = 0;
int colon_count = 0;
for (const QChar &c : time_str) {
if (c.isDigit()) {
accum = accum * 10 + c.digitValue();
}
else if (c == ':') {
seconds = seconds * 60 + accum;
accum = 0;
++colon_count;
if (colon_count > 2) {
return 0;
}
}
else if (!c.isSpace()) {
return 0;
}
}
seconds = seconds * 60 + accum;
return seconds;
}

View File

@@ -0,0 +1,103 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTFILTERPARSER_H
#define PLAYLISTFILTERPARSER_H
#include "config.h"
#include <QMap>
#include <QModelIndex>
#include <QSet>
#include <QString>
class QAbstractItemModel;
// structure for filter parse tree
class FilterTree {
public:
virtual ~FilterTree() {}
virtual bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const = 0;
enum FilterType {
Nop = 0,
Or,
And,
Not,
Column,
Term
};
virtual FilterType type() = 0;
};
// trivial filter that accepts *anything*
class NopFilter : public FilterTree {
public:
virtual bool accept(int row, const QModelIndex &parent, const QAbstractItemModel *const model) const { return true; }
virtual FilterType type() { return Nop; }
};
// A utility class to parse search filter strings into a decision tree
// that can decide whether a playlist entry matches the filter.
//
// Here's a grammar describing the filters we expect:
//  expr ::= or-group
// or-group ::= and-group ('OR' and-group)*
// and-group ::= sexpr ('AND' sexpr)*
// sexpr ::= sterm | '-' sexpr | '(' or-group ')'
// sterm ::= col ':' sstring | sstring
// sstring ::= prefix? string
// string ::= [^:-()" ]+ | '"' [^"]+ '"'
// prefix ::= '=' | '<' | '>' | '<=' | '>='
// col ::= "title" | "artist" | ...
class FilterParser {
public:
FilterParser(
const QString &filter,
const QMap<QString, int> &columns,
const QSet<int> &numerical_cols);
FilterTree *parse();
private:
void advance();
FilterTree *parseOrGroup();
FilterTree *parseAndGroup();
// check if iter is at the start of 'AND'
// if so, step over it and return true
// it not, return false and leave iter where it was
bool checkAnd();
// check if iter is at the start of 'OR'
bool checkOr(bool step_over = true);
FilterTree *parseSearchExpression();
FilterTree *parseSearchTerm();
FilterTree *createSearchTermTreeNode(const QString &col, const QString &prefix, const QString &search) const;
int parseTime(const QString &time_str) const;
QString::const_iterator iter_;
QString::const_iterator end_;
QString buf_;
const QString filterstring_;
const QMap<QString, int> columns_;
const QSet<int> numerical_columns_;
};
#endif // PLAYLISTFILTERPARSER_H

View File

@@ -0,0 +1,133 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistheader.h"
#include "playlistview.h"
#include <QtDebug>
#include <QContextMenuEvent>
#include <QMenu>
#include <QSignalMapper>
PlaylistHeader::PlaylistHeader(Qt::Orientation orientation, PlaylistView *view, QWidget *parent)
: StretchHeaderView(orientation, parent),
view_(view),
menu_(new QMenu(this)),
show_mapper_(new QSignalMapper(this))
{
hide_action_ = menu_->addAction(tr("&Hide..."), this, SLOT(HideCurrent()));
stretch_action_ = menu_->addAction(tr("&Stretch columns to fit window"), this, SLOT(ToggleStretchEnabled()));
menu_->addSeparator();
QMenu *align_menu = new QMenu(tr("&Align text"), this);
QActionGroup *align_group = new QActionGroup(this);
align_left_action_ = new QAction(tr("&Left"), align_group);
align_center_action_ = new QAction(tr("&Center"), align_group);
align_right_action_ = new QAction(tr("&Right"), align_group);
align_left_action_->setCheckable(true);
align_center_action_->setCheckable(true);
align_right_action_->setCheckable(true);
align_menu->addActions(align_group->actions());
connect(align_group, SIGNAL(triggered(QAction*)), SLOT(SetColumnAlignment(QAction*)));
menu_->addMenu(align_menu);
menu_->addSeparator();
stretch_action_->setCheckable(true);
stretch_action_->setChecked(is_stretch_enabled());
connect(show_mapper_, SIGNAL(mapped(int)), SLOT(ToggleVisible(int)));
connect(this, SIGNAL(StretchEnabledChanged(bool)), stretch_action_, SLOT(setChecked(bool)));
}
void PlaylistHeader::contextMenuEvent(QContextMenuEvent *e) {
menu_section_ = logicalIndexAt(e->pos());
if (menu_section_ == -1 || (menu_section_ == logicalIndex(0) && logicalIndex(1) == -1))
hide_action_->setVisible(false);
else {
hide_action_->setVisible(true);
QString title(model()->headerData(menu_section_, Qt::Horizontal).toString());
hide_action_->setText(tr("&Hide %1").arg(title));
Qt::Alignment alignment = view_->column_alignment(menu_section_);
if (alignment & Qt::AlignLeft) align_left_action_->setChecked(true);
else if (alignment & Qt::AlignHCenter) align_center_action_->setChecked(true);
else if (alignment & Qt::AlignRight) align_right_action_->setChecked(true);
}
qDeleteAll(show_actions_);
show_actions_.clear();
for (int i = 0 ; i < count() ; ++i) {
AddColumnAction(i);
}
menu_->popup(e->globalPos());
}
void PlaylistHeader::AddColumnAction(int index) {
QString title(model()->headerData(index, Qt::Horizontal).toString());
QAction *action = menu_->addAction(title, show_mapper_, SLOT(map()));
action->setCheckable(true);
action->setChecked(!isSectionHidden(index));
show_actions_ << action;
show_mapper_->setMapping(action, index);
}
void PlaylistHeader::HideCurrent() {
if (menu_section_ == -1) return;
SetSectionHidden(menu_section_, true);
}
void PlaylistHeader::SetColumnAlignment(QAction *action) {
Qt::Alignment alignment = Qt::AlignVCenter;
if (action == align_left_action_) alignment |= Qt::AlignLeft;
if (action == align_center_action_) alignment |= Qt::AlignHCenter;
if (action == align_right_action_) alignment |= Qt::AlignRight;
view_->SetColumnAlignment(menu_section_, alignment);
}
void PlaylistHeader::ToggleVisible(int section) {
SetSectionHidden(section, !isSectionHidden(section));
emit SectionVisibilityChanged(section, !isSectionHidden(section));
}
void PlaylistHeader::enterEvent(QEvent*) {
emit MouseEntered();
}

View File

@@ -0,0 +1,71 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTHEADER_H
#define PLAYLISTHEADER_H
#include "config.h"
#include "widgets/stretchheaderview.h"
class PlaylistView;
class QMenu;
class QSignalMapper;
class PlaylistHeader : public StretchHeaderView {
Q_OBJECT
public:
PlaylistHeader(Qt::Orientation orientation, PlaylistView *view, QWidget *parent = nullptr);
// QWidget
void contextMenuEvent(QContextMenuEvent *e);
void enterEvent(QEvent *);
signals:
void SectionVisibilityChanged(int logical, bool visible);
void MouseEntered();
private slots:
void HideCurrent();
void ToggleVisible(int section);
void SetColumnAlignment(QAction *action);
private:
void AddColumnAction(int index);
private:
PlaylistView *view_;
int menu_section_;
QMenu *menu_;
QAction *hide_action_;
QAction *stretch_action_;
QAction *align_left_action_;
QAction *align_center_action_;
QAction *align_right_action_;
QList<QAction*> show_actions_;
QSignalMapper *show_mapper_;
};
#endif // PLAYLISTHEADER_H

View File

@@ -0,0 +1,116 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistitem.h"
#include "songplaylistitem.h"
#include "core/logging.h"
#include "core/song.h"
#include "collection/collection.h"
#include "collection/collectionplaylistitem.h"
#include <QSqlQuery>
#include <QtConcurrentRun>
#include <QtDebug>
PlaylistItem::~PlaylistItem() {
}
PlaylistItem* PlaylistItem::NewFromType(const QString &type) {
if (type == "Collection") return new CollectionPlaylistItem(type);
else if (type == "File") return new SongPlaylistItem(type);
qLog(Warning) << "Invalid PlaylistItem type:" << type;
return nullptr;
}
PlaylistItem* PlaylistItem::NewFromSongsTable(const QString &table, const Song &song) {
if (table == Collection::kSongsTable)
return new CollectionPlaylistItem(song);
qLog(Warning) << "Invalid PlaylistItem songs table:" << table;
return nullptr;
}
void PlaylistItem::BindToQuery(QSqlQuery* query) const {
query->bindValue(":type", type());
query->bindValue(":collection_id", DatabaseValue(Column_CollectionId));
DatabaseSongMetadata().BindToQuery(query);
}
void PlaylistItem::SetTemporaryMetadata(const Song &metadata) {
temp_metadata_ = metadata;
}
void PlaylistItem::ClearTemporaryMetadata() {
temp_metadata_ = Song();
}
static void ReloadPlaylistItem(PlaylistItemPtr item) {
item->Reload();
}
QFuture<void> PlaylistItem::BackgroundReload() {
return QtConcurrent::run(ReloadPlaylistItem, shared_from_this());
}
void PlaylistItem::SetBackgroundColor(short priority, const QColor &color) {
background_colors_[priority] = color;
}
bool PlaylistItem::HasBackgroundColor(short priority) const {
return background_colors_.contains(priority);
}
void PlaylistItem::RemoveBackgroundColor(short priority) {
background_colors_.remove(priority);
}
QColor PlaylistItem::GetCurrentBackgroundColor() const {
return background_colors_.isEmpty() ? QColor() : background_colors_[background_colors_.keys().last()];
}
bool PlaylistItem::HasCurrentBackgroundColor() const {
return !background_colors_.isEmpty();
}
void PlaylistItem::SetForegroundColor(short priority, const QColor &color) {
foreground_colors_[priority] = color;
}
bool PlaylistItem::HasForegroundColor(short priority) const {
return foreground_colors_.contains(priority);
}
void PlaylistItem::RemoveForegroundColor(short priority) {
foreground_colors_.remove(priority);
}
QColor PlaylistItem::GetCurrentForegroundColor() const {
return foreground_colors_.isEmpty() ? QColor() : foreground_colors_[foreground_colors_.keys().last()];
}
bool PlaylistItem::HasCurrentForegroundColor() const {
return !foreground_colors_.isEmpty();
}
void PlaylistItem::SetShouldSkip(bool val) { should_skip_ = val; }
bool PlaylistItem::GetShouldSkip() const { return should_skip_; }

123
src/playlist/playlistitem.h Normal file
View File

@@ -0,0 +1,123 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTITEM_H
#define PLAYLISTITEM_H
#include "config.h"
#include <memory>
#include <QFuture>
#include <QMap>
#include <QMetaType>
#include <QStandardItem>
#include <QUrl>
#include "core/song.h"
class QAction;
class SqlRow;
class PlaylistItem : public std::enable_shared_from_this<PlaylistItem> {
public:
PlaylistItem(const QString &type) : should_skip_(false), type_(type) {}
virtual ~PlaylistItem();
static PlaylistItem* NewFromType(const QString &type);
static PlaylistItem* NewFromSongsTable(const QString &table, const Song &song);
enum Option {
Default = 0x00,
// Disables the "pause" action.
PauseDisabled = 0x01,
// Disables the seek slider.
SeekDisabled = 0x04,
};
Q_DECLARE_FLAGS(Options, Option);
virtual QString type() const { return type_; }
virtual Options options() const { return Default; }
virtual QList<QAction*> actions() { return QList<QAction*>(); }
virtual bool InitFromQuery(const SqlRow &query) = 0;
void BindToQuery(QSqlQuery* query) const;
virtual void Reload() {}
QFuture<void> BackgroundReload();
virtual Song Metadata() const = 0;
virtual QUrl Url() const = 0;
void SetTemporaryMetadata(const Song &metadata);
void ClearTemporaryMetadata();
bool HasTemporaryMetadata() const { return temp_metadata_.is_valid(); }
// Background colors.
void SetBackgroundColor(short priority, const QColor &color);
bool HasBackgroundColor(short priority) const;
void RemoveBackgroundColor(short priority);
QColor GetCurrentBackgroundColor() const;
bool HasCurrentBackgroundColor() const;
// Foreground colors.
void SetForegroundColor(short priority, const QColor &color);
bool HasForegroundColor(short priority) const;
void RemoveForegroundColor(short priority);
QColor GetCurrentForegroundColor() const;
bool HasCurrentForegroundColor() const;
// Convenience function to find out whether this item is from the local
// collection, as opposed to a device, a file on disk, or a stream.
// Remember that even if this returns true, the collection item might be
// invalid so you might want to check that its id is not equal to -1
// before actually using it.
virtual bool IsLocalCollectionItem() const { return false; }
void SetShouldSkip(bool val);
bool GetShouldSkip() const;
protected:
bool should_skip_;
enum DatabaseColumn { Column_CollectionId, Column_InternetService, };
virtual QVariant DatabaseValue(DatabaseColumn) const {
return QVariant(QVariant::String);
}
virtual Song DatabaseSongMetadata() const { return Song(); }
QString type_;
Song temp_metadata_;
QMap<short, QColor> background_colors_;
QMap<short, QColor> foreground_colors_;
};
typedef std::shared_ptr<PlaylistItem> PlaylistItemPtr;
typedef QList<PlaylistItemPtr> PlaylistItemList;
Q_DECLARE_METATYPE(PlaylistItemPtr)
Q_DECLARE_METATYPE(QList<PlaylistItemPtr>)
Q_DECLARE_OPERATORS_FOR_FLAGS(PlaylistItem::Options)
#endif // PLAYLISTITEM_H

View File

@@ -0,0 +1,39 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTITEMMIMEDATA_H
#define PLAYLISTITEMMIMEDATA_H
#include "config.h"
#include "playlistitem.h"
#include "core/mimedata.h"
class PlaylistItemMimeData : public MimeData {
Q_OBJECT
public:
PlaylistItemMimeData(const PlaylistItemPtr &item) : items_(PlaylistItemList() << item) {}
PlaylistItemMimeData(const PlaylistItemList &items) : items_(items) {}
PlaylistItemList items_;
};
#endif // PLAYLISTITEMMIMEDATA_H

View File

@@ -0,0 +1,423 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QContextMenuEvent>
#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QPainter>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include "playlist.h"
#include "playlistlistcontainer.h"
#include "playlistlistmodel.h"
#include "playlistmanager.h"
#include "ui_playlistlistcontainer.h"
#include "core/application.h"
#include "core/logging.h"
#include "core/player.h"
#include "core/iconloader.h"
class PlaylistListSortFilterModel : public QSortFilterProxyModel {
public:
explicit PlaylistListSortFilterModel(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();
}
};
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 PlaylistListSortFilterModel(this)),
loaded_icons_(false),
active_playlist_id_(-1)
{
ui_->setupUi(this);
ui_->tree->setAttribute(Qt::WA_MacShowFocusRect, 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);
}
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"));
action_remove_->setIcon(IconLoader::Load("edit-delete"));
action_save_playlist_->setIcon(IconLoader::Load("document-save"));
model_->SetIcons(IconLoader::Load("view-media-playlist"), IconLoader::Load("folder"));
// 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;
}
}
}
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()) {
QStandardItem *playlist_item = model_->NewPlaylist(p.name, p.id);
QStandardItem *parent_folder = model_->FolderByPath(p.ui_path);
parent_folder->appendRow(playlist_item);
}
}
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) {
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;
}
const QString &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);
}
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::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.child(i, 0), 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(":pictures/tiny-play.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(":pictures/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);
}
}

View File

@@ -0,0 +1,102 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTLISTCONTAINER_H
#define PLAYLISTLISTCONTAINER_H
#include "config.h"
#include "playlistbackend.h"
#include <QWidget>
class QMenu;
class QSortFilterProxyModel;
class QStandardItemModel;
class Application;
class Playlist;
class PlaylistListModel;
class Ui_PlaylistListContainer;
class PlaylistListContainer : public QWidget {
Q_OBJECT
public:
PlaylistListContainer(QWidget *parent = nullptr);
~PlaylistListContainer();
void SetApplication(Application *app);
protected:
void showEvent(QShowEvent *e);
void contextMenuEvent(QContextMenuEvent *e);
private slots:
// From the UI
void NewFolderClicked();
void DeleteClicked();
void ItemDoubleClicked(const QModelIndex &index);
// From the model
void PlaylistPathChanged(int id, const QString &new_path);
// From the PlaylistManager
void PlaylistRenamed(int id, const QString &new_name);
// Add playlist if favorite == true
void AddPlaylist(int id, const QString &name, bool favorite);
void RemovePlaylist(int id);
void SavePlaylist();
void PlaylistFavoriteStateChanged(int id, bool favorite);
void CurrentChanged(Playlist *new_playlist);
void ActiveChanged(Playlist *new_playlist);
// From the Player
void ActivePlaying();
void ActivePaused();
void ActiveStopped();
private:
QStandardItem *ItemForPlaylist(const QString &name, int id);
QStandardItem *ItemForFolder(const QString &name) const;
void RecursivelySetIcons(QStandardItem *parent) const;
void RecursivelyFindPlaylists(const QModelIndex &parent, QSet<int> *ids) const;
void UpdateActiveIcon(int id, const QIcon &icon);
Application *app_;
Ui_PlaylistListContainer *ui_;
QMenu *menu_;
QAction *action_new_folder_;
QAction *action_remove_;
QAction *action_save_playlist_;
PlaylistListModel *model_;
QSortFilterProxyModel *proxy_;
bool loaded_icons_;
QIcon padded_play_icon_;
int active_playlist_id_;
};
#endif // PLAYLISTLISTCONTAINER_H

View File

@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlaylistListContainer</class>
<widget class="QWidget" name="PlaylistListContainer">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>160</width>
<height>503</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="widget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="new_folder">
<property name="toolTip">
<string>New folder</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove">
<property name="toolTip">
<string>Delete</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="save_playlist"/>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>70</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="PlaylistListView" name="tree">
<property name="sizePolicy">
<sizepolicy hsizetype="Ignored" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>AutoExpandingTreeView</class>
<extends>QTreeView</extends>
<header>widgets/autoexpandingtreeview.h</header>
</customwidget>
<customwidget>
<class>PlaylistListView</class>
<extends>AutoExpandingTreeView</extends>
<header>playlist/playlistlistview.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,238 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistlistmodel.h"
#include "core/logging.h"
#include <QMimeData>
PlaylistListModel::PlaylistListModel(QObject *parent) : QStandardItemModel(parent), dropping_rows_(false) {
connect(this, SIGNAL(dataChanged(QModelIndex, QModelIndex)), SLOT(RowsChanged(QModelIndex, QModelIndex)));
connect(this, SIGNAL(rowsAboutToBeRemoved(QModelIndex, int, int)), SLOT(RowsAboutToBeRemoved(QModelIndex, int, int)));
connect(this, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(RowsInserted(QModelIndex, int, int)));
}
void PlaylistListModel::SetIcons(const QIcon &playlist_icon, const QIcon &folder_icon) {
playlist_icon_ = playlist_icon;
folder_icon_ = folder_icon;
}
bool PlaylistListModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) {
dropping_rows_ = true;
bool ret = QStandardItemModel::dropMimeData(data, action, row, column, parent);
dropping_rows_ = false;
return ret;
}
QString PlaylistListModel::ItemPath(const QStandardItem *item) const {
QStringList components;
const QStandardItem *current = item;
while (current) {
if (current->data(Role_Type).toInt() == Type_Folder) {
components.insert(0, current->data(Qt::DisplayRole).toString());
}
current = current->parent();
}
return components.join("/");
}
void PlaylistListModel::RowsChanged(const QModelIndex &begin, const QModelIndex &end) {
AddRowMappings(begin, end);
}
void PlaylistListModel::RowsInserted(const QModelIndex &parent, int start, int end) {
// RowsChanged will take care of these when dropping.
if (!dropping_rows_) {
AddRowMappings(index(start, 0, parent), index(end, 0, parent));
}
}
void PlaylistListModel::AddRowMappings(const QModelIndex &begin, const QModelIndex &end) {
const QString parent_path = ItemPath(itemFromIndex(begin));
for (int i = begin.row(); i <= end.row(); ++i) {
const QModelIndex index = begin.sibling(i, 0);
QStandardItem *item = itemFromIndex(index);
AddRowItem(item, parent_path);
}
}
void PlaylistListModel::AddRowItem(QStandardItem *item, const QString &parent_path) {
switch (item->data(Role_Type).toInt()) {
case Type_Playlist: {
const int id = item->data(Role_PlaylistId).toInt();
playlists_by_id_[id] = item;
if (dropping_rows_) {
emit PlaylistPathChanged(id, parent_path);
}
break;
}
case Type_Folder:
for (int j = 0; j < item->rowCount(); ++j) {
QStandardItem *child_item = item->child(j);
AddRowItem(child_item, parent_path);
}
break;
}
}
void PlaylistListModel::RowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) {
for (int i = start; i <= end; ++i) {
const QModelIndex idx = index(i, 0, parent);
const QStandardItem *item = itemFromIndex(idx);
switch (idx.data(Role_Type).toInt()) {
case Type_Playlist: {
const int id = idx.data(Role_PlaylistId).toInt();
QMap<int, QStandardItem*>::Iterator it = playlists_by_id_.find(id);
if (it != playlists_by_id_.end() && it.value() == item) {
playlists_by_id_.erase(it);
}
break;
}
case Type_Folder:
break;
}
}
}
QStandardItem *PlaylistListModel::PlaylistById(int id) const {
return playlists_by_id_[id];
}
QStandardItem *PlaylistListModel::FolderByPath(const QString &path) {
if (path.isEmpty()) {
return invisibleRootItem();
}
// Walk down from the root until we find the target folder. This is pretty
// inefficient but maintaining a path -> item map is difficult.
QStandardItem *parent = invisibleRootItem();
const QStringList parts = path.split('/', QString::SkipEmptyParts);
for (const QString &part : parts) {
QStandardItem *matching_child = nullptr;
const int child_count = parent->rowCount();
for (int i = 0; i < child_count; ++i) {
if (parent->child(i)->data(Qt::DisplayRole).toString() == part) {
matching_child = parent->child(i);
break;
}
}
// Does this folder exist already?
if (matching_child) {
parent = matching_child;
}
else {
QStandardItem *child = NewFolder(part);
parent->appendRow(child);
parent = child;
}
}
return parent;
}
QStandardItem *PlaylistListModel::NewFolder(const QString &name) const {
QStandardItem *ret = new QStandardItem;
ret->setText(name);
ret->setData(PlaylistListModel::Type_Folder, PlaylistListModel::Role_Type);
ret->setIcon(folder_icon_);
ret->setFlags(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
return ret;
}
QStandardItem *PlaylistListModel::NewPlaylist(const QString &name, int id) const {
QStandardItem *ret = new QStandardItem;
ret->setText(name);
ret->setData(PlaylistListModel::Type_Playlist, PlaylistListModel::Role_Type);
ret->setData(id, PlaylistListModel::Role_PlaylistId);
ret->setIcon(playlist_icon_);
ret->setFlags(Qt::ItemIsDragEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
return ret;
}
bool PlaylistListModel::setData(const QModelIndex &index, const QVariant &value, int role) {
if (!QStandardItemModel::setData(index, value, role)) {
return false;
}
switch (index.data(Role_Type).toInt()) {
case Type_Playlist:
emit PlaylistRenamed(index.data(Role_PlaylistId).toInt(), value.toString());
break;
case Type_Folder:
// Walk all the children and modify their paths.
UpdatePathsRecursive(index);
break;
}
return true;
}
void PlaylistListModel::UpdatePathsRecursive(const QModelIndex &parent) {
switch (parent.data(Role_Type).toInt()) {
case Type_Playlist:
emit PlaylistPathChanged(parent.data(Role_PlaylistId).toInt(), ItemPath(itemFromIndex(parent)));
break;
case Type_Folder:
for (int i = 0; i < rowCount(parent); ++i) {
UpdatePathsRecursive(index(i, 0, parent));
}
break;
}
}

View File

@@ -0,0 +1,98 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTLISTMODEL_H
#define PLAYLISTLISTMODEL_H
#include "config.h"
#include <QStandardItemModel>
class PlaylistListModel : public QStandardItemModel {
Q_OBJECT
public:
PlaylistListModel(QObject *parent = nullptr);
enum Types {
Type_Folder,
Type_Playlist
};
enum Roles {
Role_Type = Qt::UserRole,
Role_PlaylistId
};
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
// These icons will be used for newly created playlists and folders.
// The caller will need to set these icons on existing items if there are any.
void SetIcons(const QIcon &playlist_icon, const QIcon &folder_icon);
const QIcon &playlist_icon() const { return playlist_icon_; }
const QIcon &folder_icon() const { return folder_icon_; }
// Walks from the given item to the root, returning the / separated path of
// all the parent folders. The path includes this item if it is a folder.
QString ItemPath(const QStandardItem *item) const;
// Finds the playlist with the given ID, returns 0 if it doesn't exist.
QStandardItem *PlaylistById(int id) const;
// Finds the folder with the given path, creating it (and its parents) if they
// do not exist. Returns invisibleRootItem() if path is empty.
QStandardItem *FolderByPath(const QString &path);
// Returns a new folder item with the given name. The item isn't added to
// the model yet.
QStandardItem *NewFolder(const QString &name) const;
// Returns a new playlist item with the given name and ID. The item isn't
// added to the model yet.
QStandardItem *NewPlaylist(const QString &name, int id) const;
// QStandardItemModel
bool setData(const QModelIndex &index, const QVariant &value, int role);
signals:
void PlaylistPathChanged(int id, const QString &new_path);
void PlaylistRenamed(int id, const QString &new_name);
private slots:
void RowsChanged(const QModelIndex &begin, const QModelIndex &end);
void RowsAboutToBeRemoved(const QModelIndex &parent, int start, int end);
void RowsInserted(const QModelIndex &parent, int start, int end);
private:
void AddRowMappings(const QModelIndex &begin, const QModelIndex &end);
void AddRowItem(QStandardItem *item, const QString &parent_path);
void UpdatePathsRecursive(const QModelIndex &parent);
private:
bool dropping_rows_;
QIcon playlist_icon_;
QIcon folder_icon_;
QMap<int, QStandardItem*> playlists_by_id_;
QMap<QString, QStandardItem*> folders_by_path_;
};
#endif // PLAYLISTLISTMODEL_H

View File

@@ -0,0 +1,51 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2013, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistlistview.h"
#include <QPainter>
PlaylistListView::PlaylistListView(QWidget *parent)
: AutoExpandingTreeView(parent) {}
void PlaylistListView::paintEvent(QPaintEvent *event) {
if (model()->rowCount() <= 0) {
QPainter p(viewport());
QRect rect(viewport()->rect());
p.setPen(palette().color(QPalette::Disabled, QPalette::Text));
QFont bold_font;
bold_font.setBold(true);
p.setFont(bold_font);
p.drawText(rect, Qt::AlignHCenter | Qt::TextWordWrap,
tr("\n\n"
"You can favorite playlists by clicking the star icon next "
"to a playlist name\n\n"
"Favorited playlists will be saved here"));
}
else {
AutoExpandingTreeView::paintEvent(event);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2013, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "widgets/autoexpandingtreeview.h"
class PlaylistListView : public AutoExpandingTreeView {
Q_OBJECT
public:
PlaylistListView(QWidget *parent = nullptr);
~PlaylistListView() {}
protected:
// QWidget
void paintEvent(QPaintEvent* event);
};

View File

@@ -0,0 +1,568 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QFileDialog>
#include <QFileInfo>
#include <QFuture>
#include <QMessageBox>
#include <QtConcurrentRun>
#include <QtDebug>
#include "playlistmanager.h"
#include "playlistbackend.h"
#include "playlistcontainer.h"
#include "playlistmanager.h"
#include "playlistsaveoptionsdialog.h"
#include "playlistview.h"
#include "core/application.h"
#include "core/logging.h"
#include "core/player.h"
#include "core/songloader.h"
#include "core/utilities.h"
#include "collection/collectionbackend.h"
#include "collection/collectionplaylistitem.h"
#include "playlistparsers/playlistparser.h"
PlaylistManager::PlaylistManager(Application *app, QObject *parent)
: PlaylistManagerInterface(app, parent),
app_(app),
playlist_backend_(nullptr),
collection_backend_(nullptr),
sequence_(nullptr),
parser_(nullptr),
playlist_container_(nullptr),
current_(-1),
active_(-1)
{
connect(app_->player(), SIGNAL(Paused()), SLOT(SetActivePaused()));
connect(app_->player(), SIGNAL(Playing()), SLOT(SetActivePlaying()));
connect(app_->player(), SIGNAL(Stopped()), SLOT(SetActiveStopped()));
}
PlaylistManager::~PlaylistManager() {
for (const Data &data : playlists_.values()) {
delete data.p;
}
}
void PlaylistManager::Init(CollectionBackend *collection_backend, PlaylistBackend *playlist_backend, PlaylistSequence *sequence, PlaylistContainer *playlist_container) {
collection_backend_ = collection_backend;
playlist_backend_ = playlist_backend;
sequence_ = sequence;
parser_ = new PlaylistParser(collection_backend, this);
playlist_container_ = playlist_container;
connect(collection_backend_, SIGNAL(SongsDiscovered(SongList)), SLOT(SongsDiscovered(SongList)));
for (const PlaylistBackend::Playlist &p : playlist_backend->GetAllOpenPlaylists()) {
AddPlaylist(p.id, p.name, p.special_type, p.ui_path, p.favorite);
}
// If no playlist exists then make a new one
if (playlists_.isEmpty()) New(tr("Playlist"));
emit PlaylistManagerInitialized();
}
QList<Playlist*> PlaylistManager::GetAllPlaylists() const {
QList<Playlist*> result;
for (const Data &data : playlists_.values()) {
result.append(data.p);
}
return result;
}
QItemSelection PlaylistManager::selection(int id) const {
QMap<int, Data>::const_iterator it = playlists_.find(id);
return it->selection;
}
Playlist *PlaylistManager::AddPlaylist(int id, const QString &name, const QString &special_type, const QString &ui_path, bool favorite) {
Playlist *ret = new Playlist(playlist_backend_, app_->task_manager(), collection_backend_, id, special_type, favorite);
ret->set_sequence(sequence_);
ret->set_ui_path(ui_path);
connect(ret, SIGNAL(CurrentSongChanged(Song)), SIGNAL(CurrentSongChanged(Song)));
connect(ret, SIGNAL(PlaylistChanged()), SLOT(OneOfPlaylistsChanged()));
connect(ret, SIGNAL(PlaylistChanged()), SLOT(UpdateSummaryText()));
connect(ret, SIGNAL(EditingFinished(QModelIndex)), SIGNAL(EditingFinished(QModelIndex)));
connect(ret, SIGNAL(Error(QString)), SIGNAL(Error(QString)));
connect(ret, SIGNAL(PlayRequested(QModelIndex)), SIGNAL(PlayRequested(QModelIndex)));
connect(playlist_container_->view(), SIGNAL(ColumnAlignmentChanged(ColumnAlignmentMap)), ret, SLOT(SetColumnAlignment(ColumnAlignmentMap)));
playlists_[id] = Data(ret, name);
emit PlaylistAdded(id, name, favorite);
if (current_ == -1) {
SetCurrentPlaylist(id);
}
if (active_ == -1) {
SetActivePlaylist(id);
}
return ret;
}
void PlaylistManager::New(const QString &name, const SongList &songs, const QString &special_type) {
if (name.isNull()) return;
int id = playlist_backend_->CreatePlaylist(name, special_type);
if (id == -1) qFatal("Couldn't create playlist");
Playlist *playlist = AddPlaylist(id, name, special_type, QString(), false);
playlist->InsertSongsOrCollectionItems(songs);
SetCurrentPlaylist(id);
// If the name is just "Playlist", append the id
if (name == tr("Playlist")) {
Rename(id, QString("%1 %2").arg(name).arg(id));
}
}
void PlaylistManager::Load(const QString &filename) {
QFileInfo info(filename);
int id = playlist_backend_->CreatePlaylist(info.baseName(), QString());
if (id == -1) {
emit Error(tr("Couldn't create playlist"));
return;
}
Playlist *playlist = AddPlaylist(id, info.baseName(), QString(), QString(), false);
QList<QUrl> urls;
playlist->InsertUrls(urls << QUrl::fromLocalFile(filename));
}
void PlaylistManager::Save(int id, const QString &filename, Playlist::Path path_type) {
if (playlists_.contains(id)) {
parser_->Save(playlist(id)->GetAllSongs(), filename, path_type);
}
else {
// Playlist is not in the playlist manager: probably save action was
// triggered
// from the left side bar and the playlist isn't loaded.
QFuture<QList<Song>> future = QtConcurrent::run(playlist_backend_, &PlaylistBackend::GetPlaylistSongs, id);
NewClosure(future, this, SLOT(ItemsLoadedForSavePlaylist(QFuture<SongList>, QString, Playlist::Path)), future, filename, path_type);
}
}
void PlaylistManager::ItemsLoadedForSavePlaylist(QFuture<SongList> future, const QString &filename, Playlist::Path path_type) {
parser_->Save(future.result(), filename, path_type);
}
void PlaylistManager::SaveWithUI(int id, const QString &playlist_name) {
QSettings settings;
settings.beginGroup(Playlist::kSettingsGroup);
QString filename = settings.value("last_save_playlist").toString();
QString extension = settings.value("last_save_extension", parser()->default_extension()).toString();
QString filter = settings.value("last_save_filter", parser()->default_filter()).toString();
QString suggested_filename = playlist_name;
suggested_filename.replace(QRegExp("\\W"), "");
qLog(Debug) << "Using extension:" << extension;
// We want to use the playlist tab name (with disallowed characters removed)
// as a default filename, but in the same directory as the last saved file.
// Strip off filename components until we find something that's a folder
forever {
QFileInfo fileinfo(filename);
if (filename.isEmpty() || fileinfo.isDir()) break;
filename = filename.section('/', 0, -2);
}
// Use the home directory as a fallback in case the path is empty.
if (filename.isEmpty()) filename = QDir::homePath();
// Add the suggested filename
filename += "/" + suggested_filename + "." + extension;
qLog(Debug) << "Suggested filename:" << filename;
filename = QFileDialog::getSaveFileName(nullptr, tr("Save playlist", "Title of the playlist save dialog."), filename, parser()->filters(), &filter);
if (filename.isNull()) {
return;
}
// Check if the file extension is valid. Fallback to the default if not.
QFileInfo info(filename);
ParserBase *parser = parser_->ParserForExtension(info.suffix());
if (!parser) {
qLog(Warning) << "Unknown file extension:" << info.suffix();
filename = info.absolutePath() + "/" + info.fileName() + "." + parser_->default_extension();
info.setFile(filename);
filter = info.suffix();
}
int p = settings.value(Playlist::kPathType, Playlist::Path_Automatic).toInt();
Playlist::Path path = static_cast<Playlist::Path>(p);
if (path == Playlist::Path_Ask_User) {
PlaylistSaveOptionsDialog optionsDialog(nullptr);
optionsDialog.setModal(true);
if (optionsDialog.exec() != QDialog::Accepted) {
return;
}
path = optionsDialog.path_type();
}
settings.setValue("last_save_playlist", filename);
settings.setValue("last_save_filter", filter);
settings.setValue("last_save_extension", info.suffix());
Save(id == -1 ? current_id() : id, filename, path);
}
void PlaylistManager::Rename(int id, const QString &new_name) {
Q_ASSERT(playlists_.contains(id));
playlist_backend_->RenamePlaylist(id, new_name);
playlists_[id].name = new_name;
emit PlaylistRenamed(id, new_name);
}
void PlaylistManager::Favorite(int id, bool favorite) {
if (playlists_.contains(id)) {
// If playlists_ contains this playlist, its means it's opened: star or
// unstar it.
playlist_backend_->FavoritePlaylist(id, favorite);
playlists_[id].p->set_favorite(favorite);
}
else {
Q_ASSERT(!favorite);
// Otherwise it means user wants to remove this playlist from the left
// panel,
// while it's not visible in the playlist tabbar either, because it has been
// closed: delete it.
playlist_backend_->RemovePlaylist(id);
}
emit PlaylistFavorited(id, favorite);
}
bool PlaylistManager::Close(int id) {
// Won't allow removing the last playlist
if (playlists_.count() <= 1 || !playlists_.contains(id)) return false;
int next_id = -1;
for (int possible_next_id : playlists_.keys()) {
if (possible_next_id != id) {
next_id = possible_next_id;
break;
}
}
if (next_id == -1) return false;
if (id == active_) SetActivePlaylist(next_id);
if (id == current_) SetCurrentPlaylist(next_id);
Data data = playlists_.take(id);
emit PlaylistClosed(id);
if (!data.p->is_favorite()) {
playlist_backend_->RemovePlaylist(id);
emit PlaylistDeleted(id);
}
delete data.p;
return true;
}
void PlaylistManager::Delete(int id) {
if (!Close(id)) {
return;
}
playlist_backend_->RemovePlaylist(id);
emit PlaylistDeleted(id);
}
void PlaylistManager::OneOfPlaylistsChanged() {
emit PlaylistChanged(qobject_cast<Playlist*>(sender()));
}
void PlaylistManager::SetCurrentPlaylist(int id) {
Q_ASSERT(playlists_.contains(id));
current_ = id;
emit CurrentChanged(current());
UpdateSummaryText();
}
void PlaylistManager::SetActivePlaylist(int id) {
Q_ASSERT(playlists_.contains(id));
// Kinda a hack: unset the current item from the old active playlist before
// setting the new one
if (active_ != -1 && active_ != id) active()->set_current_row(-1);
active_ = id;
emit ActiveChanged(active());
}
void PlaylistManager::SetActiveToCurrent() {
// Check if we need to update the active playlist.
// By calling SetActiveToCurrent, the playlist manager emits the signal
// "ActiveChanged". This signal causes the network remote module to
// send all playlists to the clients, even no change happend.
if (current_id() != active_id()) {
SetActivePlaylist(current_id());
}
}
void PlaylistManager::ClearCurrent() {
current()->Clear();
}
void PlaylistManager::ShuffleCurrent() {
current()->Shuffle();
}
void PlaylistManager::RemoveDuplicatesCurrent() {
current()->RemoveDuplicateSongs();
}
void PlaylistManager::RemoveUnavailableCurrent() {
current()->RemoveUnavailableSongs();
}
void PlaylistManager::SetActivePlaying() { active()->Playing(); }
void PlaylistManager::SetActivePaused() { active()->Paused(); }
void PlaylistManager::SetActiveStopped() { active()->Stopped(); }
void PlaylistManager::ChangePlaylistOrder(const QList<int> &ids) {
playlist_backend_->SetPlaylistOrder(ids);
}
void PlaylistManager::UpdateSummaryText() {
int tracks = current()->rowCount();
quint64 nanoseconds = 0;
int selected = 0;
// Get the length of the selected tracks
for (const QItemSelectionRange &range : playlists_[current_id()].selection) {
if (!range.isValid()) continue;
selected += range.bottom() - range.top() + 1;
for (int i = range.top() ; i <= range.bottom() ; ++i) {
qint64 length = range.model()->index(i, Playlist::Column_Length).data().toLongLong();
if (length > 0)
nanoseconds += length;
}
}
QString summary;
if (selected > 1) {
summary += tr("%1 selected of").arg(selected) + " ";
} else {
nanoseconds = current()->GetTotalLength();
}
// TODO: Make the plurals translatable
summary += tracks == 1 ? tr("1 track") : tr("%1 tracks").arg(tracks);
if (nanoseconds)
summary += " - [ " + Utilities::WordyTimeNanosec(nanoseconds) + " ]";
emit SummaryTextChanged(summary);
}
void PlaylistManager::SelectionChanged(const QItemSelection &selection) {
playlists_[current_id()].selection = selection;
UpdateSummaryText();
}
void PlaylistManager::SongsDiscovered(const SongList &songs) {
// Some songs might've changed in the collection, let's update any playlist
// items we have that match those songs
for (const Song &song : songs) {
for (const Data &data : playlists_) {
PlaylistItemList items = data.p->collection_items_by_id(song.id());
for (PlaylistItemPtr item : items) {
if (item->Metadata().directory_id() != song.directory_id()) continue;
static_cast<CollectionPlaylistItem*>(item.get())->SetMetadata(song);
data.p->ItemChanged(item);
}
}
}
}
// When Player has processed the new song chosen by the user...
void PlaylistManager::SongChangeRequestProcessed(const QUrl &url, bool valid) {
for (Playlist *playlist : GetAllPlaylists()) {
if (playlist->ApplyValidityOnCurrentSong(url, valid)) {
return;
}
}
}
void PlaylistManager::InsertUrls(int id, const QList<QUrl> &urls, int pos, bool play_now, bool enqueue) {
Q_ASSERT(playlists_.contains(id));
playlists_[id].p->InsertUrls(urls, pos, play_now, enqueue);
}
void PlaylistManager::InsertSongs(int id, const SongList &songs, int pos, bool play_now, bool enqueue) {
Q_ASSERT(playlists_.contains(id));
playlists_[id].p->InsertSongs(songs, pos, play_now, enqueue);
}
void PlaylistManager::RemoveItemsWithoutUndo(int id, const QList<int> &indices) {
Q_ASSERT(playlists_.contains(id));
playlists_[id].p->RemoveItemsWithoutUndo(indices);
}
void PlaylistManager::RemoveCurrentSong() {
active()->removeRows(active()->current_index().row(), 1);
}
void PlaylistManager::InvalidateDeletedSongs() {
for (Playlist *playlist : GetAllPlaylists()) {
playlist->InvalidateDeletedSongs();
}
}
void PlaylistManager::RemoveDeletedSongs() {
for (Playlist *playlist : GetAllPlaylists()) {
playlist->RemoveDeletedSongs();
}
}
QString PlaylistManager::GetNameForNewPlaylist(const SongList &songs) {
if (songs.isEmpty()) {
return tr("Playlist");
}
QSet<QString> artists;
QSet<QString> albums;
for (const Song &song : songs) {
artists << (song.artist().isEmpty() ? tr("Unknown") : song.artist());
albums << (song.album().isEmpty() ? tr("Unknown") : song.album());
if (artists.size() > 1) {
break;
}
}
bool various_artists = artists.size() > 1;
QString result;
if (various_artists) {
result = tr("Various artists");
}
else {
result = artists.values().first();
}
if (!various_artists && albums.size() == 1) {
result += " - " + albums.toList().first();
}
return result;
}
void PlaylistManager::Open(int id) {
if (playlists_.contains(id)) {
return;
}
const PlaylistBackend::Playlist &p = playlist_backend_->GetPlaylist(id);
if (p.id != id) {
return;
}
AddPlaylist(p.id, p.name, p.special_type, p.ui_path, p.favorite);
}
void PlaylistManager::SetCurrentOrOpen(int id) {
Open(id);
SetCurrentPlaylist(id);
}
bool PlaylistManager::IsPlaylistOpen(int id) {
return playlists_.contains(id);
}

View File

@@ -0,0 +1,243 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTMANAGER_H
#define PLAYLISTMANAGER_H
#include "config.h"
#include <QColor>
#include <QItemSelection>
#include <QMap>
#include <QObject>
#include <QSettings>
#include "core/song.h"
#include "playlist.h"
class Application;
class CollectionBackend;
class PlaylistBackend;
class PlaylistContainer;
class PlaylistParser;
class PlaylistSequence;
class TaskManager;
class QModelIndex;
class QUrl;
class PlaylistManagerInterface : public QObject {
Q_OBJECT
public:
PlaylistManagerInterface(Application *app, QObject *parent) : QObject(parent) {}
virtual int current_id() const = 0;
virtual int active_id() const = 0;
virtual Playlist *playlist(int id) const = 0;
virtual Playlist *current() const = 0;
virtual Playlist *active() const = 0;
// Returns the collection of playlists managed by this PlaylistManager.
virtual QList<Playlist*> GetAllPlaylists() const = 0;
// Grays out and reloads all deleted songs in all playlists.
virtual void InvalidateDeletedSongs() = 0;
// Removes all deleted songs from all playlists.
virtual void RemoveDeletedSongs() = 0;
virtual QItemSelection selection(int id) const = 0;
virtual QItemSelection current_selection() const = 0;
virtual QItemSelection active_selection() const = 0;
virtual QString GetPlaylistName(int index) const = 0;
virtual CollectionBackend *collection_backend() const = 0;
virtual PlaylistBackend *playlist_backend() const = 0;
virtual PlaylistSequence *sequence() const = 0;
virtual PlaylistParser *parser() const = 0;
virtual PlaylistContainer *playlist_container() const = 0;
public slots:
virtual void New(const QString& name, const SongList& songs = SongList(), const QString& special_type = QString()) = 0;
virtual void Load(const QString& filename) = 0;
virtual void Save(int id, const QString& filename, Playlist::Path path_type) = 0;
virtual void Rename(int id, const QString& new_name) = 0;
virtual void Delete(int id) = 0;
virtual bool Close(int id) = 0;
virtual void Open(int id) = 0;
virtual void ChangePlaylistOrder(const QList<int>& ids) = 0;
virtual void SongChangeRequestProcessed(const QUrl& url, bool valid) = 0;
virtual void SetCurrentPlaylist(int id) = 0;
virtual void SetActivePlaylist(int id) = 0;
virtual void SetActiveToCurrent() = 0;
virtual void SelectionChanged(const QItemSelection& selection) = 0;
// Convenience slots that defer to either current() or active()
virtual void ClearCurrent() = 0;
virtual void ShuffleCurrent() = 0;
virtual void RemoveDuplicatesCurrent() = 0;
virtual void RemoveUnavailableCurrent() = 0;
virtual void SetActivePlaying() = 0;
virtual void SetActivePaused() = 0;
virtual void SetActiveStopped() = 0;
signals:
void PlaylistManagerInitialized();
void PlaylistAdded(int id, const QString& name, bool favorite);
void PlaylistDeleted(int id);
void PlaylistClosed(int id);
void PlaylistRenamed(int id, const QString& new_name);
void PlaylistFavorited(int id, bool favorite);
void CurrentChanged(Playlist *new_playlist);
void ActiveChanged(Playlist *new_playlist);
void Error(const QString& message);
void SummaryTextChanged(const QString& summary);
// Forwarded from individual playlists
void CurrentSongChanged(const Song& song);
// Signals that one of manager's playlists has changed (new items, new
// ordering etc.) - the argument shows which.
void PlaylistChanged(Playlist *playlist);
void EditingFinished(const QModelIndex& index);
void PlayRequested(const QModelIndex& index);
};
class PlaylistManager : public PlaylistManagerInterface {
Q_OBJECT
public:
PlaylistManager(Application *app, QObject *parent = nullptr);
~PlaylistManager();
int current_id() const { return current_; }
int active_id() const { return active_; }
Playlist *playlist(int id) const { return playlists_[id].p; }
Playlist *current() const { return playlist(current_id()); }
Playlist *active() const { return playlist(active_id()); }
// Returns the collection of playlists managed by this PlaylistManager.
QList<Playlist*> GetAllPlaylists() const;
// Grays out and reloads all deleted songs in all playlists.
void InvalidateDeletedSongs();
// Removes all deleted songs from all playlists.
void RemoveDeletedSongs();
// Returns true if the playlist is open
bool IsPlaylistOpen(int id);
// Returns a pretty automatic name for playlist created from the given list of songs.
static QString GetNameForNewPlaylist(const SongList& songs);
QItemSelection selection(int id) const;
QItemSelection current_selection() const { return selection(current_id()); }
QItemSelection active_selection() const { return selection(active_id()); }
QString GetPlaylistName(int index) const { return playlists_[index].name; }
bool IsPlaylistFavorite(int index) const { return playlists_[index].p->is_favorite(); }
void Init(CollectionBackend *collection_backend, PlaylistBackend *playlist_backend, PlaylistSequence *sequence, PlaylistContainer *playlist_container);
CollectionBackend *collection_backend() const { return collection_backend_; }
PlaylistBackend *playlist_backend() const { return playlist_backend_; }
PlaylistSequence *sequence() const { return sequence_; }
PlaylistParser *parser() const { return parser_; }
PlaylistContainer *playlist_container() const { return playlist_container_; }
public slots:
void New(const QString& name, const SongList& songs = SongList(), const QString& special_type = QString());
void Load(const QString& filename);
void Save(int id, const QString& filename, Playlist::Path path_type);
// Display a file dialog to let user choose a file before saving the file
void SaveWithUI(int id, const QString& playlist_name);
void Rename(int id, const QString& new_name);
void Favorite(int id, bool favorite);
void Delete(int id);
bool Close(int id);
void Open(int id);
void ChangePlaylistOrder(const QList<int>& ids);
void SetCurrentPlaylist(int id);
void SetActivePlaylist(int id);
void SetActiveToCurrent();
void SelectionChanged(const QItemSelection& selection);
// Makes a playlist current if it's open already, or opens it and makes it current if it is hidden.
void SetCurrentOrOpen(int id);
// Convenience slots that defer to either current() or active()
void ClearCurrent();
void ShuffleCurrent();
void RemoveDuplicatesCurrent();
void RemoveUnavailableCurrent();
//void SetActiveStreamMetadata(const QUrl& url, const Song& song);
void SongChangeRequestProcessed(const QUrl& url, bool valid);
void InsertUrls(int id, const QList<QUrl>& urls, int pos = -1, bool play_now = false, bool enqueue = false);
void InsertSongs(int id, const SongList& songs, int pos = -1, bool play_now = false, bool enqueue = false);
// Removes items with given indices from the playlist. This operation is not undoable.
void RemoveItemsWithoutUndo(int id, const QList<int>& indices);
// Remove the current playing song
void RemoveCurrentSong();
private slots:
void SetActivePlaying();
void SetActivePaused();
void SetActiveStopped();
void OneOfPlaylistsChanged();
void UpdateSummaryText();
void SongsDiscovered(const SongList& songs);
void ItemsLoadedForSavePlaylist(QFuture<SongList> future, const QString& filename, Playlist::Path path_type);
private:
Playlist *AddPlaylist(int id, const QString& name, const QString& special_type, const QString& ui_path, bool favorite);
private:
struct Data {
Data(Playlist *_p = nullptr, const QString& _name = QString()) : p(_p), name(_name) {}
Playlist *p;
QString name;
QItemSelection selection;
};
Application *app_;
PlaylistBackend *playlist_backend_;
CollectionBackend *collection_backend_;
PlaylistSequence *sequence_;
PlaylistParser *parser_;
PlaylistContainer *playlist_container_;
// key = id
QMap<int, Data> playlists_;
int current_;
int active_;
};
#endif // PLAYLISTMANAGER_H

View File

@@ -0,0 +1,58 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2014, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistsaveoptionsdialog.h"
#include "ui_playlistsaveoptionsdialog.h"
#include "playlistparsers/parserbase.h"
#include <QSettings>
const char *PlaylistSaveOptionsDialog::kSettingsGroup = "PlaylistSaveOptionsDialog";
PlaylistSaveOptionsDialog::PlaylistSaveOptionsDialog(QWidget *parent) : QDialog(parent), ui(new Ui::PlaylistSaveOptionsDialog) {
ui->setupUi(this);
ui->filePaths->addItem(tr("Automatic"), Playlist::Path_Automatic);
ui->filePaths->addItem(tr("Relative"), Playlist::Path_Relative);
ui->filePaths->addItem(tr("Absolute"), Playlist::Path_Absolute);
}
PlaylistSaveOptionsDialog::~PlaylistSaveOptionsDialog() { delete ui; }
void PlaylistSaveOptionsDialog::accept() {
if (ui->remember_user_choice->isChecked()) {
QSettings s;
s.beginGroup(Playlist::kSettingsGroup);
s.setValue(Playlist::kPathType, ui->filePaths->itemData(ui->filePaths->currentIndex()).toInt());
}
QDialog::accept();
}
Playlist::Path PlaylistSaveOptionsDialog::path_type() const {
return static_cast<Playlist::Path>(ui->filePaths->itemData(ui->filePaths->currentIndex()).toInt());
}

View File

@@ -0,0 +1,50 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2014, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTSAVEOPTIONSDIALOG_H
#define PLAYLISTSAVEOPTIONSDIALOG_H
#include "config.h"
#include <QDialog>
#include "playlist.h"
namespace Ui {
class PlaylistSaveOptionsDialog;
}
class PlaylistSaveOptionsDialog : public QDialog {
Q_OBJECT
public:
explicit PlaylistSaveOptionsDialog(QWidget *parent = 0);
~PlaylistSaveOptionsDialog();
void accept();
Playlist::Path path_type() const;
private:
static const char *kSettingsGroup;
Ui::PlaylistSaveOptionsDialog* ui;
};
#endif // PLAYLISTSAVEOPTIONSDIALOG_H

View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlaylistSaveOptionsDialog</class>
<widget class="QDialog" name="PlaylistSaveOptionsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>348</width>
<height>116</height>
</rect>
</property>
<property name="windowTitle">
<string>Playlist options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,0">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>File paths</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="filePaths"/>
</item>
</layout>
</item>
<item>
<layout class="QGridLayout" name="gridLayout_2">
<property name="topMargin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QCheckBox" name="remember_user_choice">
<property name="toolTip">
<string>This can be changed later through the preferences</string>
</property>
<property name="text">
<string>Remember my choice</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>PlaylistSaveOptionsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>PlaylistSaveOptionsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,254 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistsequence.h"
#include "ui_playlistsequence.h"
#include "core/iconloader.h"
#include <QMenu>
#include <QActionGroup>
#include <QSettings>
#include <QtDebug>
#include <QPainter>
const char *PlaylistSequence::kSettingsGroup = "PlaylistSequence";
PlaylistSequence::PlaylistSequence(QWidget *parent, SettingsProvider *settings)
: QWidget(parent),
ui_(new Ui_PlaylistSequence),
settings_(settings ? settings : new DefaultSettingsProvider),
repeat_menu_(new QMenu(this)),
shuffle_menu_(new QMenu(this)),
loading_(false),
repeat_mode_(Repeat_Off),
shuffle_mode_(Shuffle_Off),
dynamic_(false)
{
ui_->setupUi(this);
// Icons
ui_->repeat->setIcon(AddDesaturatedIcon(IconLoader::Load("media-playlist-repeat")));
ui_->shuffle->setIcon(AddDesaturatedIcon(IconLoader::Load("media-playlist-shuffle")));
// Remove arrow indicators
ui_->repeat->setStyleSheet("QToolButton::menu-indicator { image: none; }");
ui_->shuffle->setStyleSheet("QToolButton::menu-indicator { image: none; }");
settings_->set_group(kSettingsGroup);
QActionGroup *repeat_group = new QActionGroup(this);
repeat_group->addAction(ui_->action_repeat_off);
repeat_group->addAction(ui_->action_repeat_track);
repeat_group->addAction(ui_->action_repeat_album);
repeat_group->addAction(ui_->action_repeat_playlist);
repeat_group->addAction(ui_->action_repeat_onebyone);
repeat_group->addAction(ui_->action_repeat_intro);
repeat_menu_->addActions(repeat_group->actions());
ui_->repeat->setMenu(repeat_menu_);
QActionGroup *shuffle_group = new QActionGroup(this);
shuffle_group->addAction(ui_->action_shuffle_off);
shuffle_group->addAction(ui_->action_shuffle_all);
shuffle_group->addAction(ui_->action_shuffle_inside_album);
shuffle_group->addAction(ui_->action_shuffle_albums);
shuffle_menu_->addActions(shuffle_group->actions());
ui_->shuffle->setMenu(shuffle_menu_);
connect(repeat_group, SIGNAL(triggered(QAction*)), SLOT(RepeatActionTriggered(QAction*)));
connect(shuffle_group, SIGNAL(triggered(QAction*)), SLOT(ShuffleActionTriggered(QAction*)));
Load();
}
PlaylistSequence::~PlaylistSequence() {
delete ui_;
}
void PlaylistSequence::Load() {
loading_ = true; // Stops these setter functions calling Save()
SetShuffleMode(ShuffleMode(settings_->value("shuffle_mode", Shuffle_Off).toInt()));
SetRepeatMode(RepeatMode(settings_->value("repeat_mode", Repeat_Off).toInt()));
loading_ = false;
}
void PlaylistSequence::Save() {
if (loading_) return;
settings_->setValue("shuffle_mode", shuffle_mode_);
settings_->setValue("repeat_mode", repeat_mode_);
}
QIcon PlaylistSequence::AddDesaturatedIcon(const QIcon &icon) {
QIcon ret;
for (const QSize &size : icon.availableSizes()) {
QPixmap on(icon.pixmap(size));
QPixmap off(DesaturatedPixmap(on));
ret.addPixmap(off, QIcon::Normal, QIcon::Off);
ret.addPixmap(on, QIcon::Normal, QIcon::On);
}
return ret;
}
QPixmap PlaylistSequence::DesaturatedPixmap(const QPixmap &pixmap) {
QPixmap ret(pixmap.size());
ret.fill(Qt::transparent);
QPainter p(&ret);
p.setOpacity(0.5);
p.drawPixmap(0, 0, pixmap);
p.end();
return ret;
}
void PlaylistSequence::RepeatActionTriggered(QAction *action) {
RepeatMode mode = Repeat_Off;
if (action == ui_->action_repeat_track) mode = Repeat_Track;
if (action == ui_->action_repeat_album) mode = Repeat_Album;
if (action == ui_->action_repeat_playlist) mode = Repeat_Playlist;
if (action == ui_->action_repeat_onebyone) mode = Repeat_OneByOne;
if (action == ui_->action_repeat_intro) mode = Repeat_Intro;
SetRepeatMode(mode);
}
void PlaylistSequence::ShuffleActionTriggered(QAction *action) {
ShuffleMode mode = Shuffle_Off;
if (action == ui_->action_shuffle_all) mode = Shuffle_All;
if (action == ui_->action_shuffle_inside_album) mode = Shuffle_InsideAlbum;
if (action == ui_->action_shuffle_albums) mode = Shuffle_Albums;
SetShuffleMode(mode);
}
void PlaylistSequence::SetRepeatMode(RepeatMode mode) {
ui_->repeat->setChecked(mode != Repeat_Off);
switch(mode) {
case Repeat_Off: ui_->action_repeat_off->setChecked(true); break;
case Repeat_Track: ui_->action_repeat_track->setChecked(true); break;
case Repeat_Album: ui_->action_repeat_album->setChecked(true); break;
case Repeat_Playlist: ui_->action_repeat_playlist->setChecked(true); break;
case Repeat_OneByOne: ui_->action_repeat_onebyone->setChecked(true); break;
case Repeat_Intro: ui_->action_repeat_intro->setChecked(true); break;
}
if (mode != repeat_mode_) {
repeat_mode_ = mode;
emit RepeatModeChanged(mode);
}
Save();
}
void PlaylistSequence::SetShuffleMode(ShuffleMode mode) {
ui_->shuffle->setChecked(mode != Shuffle_Off);
switch (mode) {
case Shuffle_Off: ui_->action_shuffle_off->setChecked(true); break;
case Shuffle_All: ui_->action_shuffle_all->setChecked(true); break;
case Shuffle_InsideAlbum: ui_->action_shuffle_inside_album->setChecked(true); break;
case Shuffle_Albums: ui_->action_shuffle_albums->setChecked(true); break;
}
if (mode != shuffle_mode_) {
shuffle_mode_ = mode;
emit ShuffleModeChanged(mode);
}
Save();
}
void PlaylistSequence::SetUsingDynamicPlaylist(bool dynamic) {
dynamic_ = dynamic;
const QString not_available(tr("Not available while using a dynamic playlist"));
setEnabled(!dynamic);
ui_->shuffle->setToolTip(dynamic ? not_available : tr("Shuffle"));
ui_->repeat->setToolTip(dynamic ? not_available : tr("Repeat"));
}
PlaylistSequence::ShuffleMode PlaylistSequence::shuffle_mode() const {
return dynamic_ ? Shuffle_Off : shuffle_mode_;
}
PlaylistSequence::RepeatMode PlaylistSequence::repeat_mode() const {
return dynamic_ ? Repeat_Off : repeat_mode_;
}
//called from global shortcut
void PlaylistSequence::CycleShuffleMode() {
ShuffleMode mode = Shuffle_Off;
//we cycle through the shuffle modes
switch (shuffle_mode()) {
case Shuffle_Off: mode = Shuffle_All; break;
case Shuffle_All: mode = Shuffle_InsideAlbum; break;
case Shuffle_InsideAlbum: mode = Shuffle_Albums; break;
case Shuffle_Albums: break;
}
SetShuffleMode(mode);
}
//called from global shortcut
void PlaylistSequence::CycleRepeatMode() {
RepeatMode mode = Repeat_Off;
//we cycle through the repeat modes
switch (repeat_mode()) {
case Repeat_Off: mode = Repeat_Track; break;
case Repeat_Track: mode = Repeat_Album; break;
case Repeat_Album: mode = Repeat_Playlist; break;
case Repeat_Playlist: mode = Repeat_OneByOne; break;
case Repeat_OneByOne: mode = Repeat_Intro; break;
case Repeat_Intro:
break;
}
SetRepeatMode(mode);
}

View File

@@ -0,0 +1,100 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTSEQUENCE_H
#define PLAYLISTSEQUENCE_H
#include "config.h"
#include <memory>
#include <QWidget>
#include "core/settingsprovider.h"
class QMenu;
class Ui_PlaylistSequence;
class PlaylistSequence : public QWidget {
Q_OBJECT
public:
PlaylistSequence(QWidget *parent = nullptr, SettingsProvider *settings = 0);
~PlaylistSequence();
enum RepeatMode {
Repeat_Off = 0,
Repeat_Track = 1,
Repeat_Album = 2,
Repeat_Playlist = 3,
Repeat_OneByOne = 4,
Repeat_Intro = 5,
};
enum ShuffleMode {
Shuffle_Off = 0,
Shuffle_All = 1,
Shuffle_InsideAlbum = 2,
Shuffle_Albums = 3,
};
static const char *kSettingsGroup;
RepeatMode repeat_mode() const;
ShuffleMode shuffle_mode() const;
QMenu *repeat_menu() const { return repeat_menu_; }
QMenu *shuffle_menu() const { return shuffle_menu_; }
public slots:
void SetRepeatMode(PlaylistSequence::RepeatMode mode);
void SetShuffleMode(PlaylistSequence::ShuffleMode mode);
void CycleShuffleMode();
void CycleRepeatMode();
void SetUsingDynamicPlaylist(bool dynamic);
signals:
void RepeatModeChanged(PlaylistSequence::RepeatMode mode);
void ShuffleModeChanged(PlaylistSequence::ShuffleMode mode);
private slots:
void RepeatActionTriggered(QAction *);
void ShuffleActionTriggered(QAction *);
private:
void Load();
void Save();
static QIcon AddDesaturatedIcon(const QIcon& icon);
static QPixmap DesaturatedPixmap(const QPixmap& pixmap);
private:
Ui_PlaylistSequence *ui_;
std::unique_ptr<SettingsProvider> settings_;
QMenu *repeat_menu_;
QMenu *shuffle_menu_;
bool loading_;
RepeatMode repeat_mode_;
ShuffleMode shuffle_mode_;
bool dynamic_;
};
#endif // PLAYLISTSEQUENCE_H

View File

@@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PlaylistSequence</class>
<widget class="QWidget" name="PlaylistSequence">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>80</width>
<height>37</height>
</rect>
</property>
<property name="styleSheet">
<string notr="true">QToolButton, QToolButton:hover, QToolButton:pressed {
border: 0px;
background: transparent;
}
</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="repeat">
<property name="toolTip">
<string>Repeat</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="shuffle">
<property name="toolTip">
<string>Shuffle</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
</layout>
<action name="action_repeat_off">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Don't repeat</string>
</property>
</action>
<action name="action_repeat_track">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Repeat track</string>
</property>
</action>
<action name="action_repeat_album">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Repeat album</string>
</property>
</action>
<action name="action_repeat_playlist">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Repeat playlist</string>
</property>
</action>
<action name="action_repeat_onebyone">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Stop after each track</string>
</property>
</action>
<action name="action_repeat_intro">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Intro tracks</string>
</property>
</action>
<action name="action_shuffle_off">
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
<property name="text">
<string>Don't shuffle</string>
</property>
</action>
<action name="action_shuffle_inside_album">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Shuffle tracks in this album</string>
</property>
</action>
<action name="action_shuffle_all">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Shuffle all</string>
</property>
</action>
<action name="action_shuffle_albums">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Shuffle albums</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,432 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlist.h"
#include "playlistmanager.h"
#include "playlisttabbar.h"
#include "playlistview.h"
#include "songmimedata.h"
#include "core/logging.h"
#include "core/iconloader.h"
#include "widgets/renametablineedit.h"
#include "widgets/favoritewidget.h"
#include <QCheckBox>
#include <QContextMenuEvent>
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QToolTip>
const char *PlaylistTabBar::kSettingsGroup = "PlaylistTabBar";
PlaylistTabBar::PlaylistTabBar(QWidget *parent)
: QTabBar(parent),
manager_(nullptr),
menu_(new QMenu(this)),
menu_index_(-1),
suppress_current_changed_(false),
initialized_(false),
rename_editor_(new RenameTabLineEdit(this)) {
setAcceptDrops(true);
setElideMode(Qt::ElideRight);
setUsesScrollButtons(true);
setTabsClosable(true);
close_ = menu_->addAction(IconLoader::Load("list-remove"), tr("Close playlist"), this, SLOT(Close()));
rename_ = menu_->addAction(IconLoader::Load("edit-rename"), tr("Rename playlist..."), this, SLOT(Rename()));
save_ = menu_->addAction(IconLoader::Load("document-save"), tr("Save playlist..."), this, SLOT(Save()));
menu_->addSeparator();
rename_editor_->setVisible(false);
connect(rename_editor_, SIGNAL(editingFinished()), SLOT(RenameInline()));
connect(rename_editor_, SIGNAL(EditingCanceled()), SLOT(HideEditor()));
connect(this, SIGNAL(currentChanged(int)), SLOT(CurrentIndexChanged(int)));
connect(this, SIGNAL(tabMoved(int, int)), SLOT(TabMoved()));
// We can't just emit Close signal, we need to extract the playlist id first
connect(this, SIGNAL(tabCloseRequested(int)), SLOT(CloseFromTabIndex(int)));
}
void PlaylistTabBar::SetActions(QAction *new_playlist, QAction *load_playlist) {
menu_->insertAction(0, new_playlist);
menu_->insertAction(0, load_playlist);
new_ = new_playlist;
}
void PlaylistTabBar::SetManager(PlaylistManager *manager) {
manager_ = manager;
connect(manager_, SIGNAL(PlaylistFavorited(int, bool)), SLOT(PlaylistFavoritedSlot(int, bool)));
connect(manager_, SIGNAL(PlaylistManagerInitialized()), this, SLOT(PlaylistManagerInitialized()));
}
void PlaylistTabBar::PlaylistManagerInitialized() {
// Signal that we are done loading and thus further changes should be
// committed to the db.
initialized_ = true;
disconnect(manager_, SIGNAL(PlaylistManagerInitialized()), this, SLOT(PlaylistManagerInitialized()));
}
void PlaylistTabBar::contextMenuEvent(QContextMenuEvent *e) {
//we need to finish the renaming action before showing context menu
if (rename_editor_->isVisible()) {
//discard any change
HideEditor();
}
menu_index_ = tabAt(e->pos());
rename_->setEnabled(menu_index_ != -1);
close_->setEnabled(menu_index_ != -1 && count() > 1);
save_->setEnabled(menu_index_ != -1);
menu_->popup(e->globalPos());
}
void PlaylistTabBar::mouseReleaseEvent(QMouseEvent *e) {
if (e->button() == Qt::MidButton) {
// Update menu index
menu_index_ = tabAt(e->pos());
Close();
}
QTabBar::mouseReleaseEvent(e);
}
void PlaylistTabBar::mouseDoubleClickEvent(QMouseEvent *e) {
int index = tabAt(e->pos());
// discard a double click with the middle button
if (e->button() != Qt::MidButton) {
if (index == -1) {
new_->activate(QAction::Trigger);
}
else {
//update current tab
menu_index_ = index;
//set position
rename_editor_->setGeometry(tabRect(index));
rename_editor_->setText(tabText(index));
rename_editor_->setVisible(true);
rename_editor_->setFocus();
}
}
QTabBar::mouseDoubleClickEvent(e);
}
void PlaylistTabBar::Rename() {
if (menu_index_ == -1) return;
QString name = tabText(menu_index_);
name = QInputDialog::getText(this, tr("Rename playlist"), tr("Enter a new name for this playlist"), QLineEdit::Normal, name);
if (name.isNull()) return;
emit Rename(tabData(menu_index_).toInt(), name);
}
void PlaylistTabBar::RenameInline() {
emit Rename(tabData(menu_index_).toInt(), rename_editor_->text());
HideEditor();
}
void PlaylistTabBar::HideEditor() {
//editingFinished() will be called twice due to Qt bug #40, so we reuse the same instance, don't delete it
rename_editor_->setVisible(false);
// hack to give back focus to playlist view
manager_->SetCurrentPlaylist(manager_->current()->id());
}
void PlaylistTabBar::Close() {
if (menu_index_ == -1) return;
const int playlist_id = tabData(menu_index_).toInt();
QSettings s;
s.beginGroup(kSettingsGroup);
const bool ask_for_delete = s.value("warn_close_playlist", true).toBool();
if (ask_for_delete && !manager_->IsPlaylistFavorite(playlist_id) && !manager_->playlist(playlist_id)->GetAllSongs().empty()) {
QMessageBox confirmation_box;
confirmation_box.setWindowIcon(QIcon(":/icons/64x64/strawberry.png"));
confirmation_box.setWindowTitle(tr("Remove playlist"));
confirmation_box.setIcon(QMessageBox::Question);
confirmation_box.setText(
tr("You are about to remove a playlist which is not part of your "
"favorite playlists: "
"the playlist will be deleted (this action cannot be undone). \n"
"Are you sure you want to continue?"));
confirmation_box.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
QCheckBox dont_prompt_again(tr("Warn me when closing a playlist tab"), &confirmation_box);
dont_prompt_again.setChecked(ask_for_delete);
dont_prompt_again.blockSignals(true);
dont_prompt_again.setToolTip(tr("This option can be changed in the \"Behavior\" preferences"));
QGridLayout *grid = qobject_cast<QGridLayout*>(confirmation_box.layout());
QDialogButtonBox *buttons = confirmation_box.findChild<QDialogButtonBox*>();
if (grid && buttons) {
const int index = grid->indexOf(buttons);
int row, column, row_span, column_span = 0;
grid->getItemPosition(index, &row, &column, &row_span, &column_span);
QLayoutItem *buttonsItem = grid->takeAt(index);
grid->addWidget(&dont_prompt_again, row, column, row_span, column_span, Qt::AlignLeft | Qt::AlignTop);
grid->addItem(buttonsItem, ++row, column, row_span, column_span);
}
else {
confirmation_box.addButton(&dont_prompt_again, QMessageBox::ActionRole);
}
if (confirmation_box.exec() != QMessageBox::Yes) {
return;
}
// If user changed the pref, save the new one
if (dont_prompt_again.isChecked() != ask_for_delete) {
s.setValue("warn_close_playlist", dont_prompt_again.isChecked());
}
}
// Close the playlist. If the playlist is not a favorite playlist, it will be
// deleted, as it will not be visible after being closed. Otherwise, the tab
// is closed but the playlist still exists and can be resurrected from the
// "Playlists" tab.
emit Close(playlist_id);
// Select the nearest tab.
if (menu_index_ > 1) {
setCurrentIndex(menu_index_ - 1);
}
// Update playlist tab order/visibility
TabMoved();
}
void PlaylistTabBar::CloseFromTabIndex(int index) {
// Update the global index
menu_index_ = index;
Close();
}
void PlaylistTabBar::Save() {
if (menu_index_ == -1) return;
emit Save(tabData(menu_index_).toInt());
}
int PlaylistTabBar::current_id() const {
if (currentIndex() == -1) return -1;
return tabData(currentIndex()).toInt();
}
int PlaylistTabBar::index_of(int id) const {
for (int i = 0; i < count(); ++i) {
if (tabData(i).toInt() == id) {
return i;
}
}
return -1;
}
void PlaylistTabBar::set_current_id(int id) { setCurrentIndex(index_of(id)); }
int PlaylistTabBar::id_of(int index) const {
if (index < 0 || index >= count()) {
qLog(Warning) << "Playlist tab index requested is out of bounds!";
return 0;
}
return tabData(index).toInt();
}
void PlaylistTabBar::set_icon_by_id(int id, const QIcon &icon) {
setTabIcon(index_of(id), icon);
}
void PlaylistTabBar::RemoveTab(int id) {
removeTab(index_of(id));
}
void PlaylistTabBar::set_text_by_id(int id, const QString &text) {
setTabText(index_of(id), text);
setTabToolTip(index_of(id), text);
}
void PlaylistTabBar::CurrentIndexChanged(int index) {
if (!suppress_current_changed_) emit CurrentIdChanged(tabData(index).toInt());
}
void PlaylistTabBar::InsertTab(int id, int index, const QString &text,
bool favorite) {
suppress_current_changed_ = true;
insertTab(index, text);
setTabData(index, id);
setTabToolTip(index, text);
FavoriteWidget *widget = new FavoriteWidget(id, favorite);
widget->setToolTip(
tr("Click here to favorite this playlist so it will be saved and remain accessible"
"through the \"Playlists\" panel on the left side bar"));
connect(widget, SIGNAL(FavoriteStateChanged(int, bool)), SIGNAL(PlaylistFavorited(int, bool)));
setTabButton(index, QTabBar::LeftSide, widget);
suppress_current_changed_ = false;
// If we are still starting up, we don't need to do this, as the
// tab ordering after startup will be the same as was already in the db.
if (initialized_) {
if (currentIndex() == index) emit CurrentIdChanged(id);
// Update playlist tab order/visibility
TabMoved();
}
}
void PlaylistTabBar::TabMoved() {
QList<int> ids;
for (int i = 0; i < count(); ++i) {
ids << tabData(i).toInt();
}
emit PlaylistOrderChanged(ids);
}
void PlaylistTabBar::dragEnterEvent(QDragEnterEvent *e) {
if (e->mimeData()->hasUrls() || e->mimeData()->hasFormat(Playlist::kRowsMimetype) || qobject_cast<const MimeData*>(e->mimeData())) {
e->acceptProposedAction();
}
}
void PlaylistTabBar::dragMoveEvent(QDragMoveEvent *e) {
drag_hover_tab_ = tabAt(e->pos());
if (drag_hover_tab_ != -1) {
e->setDropAction(Qt::CopyAction);
e->accept(tabRect(drag_hover_tab_));
if (!drag_hover_timer_.isActive())
drag_hover_timer_.start(kDragHoverTimeout, this);
}
else {
drag_hover_timer_.stop();
}
}
void PlaylistTabBar::dragLeaveEvent(QDragLeaveEvent*) {
drag_hover_timer_.stop();
}
void PlaylistTabBar::timerEvent(QTimerEvent *e) {
QTabBar::timerEvent(e);
if (e->timerId() == drag_hover_timer_.timerId()) {
drag_hover_timer_.stop();
if (drag_hover_tab_ != -1) setCurrentIndex(drag_hover_tab_);
}
}
void PlaylistTabBar::dropEvent(QDropEvent *e) {
if (drag_hover_tab_ == -1) {
const MimeData *mime_data = qobject_cast<const MimeData*>(e->mimeData());
if(mime_data && !mime_data->name_for_new_playlist_.isEmpty()) {
manager_->New(mime_data->name_for_new_playlist_);
}
else {
manager_->New(tr("Playlist"));
}
setCurrentIndex(count() - 1);
}
else {
setCurrentIndex(drag_hover_tab_);
}
manager_->current()->dropMimeData(e->mimeData(), e->proposedAction(), -1, 0, QModelIndex());
}
bool PlaylistTabBar::event(QEvent *e) {
switch (e->type()) {
case QEvent::ToolTip: {
QHelpEvent *he = static_cast<QHelpEvent*>(e);
QRect displayed_tab;
QSize real_tab;
bool is_elided = false;
real_tab = tabSizeHint(tabAt(he->pos()));
displayed_tab = tabRect(tabAt(he->pos()));
// Check whether the tab is elided or not
is_elided = displayed_tab.width() < real_tab.width();
if (!is_elided) {
// If it's not elided, don't show the tooltip
QToolTip::hideText();
}
else {
QToolTip::showText(he->globalPos(), tabToolTip(tabAt(he->pos())));
}
return true;
}
default:
return QTabBar::event(e);
}
}
void PlaylistTabBar::PlaylistFavoritedSlot(int id, bool favorite) {
const int index = index_of(id);
FavoriteWidget *favorite_widget = qobject_cast<FavoriteWidget*>(tabButton(index, QTabBar::LeftSide));
if (favorite_widget) {
favorite_widget->SetFavorite(favorite);
}
}

View File

@@ -0,0 +1,113 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTTABBAR_H
#define PLAYLISTTABBAR_H
#include "config.h"
#include <QBasicTimer>
#include <QIcon>
#include <QTabBar>
class PlaylistManager;
class RenameTabLineEdit;
class QMenu;
class PlaylistTabBar : public QTabBar {
Q_OBJECT
public:
PlaylistTabBar(QWidget *parent = nullptr);
static const int kDragHoverTimeout = 500;
static const char *kSettingsGroup;
void SetActions(QAction *new_playlist, QAction *load_playlist);
void SetManager(PlaylistManager *manager);
// We use IDs to refer to tabs so the tabs can be moved around (and their indexes change).
int index_of(int id) const;
int current_id() const;
int id_of(int index) const;
// Utility functions that use IDs rather than indexes
void set_current_id(int id);
void set_icon_by_id(int id, const QIcon &icon);
void set_text_by_id(int id, const QString &text);
void RemoveTab(int id);
void InsertTab(int id, int index, const QString &text, bool favorite);
signals:
void CurrentIdChanged(int id);
void Rename(int id, const QString &name);
void Close(int id);
void Save(int id);
void PlaylistOrderChanged(const QList<int> &ids);
void PlaylistFavorited(int id, bool favorite);
protected:
void contextMenuEvent(QContextMenuEvent *e);
void mouseReleaseEvent(QMouseEvent *e);
void mouseDoubleClickEvent(QMouseEvent *e);
void dragEnterEvent(QDragEnterEvent *e);
void dragMoveEvent(QDragMoveEvent *e);
void dragLeaveEvent(QDragLeaveEvent *e);
void dropEvent(QDropEvent *e);
void timerEvent(QTimerEvent *e);
bool event(QEvent *e);
private slots:
void CurrentIndexChanged(int index);
void Rename();
void RenameInline();
void HideEditor();
void Close();
void CloseFromTabIndex(int index);
// Used when playlist's favorite flag isn't changed from the favorite widget (e.g. from the playlistlistcontainer): will update the favorite widget
void PlaylistFavoritedSlot(int id, bool favorite);
// Used to signal that the playlist manager is done starting up
void PlaylistManagerInitialized();
void TabMoved();
void Save();
private:
PlaylistManager *manager_;
QMenu *menu_;
int menu_index_;
QAction *new_;
QAction *rename_;
QAction *close_;
QAction *save_;
QBasicTimer drag_hover_timer_;
int drag_hover_tab_;
bool suppress_current_changed_;
bool initialized_;
// Editor for inline renaming
RenameTabLineEdit *rename_editor_;
};
#endif // PLAYLISTTABBAR_H

View File

@@ -0,0 +1,128 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistundocommands.h"
#include "playlist.h"
namespace PlaylistUndoCommands {
Base::Base(Playlist* playlist) : QUndoCommand(0), playlist_(playlist) {}
InsertItems::InsertItems(Playlist *playlist, const PlaylistItemList &items, int pos, bool enqueue)
: Base(playlist),
items_(items),
pos_(pos),
enqueue_(enqueue)
{
setText(tr("add %n songs", "", items_.count()));
}
void InsertItems::redo() {
playlist_->InsertItemsWithoutUndo(items_, pos_, enqueue_);
}
void InsertItems::undo() {
const int start = pos_ == -1 ? playlist_->rowCount() - items_.count() : pos_;
playlist_->RemoveItemsWithoutUndo(start, items_.count());
}
bool InsertItems::UpdateItem(const PlaylistItemPtr &updated_item) {
for (int i = 0; i < items_.size(); i++) {
PlaylistItemPtr item = items_[i];
if (item->Metadata().url() == updated_item->Metadata().url()) {
items_[i] = updated_item;
return true;
}
}
return false;
}
RemoveItems::RemoveItems(Playlist *playlist, int pos, int count) : Base(playlist) {
setText(tr("remove %n songs", "", count));
ranges_ << Range(pos, count);
}
void RemoveItems::redo() {
for (int i = 0; i < ranges_.count(); ++i)
ranges_[i].items_ =
playlist_->RemoveItemsWithoutUndo(ranges_[i].pos_, ranges_[i].count_);
}
void RemoveItems::undo() {
for (int i = ranges_.count() - 1; i >= 0; --i)
playlist_->InsertItemsWithoutUndo(ranges_[i].items_, ranges_[i].pos_);
}
bool RemoveItems::mergeWith(const QUndoCommand *other) {
const RemoveItems* remove_command = static_cast<const RemoveItems*>(other);
ranges_.append(remove_command->ranges_);
int sum = 0;
for (const Range &range : ranges_) sum += range.count_;
setText(tr("remove %n songs", "", sum));
return true;
}
MoveItems::MoveItems(Playlist *playlist, const QList<int> &source_rows, int pos)
: Base(playlist),
source_rows_(source_rows),
pos_(pos)
{
setText(tr("move %n songs", "", source_rows.count()));
}
void MoveItems::redo() {
playlist_->MoveItemsWithoutUndo(source_rows_, pos_);
}
void MoveItems::undo() {
playlist_->MoveItemsWithoutUndo(pos_, source_rows_);
}
ReOrderItems::ReOrderItems(Playlist* playlist, const PlaylistItemList &new_items)
: Base(playlist), old_items_(playlist->items_), new_items_(new_items) {}
void ReOrderItems::undo() { playlist_->ReOrderWithoutUndo(old_items_); }
void ReOrderItems::redo() { playlist_->ReOrderWithoutUndo(new_items_); }
SortItems::SortItems(Playlist* playlist, int column, Qt::SortOrder order, const PlaylistItemList &new_items)
: ReOrderItems(playlist, new_items),
column_(column),
order_(order)
{
setText(tr("sort songs"));
}
ShuffleItems::ShuffleItems(Playlist* playlist, const PlaylistItemList &new_items)
: ReOrderItems(playlist, new_items)
{
setText(tr("shuffle songs"));
}
} // namespace

View File

@@ -0,0 +1,126 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTUNDOCOMMANDS_H
#define PLAYLISTUNDOCOMMANDS_H
#include "config.h"
#include <QUndoCommand>
#include <QCoreApplication>
#include "playlistitem.h"
class Playlist;
namespace PlaylistUndoCommands {
enum Types {
Type_RemoveItems = 0,
};
class Base : public QUndoCommand {
Q_DECLARE_TR_FUNCTIONS(PlaylistUndoCommands);
public:
Base(Playlist *playlist);
protected:
Playlist *playlist_;
};
class InsertItems : public Base {
public:
InsertItems(Playlist *playlist, const PlaylistItemList &items, int pos, bool enqueue = false);
void undo();
void redo();
// When load is async, items have already been pushed, so we need to update them.
// This function try to find the equivalent item, and replace it with the
// new (completely loaded) one.
// return true if the was found (and updated), false otherwise
bool UpdateItem(const PlaylistItemPtr &updated_item);
private:
PlaylistItemList items_;
int pos_;
bool enqueue_;
};
class RemoveItems : public Base {
public:
RemoveItems(Playlist *playlist, int pos, int count);
int id() const { return Type_RemoveItems; }
void undo();
void redo();
bool mergeWith(const QUndoCommand *other);
private:
struct Range {
Range(int pos, int count) : pos_(pos), count_(count) {}
int pos_;
int count_;
PlaylistItemList items_;
};
QList<Range> ranges_;
};
class MoveItems : public Base {
public:
MoveItems(Playlist *playlist, const QList<int> &source_rows, int pos);
void undo();
void redo();
private:
QList<int> source_rows_;
int pos_;
};
class ReOrderItems : public Base {
public:
ReOrderItems(Playlist *playlist, const PlaylistItemList &new_items);
void undo();
void redo();
private:
PlaylistItemList old_items_;
PlaylistItemList new_items_;
};
class SortItems : public ReOrderItems {
public:
SortItems(Playlist *playlist, int column, Qt::SortOrder order, const PlaylistItemList &new_items);
private:
int column_;
Qt::SortOrder order_;
};
class ShuffleItems : public ReOrderItems {
public:
ShuffleItems(Playlist *playlist, const PlaylistItemList &new_items);
};
} //namespace
#endif // PLAYLISTUNDOCOMMANDS_H

File diff suppressed because it is too large Load Diff

244
src/playlist/playlistview.h Normal file
View File

@@ -0,0 +1,244 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef PLAYLISTVIEW_H
#define PLAYLISTVIEW_H
#include "config.h"
#include <memory>
#include <QBasicTimer>
#include <QProxyStyle>
#include <QTreeView>
#include "playlist.h"
class QCommonStyle;
class Application;
class CollectionBackend;
class PlaylistHeader;
class QTimeLine;
// This proxy style works around a bug/feature introduced in Qt 4.7's QGtkStyle
// that uses Gtk to paint row backgrounds, ignoring any custom brush or palette
// the caller set in the QStyleOption. That breaks our currently playing track
// animation, which relies on the background painted by Qt to be transparent.
// This proxy style uses QCommonStyle to paint the affected elements.
// This class is used by the global search view as well.
class PlaylistProxyStyle : public QProxyStyle {
public:
PlaylistProxyStyle(QStyle *base);
void drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const;
void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const;
private:
std::unique_ptr<QCommonStyle> common_style_;
};
class PlaylistView : public QTreeView {
Q_OBJECT
public:
enum BackgroundImageType {
Invalid,
Default,
None,
Custom,
Album
};
PlaylistView(QWidget *parent = nullptr);
static const int kStateVersion;
// Constants for settings: are persistent, values should not be changed
static const char *kSettingBackgroundImageType;
static const char *kSettingBackgroundImageFilename;
static const int kDefaultBlurRadius;
static const int kDefaultOpacityLevel;
static ColumnAlignmentMap DefaultColumnAlignment();
void SetApplication(Application *app);
void SetItemDelegates(CollectionBackend *backend);
void SetPlaylist(Playlist *playlist);
void RemoveSelected(bool deleting_from_disk);
void SetReadOnlySettings(bool read_only) { read_only_settings_ = read_only; }
Playlist *playlist() const { return playlist_; }
BackgroundImageType background_image_type() const { return background_image_type_; }
Qt::Alignment column_alignment(int section) const;
// QTreeView
void drawTree(QPainter *painter, const QRegion &region) const;
void drawRow(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
void setModel(QAbstractItemModel *model);
public slots:
void ReloadSettings();
void StopGlowing();
void StartGlowing();
void JumpToCurrentlyPlayingTrack();
void JumpToLastPlayedTrack();
void closeEditor(QWidget *editor, QAbstractItemDelegate::EndEditHint hint);
void SetColumnAlignment(int section, Qt::Alignment alignment);
void CopyCurrentSongToClipboard() const;
void CurrentSongChanged(const Song &new_song, const QString &uri, const QImage &cover_art);
void PlayerStopped();
signals:
void PlayItem(const QModelIndex &index);
void PlayPause();
void RightClicked(const QPoint &global_pos, const QModelIndex &index);
void SeekForward();
void SeekBackward();
void FocusOnFilterSignal(QKeyEvent *event);
void BackgroundPropertyChanged();
void ColumnAlignmentChanged(const ColumnAlignmentMap &alignment);
protected:
// QWidget
void keyPressEvent(QKeyEvent *event);
void contextMenuEvent(QContextMenuEvent *e);
void hideEvent(QHideEvent *event);
void showEvent(QShowEvent *event);
void timerEvent(QTimerEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void leaveEvent(QEvent*);
void paintEvent(QPaintEvent *event);
void dragMoveEvent(QDragMoveEvent *event);
void dragEnterEvent(QDragEnterEvent *event);
void dragLeaveEvent(QDragLeaveEvent *event);
void dropEvent(QDropEvent *event);
//void resizeEvent(QResizeEvent *event);
bool eventFilter(QObject *object, QEvent *event);
void focusInEvent(QFocusEvent *event);
// QAbstractScrollArea
void scrollContentsBy(int dx, int dy);
// QAbstractItemView
void rowsInserted(const QModelIndex &parent, int start, int end);
private slots:
void LoadGeometry();
void SaveGeometry();
void GlowIntensityChanged();
void InhibitAutoscrollTimeout();
void MaybeAutoscroll();
void InvalidateCachedCurrentPixmap();
void PlaylistDestroyed();
void SaveSettings();
void StretchChanged(bool stretch);
void FadePreviousBackgroundImage(qreal value);
private:
void ReloadBarPixmaps();
QList<QPixmap> LoadBarPixmap(const QString &filename);
void UpdateCachedCurrentRowPixmap(QStyleOptionViewItemV4 option, const QModelIndex &index);
void set_background_image_type(BackgroundImageType bg) {
background_image_type_ = bg;
emit BackgroundPropertyChanged();
}
// Save image as the background_image_ after applying some modifications
// (opacity, ...).
// Should be used instead of modifying background_image_ directly
void set_background_image(const QImage &image);
private:
static const int kGlowIntensitySteps;
static const int kAutoscrollGraceTimeout;
static const int kDropIndicatorWidth;
static const int kDropIndicatorGradientWidth;
QList<int> GetEditableColumns();
QModelIndex NextEditableIndex(const QModelIndex &current);
QModelIndex PrevEditableIndex(const QModelIndex &current);
Application *app_;
PlaylistProxyStyle *style_;
Playlist *playlist_;
PlaylistHeader *header_;
bool setting_initial_header_layout_;
bool upgrading_from_qheaderview_;
bool read_only_settings_;
int upgrading_from_version_;
BackgroundImageType background_image_type_;
// Stores the background image to be displayed. As we want this image to be
// particular (in terms of format, opacity), you should probably use
// set_background_image_type instead of modifying background_image_ directly
QImage background_image_;
int blur_radius_;
int opacity_level_;
// Used if background image is a filemane
QString background_image_filename_;
QImage current_song_cover_art_;
QPixmap cached_scaled_background_image_;
// For fading when image change
QPixmap previous_background_image_;
qreal previous_background_image_opacity_;
QTimeLine *fade_animation_;
// To know if we should redraw the background or not
int last_height_;
int last_width_;
bool force_background_redraw_;
bool glow_enabled_;
bool currently_glowing_;
QBasicTimer glow_timer_;
int glow_intensity_step_;
QModelIndex last_current_item_;
QRect last_glow_rect_;
QTimer *inhibit_autoscroll_timer_;
bool inhibit_autoscroll_;
bool currently_autoscrolling_;
int row_height_; // Used to invalidate the currenttrack_bar pixmaps
QList<QPixmap> currenttrack_bar_left_;
QList<QPixmap> currenttrack_bar_mid_;
QList<QPixmap> currenttrack_bar_right_;
QPixmap currenttrack_play_;
QPixmap currenttrack_pause_;
QRegion current_paint_region_;
QPixmap cached_current_row_;
QRect cached_current_row_rect_;
int cached_current_row_row_;
QPixmap cached_tree_;
int drop_indicator_row_;
bool drag_over_;
ColumnAlignmentMap column_alignment_;
};
#endif // PLAYLISTVIEW_H

364
src/playlist/queue.cpp Normal file
View File

@@ -0,0 +1,364 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "queue.h"
#include <QBuffer>
#include <QMimeData>
#include <QtDebug>
const char *Queue::kRowsMimetype = "application/x-strawberry-queue-rows";
Queue::Queue(QObject *parent) : QAbstractProxyModel(parent) {}
QModelIndex Queue::mapFromSource(const QModelIndex &source_index) const {
if (!source_index.isValid()) return QModelIndex();
const int source_row = source_index.row();
for (int i = 0; i < source_indexes_.count(); ++i) {
if (source_indexes_[i].row() == source_row)
return index(i, source_index.column());
}
return QModelIndex();
}
bool Queue::ContainsSourceRow(int source_row) const {
for (int i = 0; i < source_indexes_.count(); ++i) {
if (source_indexes_[i].row() == source_row) return true;
}
return false;
}
QModelIndex Queue::mapToSource(const QModelIndex &proxy_index) const {
if (!proxy_index.isValid()) return QModelIndex();
return source_indexes_[proxy_index.row()];
}
void Queue::setSourceModel(QAbstractItemModel *source_model) {
if (sourceModel()) {
disconnect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(SourceDataChanged(QModelIndex,QModelIndex)));
disconnect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(SourceLayoutChanged()));
disconnect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(SourceLayoutChanged()));
}
QAbstractProxyModel::setSourceModel(source_model);
connect(sourceModel(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(SourceDataChanged(QModelIndex,QModelIndex)));
connect(sourceModel(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(SourceLayoutChanged()));
connect(sourceModel(), SIGNAL(layoutChanged()), this, SLOT(SourceLayoutChanged()));
}
void Queue::SourceDataChanged(const QModelIndex &top_left, const QModelIndex &bottom_right) {
for (int row = top_left.row(); row <= bottom_right.row(); ++row) {
QModelIndex proxy_index = mapFromSource(sourceModel()->index(row, 0));
if (!proxy_index.isValid()) continue;
emit dataChanged(proxy_index, proxy_index);
}
}
void Queue::SourceLayoutChanged() {
for (int i = 0; i < source_indexes_.count(); ++i) {
if (!source_indexes_[i].isValid()) {
beginRemoveRows(QModelIndex(), i, i);
source_indexes_.removeAt(i);
endRemoveRows();
--i;
}
}
}
QModelIndex Queue::index(int row, int column, const QModelIndex &parent) const {
return createIndex(row, column);
}
QModelIndex Queue::parent(const QModelIndex &child) const {
return QModelIndex();
}
int Queue::rowCount(const QModelIndex &parent) const {
if (parent.isValid()) return 0;
return source_indexes_.count();
}
int Queue::columnCount(const QModelIndex&) const { return 1; }
QVariant Queue::data(const QModelIndex &proxy_index, int role) const {
QModelIndex source_index = source_indexes_[proxy_index.row()];
switch (role) {
case Playlist::Role_QueuePosition:
return proxy_index.row();
case Qt::DisplayRole: {
const QString artist = source_index.sibling(source_index.row(), Playlist::Column_Artist).data().toString();
const QString title = source_index.sibling(source_index.row(), Playlist::Column_Title).data().toString();
if (artist.isEmpty()) return title;
return QString(artist + " - " + title);
}
default:
return QVariant();
}
}
void Queue::ToggleTracks(const QModelIndexList &source_indexes) {
for (const QModelIndex &source_index : source_indexes) {
QModelIndex proxy_index = mapFromSource(source_index);
if (proxy_index.isValid()) {
// Dequeue the track
const int row = proxy_index.row();
beginRemoveRows(QModelIndex(), row, row);
source_indexes_.removeAt(row);
endRemoveRows();
}
else {
// Enqueue the track
const int row = source_indexes_.count();
beginInsertRows(QModelIndex(), row, row);
source_indexes_ << QPersistentModelIndex(source_index);
endInsertRows();
}
}
}
int Queue::PositionOf(const QModelIndex &source_index) const {
return mapFromSource(source_index).row();
}
bool Queue::is_empty() const {
return source_indexes_.isEmpty();
}
void Queue::Clear() {
if (source_indexes_.isEmpty()) return;
beginRemoveRows(QModelIndex(), 0, source_indexes_.count() - 1);
source_indexes_.clear();
endRemoveRows();
}
void Queue::Move(const QList<int> &proxy_rows, int pos) {
layoutAboutToBeChanged();
QList<QPersistentModelIndex> moved_items;
// Take the items out of the list first, keeping track of whether the
// insertion point changes
int offset = 0;
for (int row : proxy_rows) {
moved_items << source_indexes_.takeAt(row - offset);
if (pos != -1 && pos >= row) pos--;
offset++;
}
// Put the items back in
const int start = pos == -1 ? source_indexes_.count() : pos;
for (int i = start; i < start + moved_items.count(); ++i) {
source_indexes_.insert(i, moved_items[i - start]);
}
// Update persistent indexes
for (const QModelIndex &pidx : persistentIndexList()) {
const int dest_offset = proxy_rows.indexOf(pidx.row());
if (dest_offset != -1) {
// This index was moved
changePersistentIndex(pidx, index(start + dest_offset, pidx.column(), QModelIndex()));
}
else {
int d = 0;
for (int row : proxy_rows) {
if (pidx.row() > row) d--;
}
if (pidx.row() + d >= start) d += proxy_rows.count();
changePersistentIndex(pidx, index(pidx.row() + d, pidx.column(), QModelIndex()));
}
}
layoutChanged();
}
void Queue::MoveUp(int row) {
Move(QList<int>() << row, row - 1);
}
void Queue::MoveDown(int row) {
Move(QList<int>() << row, row + 2);
}
QStringList Queue::mimeTypes() const {
return QStringList() << kRowsMimetype << Playlist::kRowsMimetype;
}
Qt::DropActions Queue::supportedDropActions() const {
return Qt::MoveAction | Qt::CopyAction | Qt::LinkAction;
}
QMimeData *Queue::mimeData(const QModelIndexList &indexes) const {
QMimeData *data = new QMimeData;
QList<int> rows;
for (const QModelIndex &index : indexes) {
if (index.column() != 0) continue;
rows << index.row();
}
QBuffer buf;
buf.open(QIODevice::WriteOnly);
QDataStream stream(&buf);
stream << rows;
buf.close();
data->setData(kRowsMimetype, buf.data());
return data;
}
bool Queue::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int, const QModelIndex&) {
if (action == Qt::IgnoreAction)
return false;
if (data->hasFormat(kRowsMimetype)) {
// Dragged from the queue
QList<int> proxy_rows;
QDataStream stream(data->data(kRowsMimetype));
stream >> proxy_rows;
qStableSort(proxy_rows); // Make sure we take them in order
Move(proxy_rows, row);
}
else if (data->hasFormat(Playlist::kRowsMimetype)) {
// Dragged from the playlist
Playlist *playlist = nullptr;
QList<int> source_rows;
QDataStream stream(data->data(Playlist::kRowsMimetype));
stream.readRawData(reinterpret_cast<char*>(&playlist), sizeof(playlist));
stream >> source_rows;
QModelIndexList source_indexes;
for (int source_row : source_rows) {
const QModelIndex source_index = sourceModel()->index(source_row, 0);
const QModelIndex proxy_index = mapFromSource(source_index);
if (proxy_index.isValid()) {
// This row was already in the queue, so no need to add it again
continue;
}
source_indexes << source_index;
}
if (!source_indexes.isEmpty()) {
const int insert_point = row == -1 ? source_indexes_.count() : row;
beginInsertRows(QModelIndex(), insert_point, insert_point + source_indexes.count() - 1);
for (int i = 0 ; i < source_indexes.count() ; ++i) {
source_indexes_.insert(insert_point + i, source_indexes[i]);
}
endInsertRows();
}
}
return true;
}
Qt::ItemFlags Queue::flags(const QModelIndex &index) const {
Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
if (index.isValid())
flags |= Qt::ItemIsDragEnabled;
else
flags |= Qt::ItemIsDropEnabled;
return flags;
}
int Queue::PeekNext() const {
if (source_indexes_.isEmpty()) return -1;
return source_indexes_.first().row();
}
int Queue::TakeNext() {
if (source_indexes_.isEmpty()) return -1;
beginRemoveRows(QModelIndex(), 0, 0);
int ret = source_indexes_.takeFirst().row();
endRemoveRows();
return ret;
}
QVariant Queue::headerData(int section, Qt::Orientation orientation, int role) const {
return QVariant();
}
void Queue::Remove(QList<int> &proxy_rows) {
// order the rows
qStableSort(proxy_rows);
// reflects immediately changes in the playlist
layoutAboutToBeChanged();
int removed_rows = 0;
for (int row : proxy_rows) {
// after the first row, the row number needs to be updated
const int real_row = row - removed_rows;
beginRemoveRows(QModelIndex(), real_row, real_row);
source_indexes_.removeAt(real_row);
endRemoveRows();
removed_rows++;
}
layoutChanged();
}

79
src/playlist/queue.h Normal file
View File

@@ -0,0 +1,79 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef QUEUE_H
#define QUEUE_H
#include "config.h"
#include "playlist.h"
#include <QAbstractProxyModel>
class Queue : public QAbstractProxyModel {
Q_OBJECT
public:
Queue(QObject *parent = nullptr);
static const char *kRowsMimetype;
// Query the queue
bool is_empty() const;
int PositionOf(const QModelIndex &source_index) const;
bool ContainsSourceRow(int source_row) const;
int PeekNext() const;
// Modify the queue
int TakeNext();
void ToggleTracks(const QModelIndexList &source_indexes);
void Clear();
void Move(const QList<int> &proxy_rows, int pos);
void MoveUp(int row);
void MoveDown(int row);
void Remove(QList<int> &proxy_rows);
// QAbstractProxyModel
void setSourceModel(QAbstractItemModel *source_model);
QModelIndex mapFromSource(const QModelIndex &source_index) const;
QModelIndex mapToSource(const QModelIndex &proxy_index) const;
// QAbstractItemModel
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const;
QModelIndex parent(const QModelIndex &child) const;
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex &parent = QModelIndex()) const;
QVariant data(const QModelIndex &proxy_index, int role) const;
QVariant headerData(int section, Qt::Orientation orientation, int role) const;
QStringList mimeTypes() const;
Qt::DropActions supportedDropActions() const;
QMimeData *mimeData(const QModelIndexList &indexes) const;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent);
Qt::ItemFlags flags(const QModelIndex &index) const;
private slots:
void SourceDataChanged(const QModelIndex &top_left, const QModelIndex &bottom_right);
void SourceLayoutChanged();
private:
QList<QPersistentModelIndex> source_indexes_;
};
#endif // QUEUE_H

View File

@@ -0,0 +1,163 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlist.h"
#include "playlistdelegates.h"
#include "playlistmanager.h"
#include "queue.h"
#include "queuemanager.h"
#include "ui_queuemanager.h"
#include "core/iconloader.h"
#include <QKeySequence>
#include <QShortcut>
QueueManager::QueueManager(QWidget *parent)
: QDialog(parent),
ui_(new Ui_QueueManager),
playlists_(nullptr),
current_playlist_(nullptr) {
ui_->setupUi(this);
ui_->list->setItemDelegate(new QueuedItemDelegate(this, 0));
// Set icons on buttons
ui_->move_down->setIcon(IconLoader::Load("go-down"));
ui_->move_up->setIcon(IconLoader::Load("go-up"));
ui_->remove->setIcon(IconLoader::Load("edit-delete"));
ui_->clear->setIcon(IconLoader::Load("edit-clear-list"));
// Set a standard shortcut
ui_->remove->setShortcut(QKeySequence::Delete);
// Button connections
connect(ui_->move_down, SIGNAL(clicked()), SLOT(MoveDown()));
connect(ui_->move_up, SIGNAL(clicked()), SLOT(MoveUp()));
connect(ui_->remove, SIGNAL(clicked()), SLOT(Remove()));
connect(ui_->clear, SIGNAL(clicked()), SLOT(Clear()));
QShortcut *close = new QShortcut(QKeySequence::Close, this);
connect(close, SIGNAL(activated()), SLOT(close()));
}
QueueManager::~QueueManager() {
delete ui_;
}
void QueueManager::SetPlaylistManager(PlaylistManager *manager) {
playlists_ = manager;
connect(playlists_, SIGNAL(CurrentChanged(Playlist*)), SLOT(CurrentPlaylistChanged(Playlist*)));
CurrentPlaylistChanged(playlists_->current());
}
void QueueManager::CurrentPlaylistChanged(Playlist *playlist) {
if (current_playlist_) {
disconnect(current_playlist_->queue(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(UpdateButtonState()));
disconnect(current_playlist_->queue(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(UpdateButtonState()));
disconnect(current_playlist_->queue(), SIGNAL(layoutChanged()), this, SLOT(UpdateButtonState()));
disconnect(current_playlist_, SIGNAL(destroyed()), this, SLOT(PlaylistDestroyed()));
}
current_playlist_ = playlist;
connect(current_playlist_->queue(), SIGNAL(rowsInserted(QModelIndex,int,int)), this, SLOT(UpdateButtonState()));
connect(current_playlist_->queue(), SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SLOT(UpdateButtonState()));
connect(current_playlist_->queue(), SIGNAL(layoutChanged()), this, SLOT(UpdateButtonState()));
connect(current_playlist_, SIGNAL(destroyed()), this, SLOT(PlaylistDestroyed()));
ui_->list->setModel(current_playlist_->queue());
connect(ui_->list->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(UpdateButtonState()));
}
void QueueManager::MoveUp() {
QModelIndexList indexes = ui_->list->selectionModel()->selectedRows();
qStableSort(indexes);
if (indexes.isEmpty() || indexes.first().row() == 0) return;
for (const QModelIndex &index : indexes) {
current_playlist_->queue()->MoveUp(index.row());
}
}
void QueueManager::MoveDown() {
QModelIndexList indexes = ui_->list->selectionModel()->selectedRows();
qStableSort(indexes);
if (indexes.isEmpty() || indexes.last().row() == current_playlist_->queue()->rowCount()-1)
return;
for (int i = indexes.count() - 1; i >= 0; --i) {
current_playlist_->queue()->MoveDown(indexes[i].row());
}
}
void QueueManager::Clear() {
current_playlist_->queue()->Clear();
}
void QueueManager::Remove() {
// collect the rows to be removed
QList<int> row_list;
for (const QModelIndex &index : ui_->list->selectionModel()->selectedRows()) {
if (index.isValid()) row_list << index.row();
}
current_playlist_->queue()->Remove(row_list);
}
void QueueManager::UpdateButtonState() {
const QModelIndex current = ui_->list->selectionModel()->currentIndex();
if (current.isValid()) {
ui_->move_up->setEnabled(current.row() != 0);
ui_->move_down->setEnabled(current.row() != current_playlist_->queue()->rowCount()-1);
ui_->remove->setEnabled(true);
}
else {
ui_->move_up->setEnabled(false);
ui_->move_down->setEnabled(false);
ui_->remove->setEnabled(false);
}
ui_->clear->setEnabled(!current_playlist_->queue()->is_empty());
}
void QueueManager::PlaylistDestroyed() {
current_playlist_ = nullptr;
// We'll get another CurrentPlaylistChanged() soon
}

View File

@@ -0,0 +1,61 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef QUEUEMANAGER_H
#define QUEUEMANAGER_H
#include "config.h"
#include <QDialog>
class Playlist;
class PlaylistManager;
class Ui_QueueManager;
class QModelIndex;
class QueueManager : public QDialog {
Q_OBJECT
public:
QueueManager(QWidget *parent = nullptr);
~QueueManager();
void SetPlaylistManager(PlaylistManager *manager);
private slots:
void CurrentPlaylistChanged(Playlist *playlist);
void PlaylistDestroyed();
void UpdateButtonState();
void MoveUp();
void MoveDown();
void Remove();
void Clear();
private:
Ui_QueueManager *ui_;
PlaylistManager *playlists_;
Playlist *current_playlist_;
};
#endif // QUEUEMANAGER_H

View File

@@ -0,0 +1,195 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QueueManager</class>
<widget class="QDialog" name="QueueManager">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>582</width>
<height>363</height>
</rect>
</property>
<property name="windowTitle">
<string>Queue Manager</string>
</property>
<property name="windowIcon">
<iconset resource="../../data/data.qrc">
<normaloff>:/icons/64x64/strawberry.png</normaloff>:/icons/64x64/strawberry.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTreeView" name="list">
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="showDropIndicator" stdset="0">
<bool>true</bool>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="move_up">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move up</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="shortcut">
<string>Ctrl+Up</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="move_down">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move down</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="shortcut">
<string>Ctrl+Down</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="remove">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Remove</string>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clear">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Clear</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="shortcut">
<string>Ctrl+K</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../data/data.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>QueueManager</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>QueueManager</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,176 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include <QtConcurrentRun>
#include "playlist.h"
#include "songloaderinserter.h"
#include "core/logging.h"
#include "core/songloader.h"
#include "core/taskmanager.h"
SongLoaderInserter::SongLoaderInserter(TaskManager *task_manager, CollectionBackendInterface *collection, const Player *player)
: task_manager_(task_manager),
destination_(nullptr),
row_(-1),
play_now_(true),
enqueue_(false),
collection_(collection),
player_(player) {}
SongLoaderInserter::~SongLoaderInserter() { qDeleteAll(pending_); }
void SongLoaderInserter::Load(Playlist *destination, int row, bool play_now, bool enqueue, const QList<QUrl> &urls) {
destination_ = destination;
row_ = row;
play_now_ = play_now;
enqueue_ = enqueue;
connect(destination, SIGNAL(destroyed()), SLOT(DestinationDestroyed()));
connect(this, SIGNAL(PreloadFinished()), SLOT(InsertSongs()));
connect(this, SIGNAL(EffectiveLoadFinished(const SongList&)), destination, SLOT(UpdateItems(const SongList&)));
for (const QUrl& url : urls) {
SongLoader *loader = new SongLoader(collection_, player_, this);
SongLoader::Result ret = loader->Load(url);
if (ret == SongLoader::BlockingLoadRequired) {
pending_.append(loader);
continue;
}
if (ret == SongLoader::Success)
songs_ << loader->songs();
else
emit Error(tr("Error loading %1").arg(url.toString()));
delete loader;
}
if (pending_.isEmpty()) {
InsertSongs();
deleteLater();
}
else {
QtConcurrent::run(this, &SongLoaderInserter::AsyncLoad);
}
}
// Load audio CD tracks:
// First, we add tracks (without metadata) into the playlist
// In the meantime, MusicBrainz will be queried to get songs' metadata.
// AudioCDTagsLoaded will be called next, and playlist's items will be updated.
void SongLoaderInserter::LoadAudioCD(Playlist *destination, int row, bool play_now, bool enqueue) {
destination_ = destination;
row_ = row;
play_now_ = play_now;
enqueue_ = enqueue;
SongLoader *loader = new SongLoader(collection_, player_, this);
NewClosure(loader, SIGNAL(AudioCDTracksLoaded()), this, SLOT(AudioCDTracksLoaded(SongLoader*)), loader);
connect(loader, SIGNAL(LoadAudioCDFinished(bool)), SLOT(AudioCDTagsLoaded(bool)));
qLog(Info) << "Loading audio CD...";
SongLoader::Result ret = loader->LoadAudioCD();
if (ret == SongLoader::Error) {
emit Error(tr("Error while loading audio CD"));
delete loader;
}
// Songs will be loaded later: see AudioCDTracksLoaded and AudioCDTagsLoaded slots
}
void SongLoaderInserter::DestinationDestroyed() { destination_ = nullptr; }
void SongLoaderInserter::AudioCDTracksLoaded(SongLoader *loader) {
songs_ = loader->songs();
InsertSongs();
}
void SongLoaderInserter::AudioCDTagsLoaded(bool success) {
SongLoader *loader = qobject_cast<SongLoader*>(sender());
if (!loader || !destination_) return;
if (success)
destination_->UpdateItems(loader->songs());
else
qLog(Error) << "Error while getting audio CD metadata from MusicBrainz";
deleteLater();
}
void SongLoaderInserter::InsertSongs() {
// Insert songs (that haven't been completely loaded) to allow user to see
// and play them while not loaded completely
if (destination_) {
destination_->InsertSongsOrCollectionItems(songs_, row_, play_now_, enqueue_);
}
}
void SongLoaderInserter::AsyncLoad() {
// First, quick load raw songs.
int async_progress = 0;
int async_load_id = task_manager_->StartTask(tr("Loading tracks"));
task_manager_->SetTaskProgress(async_load_id, async_progress,
pending_.count());
for (int i = 0; i < pending_.count(); ++i) {
SongLoader *loader = pending_[i];
loader->LoadFilenamesBlocking();
task_manager_->SetTaskProgress(async_load_id, ++async_progress);
if (i == 0) {
// Load everything from the first song. It'll start playing as soon as
// we emit PreloadFinished, so it needs to have the duration set to show
// properly in the UI.
loader->LoadMetadataBlocking();
}
songs_ << loader->songs();
}
task_manager_->SetTaskFinished(async_load_id);
emit PreloadFinished();
// Songs are inserted in playlist, now load them completely.
async_progress = 0;
async_load_id = task_manager_->StartTask(tr("Loading tracks info"));
task_manager_->SetTaskProgress(async_load_id, async_progress, songs_.count());
SongList songs;
for (int i = 0; i < pending_.count(); ++i) {
SongLoader *loader = pending_[i];
if (i != 0) {
// We already did this earlier for the first song.
loader->LoadMetadataBlocking();
}
songs << loader->songs();
task_manager_->SetTaskProgress(async_load_id, songs.count());
}
task_manager_->SetTaskFinished(async_load_id);
// Replace the partially-loaded items by the new ones, fully loaded.
emit EffectiveLoadFinished(songs);
deleteLater();
}

View File

@@ -0,0 +1,78 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SONGLOADERINSERTER_H
#define SONGLOADERINSERTER_H
#include "config.h"
#include <QList>
#include <QObject>
#include <QUrl>
#include "core/song.h"
class CollectionBackendInterface;
class Player;
class Playlist;
class SongLoader;
class TaskManager;
class QModelIndex;
class SongLoaderInserter : public QObject {
Q_OBJECT
public:
SongLoaderInserter(TaskManager *task_manager, CollectionBackendInterface *collection, const Player *player);
~SongLoaderInserter();
void Load(Playlist *destination, int row, bool play_now, bool enqueue, const QList<QUrl> &urls);
void LoadAudioCD(Playlist *destination, int row, bool play_now, bool enqueue);
signals:
void Error(const QString &message);
void PreloadFinished();
void EffectiveLoadFinished(const SongList &songs);
private slots:
void DestinationDestroyed();
void AudioCDTracksLoaded(SongLoader *loader);
void AudioCDTagsLoaded(bool success);
void InsertSongs();
private:
void AsyncLoad();
private:
TaskManager *task_manager_;
Playlist *destination_;
int row_;
bool play_now_;
bool enqueue_;
SongList songs_;
QList<SongLoader*> pending_;
CollectionBackendInterface *collection_;
const Player *player_;
};
#endif // SONGLOADERINSERTER_H

View File

@@ -0,0 +1,44 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SONGMIMEDATA_H
#define SONGMIMEDATA_H
#include "config.h"
#include <QMimeData>
#include "core/mimedata.h"
#include "core/song.h"
class CollectionBackendInterface;
class SongMimeData : public MimeData {
Q_OBJECT
public:
SongMimeData() : backend(nullptr) {}
CollectionBackendInterface *backend;
SongList songs;
};
#endif // SONGMIMEDATA_H

View File

@@ -0,0 +1,56 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#include "config.h"
#include "playlistbackend.h"
#include "songplaylistitem.h"
#include "core/tagreaderclient.h"
#include "collection/sqlrow.h"
#include <QtDebug>
#include <QFile>
#include <QSettings>
SongPlaylistItem::SongPlaylistItem(const QString &type) : PlaylistItem(type) {}
SongPlaylistItem::SongPlaylistItem(const Song &song)
: PlaylistItem("File"), song_(song) {}
bool SongPlaylistItem::InitFromQuery(const SqlRow &query) {
song_.InitFromQuery(query, false, (Song::kColumns.count() + 1) * 3);
return true;
}
QUrl SongPlaylistItem::Url() const { return song_.url(); }
void SongPlaylistItem::Reload() {
if (song_.url().scheme() != "file") return;
TagReaderClient::Instance()->ReadFileBlocking(song_.url().toLocalFile(), &song_);
}
Song SongPlaylistItem::Metadata() const {
if (HasTemporaryMetadata()) return temp_metadata_;
return song_;
}

View File

@@ -0,0 +1,51 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2010, David Sansome <me@davidsansome.com>
*
* Strawberry 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.
*
* Strawberry 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 Strawberry. If not, see <http://www.gnu.org/licenses/>.
*
*/
#ifndef SONGPLAYLISTITEM_H
#define SONGPLAYLISTITEM_H
#include "config.h"
#include "playlistitem.h"
#include "core/song.h"
class SongPlaylistItem : public PlaylistItem {
public:
SongPlaylistItem(const QString& type);
SongPlaylistItem(const Song& song);
// Restores a stream- or file-related playlist item using query row.
// If it's a file related playlist item, this will restore it's CUE
// attributes (if any) but won't parse the CUE!
bool InitFromQuery(const SqlRow& query);
void Reload();
Song Metadata() const;
QUrl Url() const;
protected:
Song DatabaseSongMetadata() const { return song_; }
private:
Song song_;
};
#endif // SONGPLAYLISTITEM_H