Commit 3a9b36fe authored by Matěj Laitl's avatar Matěj Laitl
Browse files

Playlist browser: drag & drop and plugged playlists cleanups

This change fixes a couple of bugs:
 * it is now possible to drop tracks & playlists to playlist providers
   in Saved Playlists
 * When a new empty playlist provider appears or disappears when Amarok is
   running, the view is correctly updated.

FEATURES:
  * "Crop playlist" functionality implemented using drag & drop.

CHANGES:
  * Drag & drop of tracks and playlists to Saved Playlists works in all cases.

BUGFIXES:
  * iPod playlists now appear even when plugged in while Amarok is running.

FEATURE: 267729
FEATURE: 211811
BUG: 289303
CCBUG: 239950
FIXED-IN: 2.6
parent f86d4eb7
......@@ -4,6 +4,7 @@ Amarok ChangeLog
Version 2.6-Beta 1
FEATURES:
* "Crop playlist" functionality implemented using drag & drop. (BR 267729, 211811)
* Added keyboard shortcut for "Edit Track Information..." (BR 173814)
Patch by Jasneet Bhatti.
* Support for embedded album covers in non-collection tracks and
......@@ -25,6 +26,7 @@ Version 2.6-Beta 1
vice versa. (BR 142579)
CHANGES:
* Drag & drop of tracks and playlists to Saved Playlists works in all cases.
* Don't show unmounted USB Mass Storage devices and make it clear when
the device is not "activated" yet.
* "Devices" in Amarok configuration -> Plugins is with other related strings
......@@ -40,6 +42,7 @@ Version 2.6-Beta 1
"1.2 GB free" is shown instead of "85% used"; thicker capacity bar.
BUGFIXES:
* iPod playlists now appear even when plugged in while Amarok is running. (BR 289303)
* Show in Media Sources actions no longer reset the collection sorting to artist/album. (BR 231858)
* Don't mark audio-CD tracks as unplayable (greyed out). (BR 285885)
* Don't misbehave when track is dropped directly to saved playlist. (BR 293295)
......
......@@ -84,6 +84,9 @@ PlaylistBrowserModel::data( const QModelIndex &index, int role ) const
KIcon icon;
int playlistCount = 0;
QList<QAction *> providerActions;
QList<Playlists::PlaylistProvider *> providers =
The::playlistManager()->getProvidersForPlaylist( playlist );
Playlists::PlaylistProvider *provider = providers.count() == 1 ? providers.first() : 0;
switch( index.column() )
{
......@@ -115,9 +118,6 @@ PlaylistBrowserModel::data( const QModelIndex &index, int role ) const
case PlaylistBrowserModel::ProviderColumn: //source
{
QList<Playlists::PlaylistProvider *> providers =
The::playlistManager()->getProvidersForPlaylist( playlist );
if( providers.count() > 1 )
{
QVariantList nameData;
......@@ -153,15 +153,8 @@ PlaylistBrowserModel::data( const QModelIndex &index, int role ) const
return providerActionsData;
}
}
else if( providers.count() )
else if( provider )
{
Playlists::PlaylistProvider *provider = providers.first();
//prevent crash
if( !provider )
{
error() << "Invalid PlaylistProvider.";
break;
}
name = description = provider->prettyName();
icon = provider->icon();
playlistCount = provider->playlistCount();
......@@ -184,7 +177,7 @@ PlaylistBrowserModel::data( const QModelIndex &index, int role ) const
case Qt::DecorationRole: return QVariant( icon );
case PlaylistBrowserModel::ByLineRole:
return i18ncp( "number of playlists from one source",
"One Playlist", "%1 playlists",
"One playlist", "%1 playlists",
playlistCount );
case PlaylistBrowserModel::ActionRole:
return QVariant::fromValue( index.column() == PlaylistBrowserModel::ProviderColumn ?
......@@ -192,6 +185,8 @@ PlaylistBrowserModel::data( const QModelIndex &index, int role ) const
case PlaylistBrowserModel::ActionCountRole:
return QVariant( index.column() == PlaylistBrowserModel::ProviderColumn ?
providerActions.count() : actionsFor( index ).count() );
case PlaylistBrowserModel::ProviderRole:
return provider ? QVariant::fromValue<Playlists::PlaylistProvider *>( provider ) : QVariant();
default: return QVariant();
}
......@@ -456,7 +451,8 @@ bool
PlaylistBrowserModel::dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int column,
const QModelIndex &parent )
{
Q_UNUSED( column );
DEBUG_BLOCK
debug() << "Dropped on" << parent << "row" << row << "column" << column << "action" << action;
if( action == Qt::IgnoreAction )
return true;
......@@ -470,6 +466,7 @@ PlaylistBrowserModel::dropMimeData( const QMimeData *data, Qt::DropAction action
if( data->hasFormat( AmarokMimeData::PLAYLIST_MIME ) )
{
// TODO: is this ever called????
Playlists::PlaylistList playlists = amarokMime->playlists();
foreach( Playlists::PlaylistPtr playlist, playlists )
......@@ -483,14 +480,20 @@ PlaylistBrowserModel::dropMimeData( const QMimeData *data, Qt::DropAction action
else if( data->hasFormat( AmarokMimeData::TRACK_MIME ) )
{
Meta::TrackList tracks = amarokMime->tracks();
if( !parent.isValid() && row == -1 )
if( !parent.isValid() && row == -1 && column == -1 )
{
debug() << "Dropped tracks on empty area: create new playlist";
debug() << "Dropped tracks on empty area: create new playlist in a default provider";
The::playlistManager()->save( tracks, Amarok::generatePlaylistName( tracks ) );
return true;
}
else if( !parent.isValid() )
{
warning() << "Dropped tracks between root items, this is not supported!";
return false;
}
else
{
debug() << "Dropped track on " << parent << " at row: " << row;
debug() << "Dropped tracks on " << parent << " at row: " << row;
Playlists::PlaylistPtr playlist = playlistFromIndex( parent );
if( !playlist )
......
......@@ -57,6 +57,7 @@ class PlaylistBrowserModel : public QAbstractItemModel,
ByLineRole, //show some additional info like count or status. Displayed under description
ActionCountRole,
ActionRole, //list of QActions for the index
ProviderRole, // pointer to associated PlaylistProvider
CustomRoleOffset //first role that can be used by sublasses for their own data
};
......
......@@ -27,15 +27,18 @@
#include <QStack>
const QString PlaylistsByProviderProxy::AMAROK_PROVIDERPROXY_INDEXES =
"application/x-amarok-providerproxy-indexes";
PlaylistsByProviderProxy::PlaylistsByProviderProxy( QAbstractItemModel *model, int column, int playlistCategory )
: QtGroupingProxy( model, QModelIndex(), column )
, m_playlistCategory( playlistCategory )
{
connect( sourceModel(), SIGNAL(renameIndex( const QModelIndex & )),
SLOT(slotRenameIndex( const QModelIndex & )) );
// we need this to track providers with no playlists
connect( The::playlistManager(), SIGNAL(providerAdded(Playlists::PlaylistProvider*,int)),
this, SLOT(slotProviderAdded(Playlists::PlaylistProvider*,int)) );
connect( The::playlistManager(), SIGNAL(providerRemoved(Playlists::PlaylistProvider*,int)),
this, SLOT(slotProviderRemoved(Playlists::PlaylistProvider*,int)) );
}
QVariant
......@@ -102,133 +105,69 @@ PlaylistsByProviderProxy::mimeData( const QModelIndexList &indexes ) const
QModelIndexList sourceIndexes;
foreach( const QModelIndex &idx, indexes )
{
debug() << idx;
if( isGroup( idx ) )
{
debug() << "is a group, add mimeData of all children";
//TODO: add originalRows of children to list
}
else
{
debug() << "is original item, add mimeData from source model";
QModelIndex originalIdx = mapToSource( idx );
if( originalIdx.isValid() )
sourceIndexes << originalIdx;
}
continue; // drags not enabled for playlist providers
QModelIndex originalIdx = mapToSource( idx );
if( originalIdx.isValid() )
sourceIndexes << originalIdx;
}
QMimeData* mime = 0;
if( !sourceIndexes.isEmpty() )
mime = sourceModel()->mimeData( sourceIndexes );
if( !mime )
mime = new QMimeData();
if( !sourceIndexes.isEmpty() )
{
QByteArray encodedIndexes = encodeMimeRows( sourceIndexes );
mime->setData( AMAROK_PROVIDERPROXY_INDEXES, encodedIndexes );
}
return mime;
if( sourceIndexes.isEmpty() )
return 0;
return sourceModel()->mimeData( sourceIndexes );
}
QByteArray
PlaylistsByProviderProxy::encodeMimeRows( const QList<QModelIndex> indexes ) const
bool
PlaylistsByProviderProxy::dropMimeData( const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent )
{
QByteArray encodedIndexes;
QDataStream stream( &encodedIndexes, QIODevice::WriteOnly );
foreach( const QModelIndex &idx, indexes )
DEBUG_BLOCK
debug() << "Dropped on" << parent << "row" << row << "column" << column << "action" << action;
if( action == Qt::IgnoreAction )
return true;
if( !isGroup( parent ) ) // drops on empty space fall here, it is okay
{
QStack<QModelIndex> indexStack;
//save the index and it's parents until we reach the rootnode so we have the complete tree.
QModelIndex i = idx;
while( i != m_rootNode )
{
indexStack.push( i );
i = i.parent();
}
//save the length of the stack first.
stream << indexStack.count();
while( !indexStack.isEmpty() )
{
QModelIndex i = indexStack.pop();
stream << i.row() << i.column();
}
QModelIndex sourceIndex = mapToSource( parent );
return sourceModel()->dropMimeData( data, action, row, column, sourceIndex );
}
return encodedIndexes;
}
QList<QModelIndex>
PlaylistsByProviderProxy::decodeMimeRows( QByteArray mimeData, QAbstractItemModel *model ) const
{
DEBUG_BLOCK
debug() << mimeData;
QList<QModelIndex> idxs;
QDataStream stream( &mimeData, QIODevice::ReadOnly );
while( !stream.atEnd() )
const AmarokMimeData *amarokData = dynamic_cast<const AmarokMimeData *>( data );
if( !amarokData )
{
QStack<QModelIndex> indexStack;
int count;
stream >> count;
//start from the rootNode and build "down" the tree
QModelIndex idx = m_rootNode;
while( count-- > 0 )
{
int row;
int column;
stream >> row >> column;
idx = model->index( row, column, idx );
}
//the last one should be the index we saved in encodeMimeRows
idxs << idx;
debug() << __PRETTY_FUNCTION__ << "supports only drag & drop originating in Amarok.";
return false;
}
return idxs;
}
bool
PlaylistsByProviderProxy::dropMimeData( const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent )
{
DEBUG_BLOCK
debug() << "dropped on " << QString("row: %1, column: %2, parent:").arg( row ).arg( column );
debug() << parent;
debug() << "With action: " << action;
if( action == Qt::IgnoreAction )
Playlists::PlaylistProvider *provider =
parent.data( PlaylistBrowserNS::PlaylistBrowserModel::ProviderRole )
.value<Playlists::PlaylistProvider *>();
if( !provider )
{
debug() << "ignored";
return true;
warning() << "Dropped tracks to a group with no (or multiple) providers!";
return false;
}
if( isGroup( parent ) )
if( amarokData->hasFormat( AmarokMimeData::PLAYLIST_MIME ) )
{
if( data->hasFormat( AMAROK_PROVIDERPROXY_INDEXES ) )
debug() << "Dropped playlists to provider" << provider->prettyName();
foreach( Playlists::PlaylistPtr pl, amarokData->playlists() )
{
QList<QModelIndex> originalIndexes =
decodeMimeRows( data->data( AMAROK_PROVIDERPROXY_INDEXES ), sourceModel() );
//set the groupedColumn data of all playlist indexes to the data of this group
//the model will understand this as a copy to the provider it's dropped on
ItemData groupData =
m_groupMaps.value( parent.row() ).value( parent.column() );
bool result = !originalIndexes.isEmpty();
foreach( const QModelIndex& originalIndex, originalIndexes )
{
QModelIndex groupedColumnIndex =
originalIndex.sibling( originalIndex.row(), m_groupedColumn );
if( !groupedColumnIndex.isValid() )
continue;
result = sourceModel()->setItemData( groupedColumnIndex, groupData ) ? result : false;
}
return result;
// few PlaylistProviders implement addPlaylist(), use save() instead:
The::playlistManager()->save( pl->tracks(), pl->name(), provider, false /* editName */ );
}
return false;
return true;
}
if( amarokData->hasFormat( AmarokMimeData::TRACK_MIME ) )
{
debug() << "Dropped tracks to provider" << provider->prettyName();
Meta::TrackList tracks = amarokData->tracks();
QString playlistName = Amarok::generatePlaylistName( tracks );
return The::playlistManager()->save( tracks, playlistName, provider );
}
QModelIndex sourceIndex = mapToSource( parent );
return sourceModel()->dropMimeData( data, action, row, column,
sourceIndex );
debug() << __PRETTY_FUNCTION__ << "Unsupported drop mime-data:" << data->formats();
return false;
}
Qt::DropActions
......@@ -266,27 +205,10 @@ PlaylistsByProviderProxy::buildTree()
//add the empty providers at the top of the list
PlaylistProviderList providerList =
The::playlistManager()->providersForCategory( m_playlistCategory );
if( !providerList.isEmpty() )
foreach( Playlists::PlaylistProvider *provider, providerList )
{
beginInsertRows( QModelIndex(), 0, providerList.count() );
foreach( Playlists::PlaylistProvider *provider, providerList )
{
if( provider && ( provider->playlistCount() > 0 || provider->playlists().count() > 0 ) )
continue;
ItemData itemData;
itemData.insert( Qt::DisplayRole, provider->prettyName() );
itemData.insert( Qt::DecorationRole, provider->icon() );
itemData.insert( PlaylistBrowserNS::PlaylistBrowserModel::ActionRole,
QVariant::fromValue( provider->providerActions() ) );
itemData.insert( PlaylistBrowserNS::PlaylistBrowserModel::ByLineRole, QString() );
RowData rowData;
rowData.insert( PlaylistBrowserNS::PlaylistBrowserModel::PlaylistItemColumn, itemData );
//Provider column is used for filtering.
rowData.insert( PlaylistBrowserNS::PlaylistBrowserModel::ProviderColumn, itemData );
m_groupMaps << rowData;
}
endInsertRows();
slotProviderAdded( provider, provider->category() );
}
QtGroupingProxy::buildTree();
......@@ -299,3 +221,49 @@ PlaylistsByProviderProxy::slotRenameIndex( const QModelIndex &sourceIdx )
if( idx.isValid() )
emit renameIndex( idx );
}
void
PlaylistsByProviderProxy::slotProviderAdded( Playlists::PlaylistProvider *provider, int category )
{
DEBUG_BLOCK
if( category != m_playlistCategory )
return;
if( provider->playlistCount() > 0 ||
( provider->playlistCount() < 0 /* not counted */ && !provider->playlists().isEmpty() ) )
return; // non-empty providers are handled by PlaylistBrowserModel
ItemData itemData;
itemData.insert( Qt::DisplayRole, provider->prettyName() );
itemData.insert( Qt::DecorationRole, provider->icon() );
itemData.insert( PlaylistBrowserNS::PlaylistBrowserModel::ActionRole,
QVariant::fromValue( provider->providerActions() ) );
itemData.insert( PlaylistBrowserNS::PlaylistBrowserModel::ByLineRole, i18n( "No playlists" ) );
itemData.insert( PlaylistBrowserNS::PlaylistBrowserModel::ProviderRole,
QVariant::fromValue<Playlists::PlaylistProvider*>( provider ) );
RowData rowData;
rowData.insert( PlaylistBrowserNS::PlaylistBrowserModel::PlaylistItemColumn, itemData );
//Provider column is used for filtering.
rowData.insert( PlaylistBrowserNS::PlaylistBrowserModel::ProviderColumn, itemData );
addEmptyGroup( rowData );
}
void
PlaylistsByProviderProxy::slotProviderRemoved( Playlists::PlaylistProvider *provider, int category )
{
DEBUG_BLOCK
if( category != m_playlistCategory )
return;
for( int i = 0; i < rowCount(); i++ )
{
QModelIndex idx = index( i, PlaylistBrowserNS::PlaylistBrowserModel::PlaylistItemColumn );
Playlists::PlaylistProvider *rowProvider = data( idx, PlaylistBrowserNS::PlaylistBrowserModel::ProviderRole )
.value<Playlists::PlaylistProvider *>();
if( rowProvider != provider )
continue;
removeGroup( idx );
}
}
......@@ -25,17 +25,6 @@ class PlaylistsByProviderProxy : public QtGroupingProxy
{
Q_OBJECT
public:
//TODO: move these internal drag and drop functions to QtGroupingProxy
/** serializes the indexes into a bytearray
*/
QByteArray encodeMimeRows( const QList<QModelIndex> indexes ) const;
/** \arg data serialized data
* \model this model is used to get the indexes.
*/
QList<QModelIndex> decodeMimeRows( QByteArray data, QAbstractItemModel *model ) const;
static const QString AMAROK_PROVIDERPROXY_INDEXES;
PlaylistsByProviderProxy( QAbstractItemModel *model, int column, int playlistCategory );
~PlaylistsByProviderProxy() {}
......@@ -69,6 +58,8 @@ class PlaylistsByProviderProxy : public QtGroupingProxy
private slots:
void slotRenameIndex( const QModelIndex &index );
void slotProviderAdded( Playlists::PlaylistProvider *provider, int category );
void slotProviderRemoved( Playlists::PlaylistProvider *provider, int category );
private:
int m_playlistCategory;
......
......@@ -32,6 +32,7 @@ class AMAROK_EXPORT UserPlaylistProvider : public PlaylistProvider
{
Q_OBJECT
public:
UserPlaylistProvider( QObject *parent = 0 ) : PlaylistProvider( parent ) {}
virtual ~UserPlaylistProvider();
/* PlaylistProvider functions */
......
......@@ -78,4 +78,6 @@ class AMAROK_CORE_EXPORT PlaylistProvider : public QObject
} //namespace Playlists
Q_DECLARE_METATYPE( Playlists::PlaylistProvider * )
#endif // AMAROK_PLAYLISTPROVIDER_H
......@@ -340,16 +340,24 @@ PlaylistManager::downloadComplete( KJob *job )
bool
PlaylistManager::save( Meta::TrackList tracks, const QString &name,
Playlists::UserPlaylistProvider *toProvider )
Playlists::PlaylistProvider *toProvider, bool editName )
{
//if toProvider is 0 use the default Playlists::UserPlaylistProvider (SQL)
Playlists::UserPlaylistProvider *prov = toProvider ? toProvider : m_defaultUserPlaylistProvider;
Playlists::UserPlaylistProvider *prov = toProvider
? qobject_cast<Playlists::UserPlaylistProvider *>( toProvider )
: m_defaultUserPlaylistProvider;
if( !prov )
return false;
Playlists::PlaylistPtr playlist = prov->save( tracks, name );
if( playlist.isNull() )
return false;
AmarokUrl("amarok://navigate/playlists/user playlists").run();
emit renamePlaylist( playlist );
if( editName )
{
AmarokUrl("amarok://navigate/playlists/user playlists").run();
emit renamePlaylist( playlist );
}
return true;
}
......
......@@ -115,10 +115,11 @@ class AMAROK_EXPORT PlaylistManager : public QObject
* @arg tracks list of tracks to save
* @arg name name of playlist to save
* @arg toProvider If 0 (default) will save to the default UserPlaylistProvider
* @arg editName whether to edit new playlist name as soon as it is created
* @see defaultUserPlaylists
*/
bool save( Meta::TrackList tracks, const QString &name = QString(),
Playlists::UserPlaylistProvider *toProvider = 0 );
Playlists::PlaylistProvider *toProvider = 0, bool editName = true );
/**
* Saves a playlist from a file to the database.
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment