Initial commit.
This commit is contained in:
1895
src/playlist/playlist.cpp
Normal file
1895
src/playlist/playlist.cpp
Normal file
File diff suppressed because it is too large
Load Diff
373
src/playlist/playlist.h
Normal file
373
src/playlist/playlist.h
Normal 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
|
||||
|
||||
449
src/playlist/playlistbackend.cpp
Normal file
449
src/playlist/playlistbackend.cpp
Normal 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();
|
||||
|
||||
}
|
||||
|
||||
104
src/playlist/playlistbackend.h
Normal file
104
src/playlist/playlistbackend.h
Normal 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
|
||||
448
src/playlist/playlistcontainer.cpp
Normal file
448
src/playlist/playlistcontainer.cpp
Normal 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);
|
||||
|
||||
}
|
||||
125
src/playlist/playlistcontainer.h
Normal file
125
src/playlist/playlistcontainer.h
Normal 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
|
||||
189
src/playlist/playlistcontainer.ui
Normal file
189
src/playlist/playlistcontainer.ui
Normal 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>
|
||||
475
src/playlist/playlistdelegates.cpp
Normal file
475
src/playlist/playlistdelegates.cpp
Normal 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);
|
||||
|
||||
}
|
||||
|
||||
172
src/playlist/playlistdelegates.h
Normal file
172
src/playlist/playlistdelegates.h
Normal 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
|
||||
85
src/playlist/playlistfilter.cpp
Normal file
85
src/playlist/playlistfilter.cpp
Normal 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());
|
||||
|
||||
}
|
||||
58
src/playlist/playlistfilter.h
Normal file
58
src/playlist/playlistfilter.h
Normal 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
|
||||
535
src/playlist/playlistfilterparser.cpp
Normal file
535
src/playlist/playlistfilterparser.cpp
Normal 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;
|
||||
}
|
||||
103
src/playlist/playlistfilterparser.h
Normal file
103
src/playlist/playlistfilterparser.h
Normal 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
|
||||
133
src/playlist/playlistheader.cpp
Normal file
133
src/playlist/playlistheader.cpp
Normal 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();
|
||||
}
|
||||
|
||||
71
src/playlist/playlistheader.h
Normal file
71
src/playlist/playlistheader.h
Normal 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
|
||||
|
||||
116
src/playlist/playlistitem.cpp
Normal file
116
src/playlist/playlistitem.cpp
Normal 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
123
src/playlist/playlistitem.h
Normal 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
|
||||
39
src/playlist/playlistitemmimedata.h
Normal file
39
src/playlist/playlistitemmimedata.h
Normal 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
|
||||
423
src/playlist/playlistlistcontainer.cpp
Normal file
423
src/playlist/playlistlistcontainer.cpp
Normal 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 ¤t_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);
|
||||
}
|
||||
|
||||
}
|
||||
102
src/playlist/playlistlistcontainer.h
Normal file
102
src/playlist/playlistlistcontainer.h
Normal 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
|
||||
142
src/playlist/playlistlistcontainer.ui
Normal file
142
src/playlist/playlistlistcontainer.ui
Normal 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>
|
||||
238
src/playlist/playlistlistmodel.cpp
Normal file
238
src/playlist/playlistlistmodel.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
98
src/playlist/playlistlistmodel.h
Normal file
98
src/playlist/playlistlistmodel.h
Normal 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
|
||||
51
src/playlist/playlistlistview.cpp
Normal file
51
src/playlist/playlistlistview.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
35
src/playlist/playlistlistview.h
Normal file
35
src/playlist/playlistlistview.h
Normal 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);
|
||||
};
|
||||
568
src/playlist/playlistmanager.cpp
Normal file
568
src/playlist/playlistmanager.cpp
Normal 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);
|
||||
}
|
||||
243
src/playlist/playlistmanager.h
Normal file
243
src/playlist/playlistmanager.h
Normal 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
|
||||
58
src/playlist/playlistsaveoptionsdialog.cpp
Normal file
58
src/playlist/playlistsaveoptionsdialog.cpp
Normal 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());
|
||||
}
|
||||
|
||||
50
src/playlist/playlistsaveoptionsdialog.h
Normal file
50
src/playlist/playlistsaveoptionsdialog.h
Normal 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
|
||||
108
src/playlist/playlistsaveoptionsdialog.ui
Normal file
108
src/playlist/playlistsaveoptionsdialog.ui
Normal 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>
|
||||
254
src/playlist/playlistsequence.cpp
Normal file
254
src/playlist/playlistsequence.cpp
Normal 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);
|
||||
|
||||
}
|
||||
100
src/playlist/playlistsequence.h
Normal file
100
src/playlist/playlistsequence.h
Normal 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
|
||||
155
src/playlist/playlistsequence.ui
Normal file
155
src/playlist/playlistsequence.ui
Normal 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>
|
||||
432
src/playlist/playlisttabbar.cpp
Normal file
432
src/playlist/playlisttabbar.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
113
src/playlist/playlisttabbar.h
Normal file
113
src/playlist/playlisttabbar.h
Normal 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
|
||||
128
src/playlist/playlistundocommands.cpp
Normal file
128
src/playlist/playlistundocommands.cpp
Normal 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
|
||||
126
src/playlist/playlistundocommands.h
Normal file
126
src/playlist/playlistundocommands.h
Normal 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
|
||||
1223
src/playlist/playlistview.cpp
Normal file
1223
src/playlist/playlistview.cpp
Normal file
File diff suppressed because it is too large
Load Diff
244
src/playlist/playlistview.h
Normal file
244
src/playlist/playlistview.h
Normal 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 ®ion) 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 ¤t);
|
||||
QModelIndex PrevEditableIndex(const QModelIndex ¤t);
|
||||
|
||||
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
364
src/playlist/queue.cpp
Normal 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
79
src/playlist/queue.h
Normal 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
|
||||
163
src/playlist/queuemanager.cpp
Normal file
163
src/playlist/queuemanager.cpp
Normal 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
|
||||
}
|
||||
|
||||
61
src/playlist/queuemanager.h
Normal file
61
src/playlist/queuemanager.h
Normal 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
|
||||
|
||||
195
src/playlist/queuemanager.ui
Normal file
195
src/playlist/queuemanager.ui
Normal 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>
|
||||
176
src/playlist/songloaderinserter.cpp
Normal file
176
src/playlist/songloaderinserter.cpp
Normal 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();
|
||||
|
||||
}
|
||||
|
||||
78
src/playlist/songloaderinserter.h
Normal file
78
src/playlist/songloaderinserter.h
Normal 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
|
||||
44
src/playlist/songmimedata.h
Normal file
44
src/playlist/songmimedata.h
Normal 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
|
||||
|
||||
56
src/playlist/songplaylistitem.cpp
Normal file
56
src/playlist/songplaylistitem.cpp
Normal 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_;
|
||||
}
|
||||
51
src/playlist/songplaylistitem.h
Normal file
51
src/playlist/songplaylistitem.h
Normal 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
|
||||
Reference in New Issue
Block a user