1 /****************************************************************************************
2  * Copyright (c) 2008 Daniel Jones <danielcjones@gmail.com>                             *
3  * Copyright (c) 2009-2010 Leo Franchi <lfranchi@kde.org>                               *
4  * Copyright (c) 2011 Ralf Engels <ralf-engels@gmx.de>                                  *
5  *                                                                                      *
6  * This program is free software; you can redistribute it and/or modify it under        *
7  * the terms of the GNU General Public License as published by the Free Software        *
8  * Foundation; either version 2 of the License, or (at your option) any later           *
9  * version.                                                                             *
10  *                                                                                      *
11  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
12  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
13  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
14  *                                                                                      *
15  * You should have received a copy of the GNU General Public License along with         *
16  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
17  ****************************************************************************************/
18 
19 #define DEBUG_PREFIX "DynamicModel"
20 
21 #include "DynamicModel.h"
22 
23 #include "App.h"
24 
25 #include "Bias.h"
26 #include "BiasFactory.h"
27 #include "BiasedPlaylist.h"
28 #include "biases/AlbumPlayBias.h"
29 #include "biases/IfElseBias.h"
30 #include "biases/PartBias.h"
31 #include "biases/SearchQueryBias.h"
32 #include "biases/TagMatchBias.h"
33 #include "core/support/Amarok.h"
34 #include "core/support/Debug.h"
35 
36 #include "playlist/PlaylistActions.h"
37 
38 #include <QIcon>
39 
40 #include <QFile>
41 #include <QBuffer>
42 #include <QByteArray>
43 #include <QDataStream>
44 #include <QMimeData>
45 #include <QXmlStreamReader>
46 #include <QXmlStreamWriter>
47 
48 /* general note:
49    For the sake of this file we are handling a modified active playlist as
50    a different one.
51 */
52 
53 Dynamic::DynamicModel* Dynamic::DynamicModel::s_instance = nullptr;
54 
55 Dynamic::DynamicModel*
instance()56 Dynamic::DynamicModel::instance()
57 {
58     if( !s_instance )
59     {
60         s_instance = new DynamicModel( pApp );
61         s_instance->loadPlaylists();
62     }
63     return s_instance;
64 }
65 
66 
DynamicModel(QObject * parent)67 Dynamic::DynamicModel::DynamicModel(QObject* parent)
68     : QAbstractItemModel( parent )
69     , m_activePlaylistIndex( 0 )
70 { }
71 
~DynamicModel()72 Dynamic::DynamicModel::~DynamicModel()
73 {
74     savePlaylists();
75 }
76 
77 Dynamic::DynamicPlaylist*
setActivePlaylist(int index)78 Dynamic::DynamicModel::setActivePlaylist( int index )
79 {
80     if( index < 0 || index >= m_playlists.count() )
81         return m_playlists[m_activePlaylistIndex];
82 
83     if( m_activePlaylistIndex == index )
84         return m_playlists[m_activePlaylistIndex];
85 
86     Q_EMIT dataChanged( this->index( m_activePlaylistIndex, 0 ),
87                       this->index( m_activePlaylistIndex, 0 ) );
88     m_activePlaylistIndex = index;
89     Q_EMIT dataChanged( this->index( m_activePlaylistIndex, 0 ),
90                       this->index( m_activePlaylistIndex, 0 ) );
91 
92     Q_EMIT activeChanged( index );
93     savePlaylists(); // save in between to prevent loosing too much in case of a crash
94 
95     return m_playlists[m_activePlaylistIndex];
96 }
97 
98 Dynamic::DynamicPlaylist*
activePlaylist() const99 Dynamic::DynamicModel::activePlaylist() const
100 {
101     if( m_activePlaylistIndex < 0 || m_activePlaylistIndex >= m_playlists.count() )
102         return 0;
103 
104     return m_playlists[m_activePlaylistIndex];
105 }
106 
107 int
activePlaylistIndex() const108 Dynamic::DynamicModel::activePlaylistIndex() const
109 {
110     return m_activePlaylistIndex;
111 }
112 
113 int
playlistIndex(Dynamic::DynamicPlaylist * playlist) const114 Dynamic::DynamicModel::playlistIndex( Dynamic::DynamicPlaylist* playlist ) const
115 {
116     return m_playlists.indexOf( playlist );
117 }
118 
119 QModelIndex
insertPlaylist(int index,Dynamic::DynamicPlaylist * playlist)120 Dynamic::DynamicModel::insertPlaylist( int index, Dynamic::DynamicPlaylist* playlist )
121 {
122     if( !playlist )
123         return QModelIndex();
124 
125     int oldIndex = playlistIndex( playlist );
126     bool wasActive = (oldIndex == m_activePlaylistIndex);
127 
128     // -- remove the playlist if it was already in our model
129     if( oldIndex >= 0 )
130     {
131         beginRemoveRows( QModelIndex(), oldIndex, oldIndex );
132         m_playlists.removeAt( oldIndex );
133         endRemoveRows();
134 
135         if( oldIndex < index )
136             index--;
137 
138         if( m_activePlaylistIndex > oldIndex )
139             m_activePlaylistIndex--;
140     }
141 
142     if( index < 0 )
143         index = 0;
144     if( index > m_playlists.count() )
145         index = m_playlists.count();
146 
147     // -- insert it at the new position
148     beginInsertRows( QModelIndex(), index, index );
149 
150     if( m_activePlaylistIndex > index )
151         m_activePlaylistIndex++;
152 
153     if( wasActive )
154         m_activePlaylistIndex = index;
155 
156     m_playlists.insert( index, playlist );
157 
158     endInsertRows();
159 
160     return this->index( index, 0 );
161 }
162 
163 QModelIndex
insertBias(int row,const QModelIndex & parentIndex,const Dynamic::BiasPtr & bias)164 Dynamic::DynamicModel::insertBias( int row, const QModelIndex &parentIndex, const Dynamic::BiasPtr &bias )
165 {
166     QObject* o = static_cast<QObject*>(parentIndex.internalPointer());
167     BiasedPlaylist* parentPlaylist = qobject_cast<BiasedPlaylist*>(o);
168     AndBias* parentBias = qobject_cast<Dynamic::AndBias*>(o);
169     AbstractBias* aBias = qobject_cast<Dynamic::AbstractBias*>(o);
170 
171     // Add something directly to the top
172     if( !parentIndex.isValid() )
173     {
174         if( row >= 0 && row < m_playlists.count() )
175         {
176             o = m_playlists[row];
177             parentPlaylist = qobject_cast<BiasedPlaylist*>(o);
178         }
179         else
180         {
181             return QModelIndex();
182         }
183     }
184 
185     if( parentPlaylist )
186     {
187         // already have an AND bias
188         if( parentPlaylist && qobject_cast<Dynamic::AndBias*>(parentPlaylist->bias().data()) )
189         {
190             return insertBias( 0, index( parentPlaylist->bias() ), bias );
191         }
192         else
193         {
194             // need a new AND bias
195             parentBias = new Dynamic::AndBias();
196             Dynamic::BiasPtr b( parentPlaylist->bias() ); // ensure that the bias does not get freed
197             parentPlaylist->bias()->replace( Dynamic::BiasPtr( parentBias ) );
198             parentBias->appendBias( b );
199             parentBias->appendBias( bias );
200         }
201     }
202     else if( parentBias )
203     {
204         parentBias->appendBias( bias );
205         parentBias->moveBias( parentBias->biases().count()-1, row );
206     }
207     else if( aBias )
208     {
209         // insert the bias after
210         return insertBias( parentIndex.row(), parentIndex.parent(), bias );
211     }
212     return this->index( bias );
213 }
214 
215 
216 Qt::DropActions
supportedDropActions() const217 Dynamic::DynamicModel::supportedDropActions() const
218 {
219     return Qt::MoveAction;
220     // return Qt::CopyAction | Qt::MoveAction;
221 }
222 
223 // ok. the item model stuff is a little bit complicate
224 // let's just pull it though and use Standard items the next time
225 // see http://doc.qt.nokia.com/4.7/itemviews-simpletreemodel.html
226 
227 // note to our indices: the internal pointer points to the object behind the index (not to it's parent)
228 // row is the row number inside the parent.
229 
230 QVariant
data(const QModelIndex & i,int role) const231 Dynamic::DynamicModel::data( const QModelIndex& i, int role ) const
232 {
233     if( !i.isValid() )
234         return QVariant();
235 
236     int row = i.row();
237     int column = i.column();
238     if( row < 0 || column != 0 )
239         return QVariant();
240 
241     QObject* o = static_cast<QObject*>(i.internalPointer());
242     BiasedPlaylist* indexPlaylist = qobject_cast<BiasedPlaylist*>(o);
243     AbstractBias* indexBias = qobject_cast<Dynamic::AbstractBias*>(o);
244 
245     // level 1
246     if( indexPlaylist )
247     {
248         QString title = indexPlaylist->title();
249 
250         switch( role )
251         {
252         case Qt::DisplayRole:
253             return title;
254 
255         case Qt::EditRole:
256             return title;
257 
258         case Qt::DecorationRole:
259             if( activePlaylist() == indexPlaylist )
260                 return QIcon::fromTheme( QStringLiteral("amarok_playlist") );
261             else
262                 return QIcon::fromTheme( QStringLiteral("amarok_playlist_clear") );
263 
264         case Qt::FontRole:
265             {
266                 QFont font = QFont();
267                 if( activePlaylist() == indexPlaylist )
268                     font.setBold( true );
269                 else
270                     font.setBold( false );
271                 return font;
272             }
273 
274         case PlaylistRole:
275             return QVariant::fromValue<QObject*>( indexPlaylist );
276 
277         default:
278             return QVariant();
279         }
280     }
281     // level > 1
282     else if( indexBias )
283     {
284         switch( role )
285         {
286         case Qt::DisplayRole:
287             return QVariant(indexBias->toString());
288             // return QVariant(QStringLiteral("and: ")+indexBias->toString());
289 
290         case Qt::ToolTipRole:
291             {
292                 // find the factory for the bias
293                 QList<Dynamic::AbstractBiasFactory*> factories = Dynamic::BiasFactory::factories();
294                 foreach( Dynamic::AbstractBiasFactory* factory, factories )
295                 {
296                     if( factory->name() == indexBias->name() )
297                         return factory->i18nDescription();
298                 }
299                 return QVariant();
300             }
301 
302         case BiasRole:
303             return QVariant::fromValue<QObject*>( indexBias );
304 
305         default:
306             return QVariant();
307         }
308     }
309     // level 0
310     else
311     {
312         return QVariant();
313     }
314 }
315 
316 bool
setData(const QModelIndex & index,const QVariant & value,int role)317 Dynamic::DynamicModel::setData( const QModelIndex& index, const QVariant& value, int role )
318 {
319     if( !index.isValid() )
320         return false;
321 
322     int row = index.row();
323     int column = index.column();
324     if( row < 0 || column != 0 )
325         return false;
326 
327     QObject* o = static_cast<QObject*>(index.internalPointer());
328     BiasedPlaylist* indexPlaylist = qobject_cast<BiasedPlaylist*>(o);
329     // AbstractBias* indexBias = qobject_cast<Dynamic::AbstractBias*>(o);
330 
331     // level 1
332     if( indexPlaylist )
333     {
334         switch( role )
335         {
336         case Qt::EditRole:
337             indexPlaylist->setTitle( value.toString() );
338             return true;
339 
340         default:
341             return false;
342         }
343     }
344 
345     return false;
346 }
347 
348 
349 Qt::ItemFlags
flags(const QModelIndex & index) const350 Dynamic::DynamicModel::flags( const QModelIndex& index ) const
351 {
352     Qt::ItemFlags defaultFlags = Qt::ItemIsDropEnabled;
353 
354     if( !index.isValid() )
355         return defaultFlags;
356 
357     int row = index.row();
358     int column = index.column();
359     if( row < 0 || column != 0 )
360         return defaultFlags;
361 
362     QObject* o = static_cast<QObject*>(index.internalPointer());
363     BiasedPlaylist* indexPlaylist = qobject_cast<BiasedPlaylist*>(o);
364     AbstractBias* indexBias = qobject_cast<Dynamic::AbstractBias*>(o);
365 
366     // level 1
367     if( indexPlaylist )
368     {
369         return Qt::ItemIsSelectable | Qt::ItemIsEditable |
370             Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled |
371             Qt::ItemIsUserCheckable | Qt::ItemIsEnabled;
372     }
373     // level > 1
374     else if( indexBias )
375     {
376         QModelIndex parentIndex = parent( index );
377         QObject* o2 = static_cast<QObject*>(parentIndex.internalPointer());
378         BiasedPlaylist* parentPlaylist = qobject_cast<BiasedPlaylist*>(o2);
379 
380         // level 2
381         if( parentPlaylist ) // you can't drag all the biases away from a playlist
382             return Qt::ItemIsSelectable | /* Qt::ItemIsEditable | */
383                 /* Qt::ItemIsDragEnabled | */ Qt::ItemIsDropEnabled |
384                 Qt::ItemIsUserCheckable | Qt::ItemIsEnabled;
385         // level > 2
386         else
387             return Qt::ItemIsSelectable | /* Qt::ItemIsEditable | */
388                 Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled |
389                 Qt::ItemIsUserCheckable | Qt::ItemIsEnabled;
390     }
391 
392     return defaultFlags;
393 }
394 
395 QModelIndex
index(int row,int column,const QModelIndex & parent) const396 Dynamic::DynamicModel::index( int row, int column, const QModelIndex& parent ) const
397 {
398     //ensure sanity of parameters
399     //we are a tree model, there are no columns
400     if( row < 0 || column != 0 )
401         return QModelIndex();
402 
403     QObject* o = static_cast<QObject*>(parent.internalPointer());
404     BiasedPlaylist* parentPlaylist = qobject_cast<BiasedPlaylist*>(o);
405     AndBias* parentBias = qobject_cast<Dynamic::AndBias*>(o);
406 
407     // level 1
408     if( parentPlaylist )
409     {
410         if( row >= 1 )
411             return QModelIndex();
412         else
413             return createIndex( row, column, parentPlaylist->bias().data() );
414     }
415     // level > 1
416     else if( parentBias )
417     {
418         if( row >= parentBias->biases().count() )
419             return QModelIndex();
420         else
421             return createIndex( row, column, parentBias->biases().at( row ).data() );
422     }
423     // level 0
424     else
425     {
426         if( row >= m_playlists.count() )
427             return QModelIndex();
428         else
429             return createIndex( row, column, m_playlists.at( row ) );
430     }
431 }
432 
433 QModelIndex
parent(int row,BiasedPlaylist * list,const BiasPtr & bias) const434 Dynamic::DynamicModel::parent( int row, BiasedPlaylist* list, const BiasPtr &bias ) const
435 {
436     if( list->bias() == bias )
437         return createIndex( row, 0, list );
438     return parent( 0, list->bias(), bias );
439 }
440 
441 QModelIndex
parent(int row,const BiasPtr & parent,const BiasPtr & bias) const442 Dynamic::DynamicModel::parent( int row, const BiasPtr &parent, const BiasPtr &bias ) const
443 {
444     Dynamic::AndBias* andBias = qobject_cast<Dynamic::AndBias*>(parent.data());
445     if( !andBias )
446         return QModelIndex();
447 
448     for( int i = 0; i < andBias->biases().count(); i++ )
449     {
450         Dynamic::BiasPtr child = andBias->biases().at( i );
451         if( child == bias )
452             return createIndex( row, 0, andBias );
453         QModelIndex res = this->parent( i, child, bias );
454         if( res.isValid() )
455             return res;
456     }
457     return QModelIndex();
458 }
459 
460 QModelIndex
parent(const QModelIndex & index) const461 Dynamic::DynamicModel::parent(const QModelIndex& index) const
462 {
463     if( !index.isValid() )
464         return QModelIndex();
465 
466     QObject* o = static_cast<QObject*>( index.internalPointer() );
467     BiasedPlaylist* indexPlaylist = qobject_cast<BiasedPlaylist*>(o);
468     BiasPtr indexBias( qobject_cast<AbstractBias*>(o) );
469 
470     if( indexPlaylist )
471         return QModelIndex(); // abstract root
472     else if( indexBias )
473     {
474         // search for the parent
475         for( int i = 0; i < m_playlists.count(); i++ )
476         {
477             QModelIndex res = parent( i, qobject_cast<BiasedPlaylist*>(m_playlists[i]), indexBias );
478             if( res.isValid() )
479                 return res;
480         }
481     }
482     return QModelIndex();
483 }
484 
485 int
rowCount(const QModelIndex & parent) const486 Dynamic::DynamicModel::rowCount(const QModelIndex& parent) const
487 {
488     QObject* o = static_cast<QObject*>(parent.internalPointer());
489     BiasedPlaylist* parentPlaylist = qobject_cast<BiasedPlaylist*>(o);
490     AndBias* parentBias = qobject_cast<Dynamic::AndBias*>(o);
491     AbstractBias* bias = qobject_cast<Dynamic::AbstractBias*>(o);
492 
493     // level 1
494     if( parentPlaylist )
495     {
496         return 1;
497     }
498     // level > 1
499     else if( parentBias )
500     {
501         return parentBias->biases().count();
502     }
503     // for all other biases that are no And-Bias
504     else if( bias )
505     {
506         return 0;
507     }
508     // level 0
509     else
510     {
511         return m_playlists.count();
512     }
513 }
514 
515 int
columnCount(const QModelIndex & parent) const516 Dynamic::DynamicModel::columnCount(const QModelIndex & parent) const
517 {
518     Q_UNUSED( parent )
519     return 1;
520 }
521 
522 QStringList
mimeTypes() const523 Dynamic::DynamicModel::mimeTypes() const
524 {
525     QStringList types;
526     types << QStringLiteral("application/amarok.biasModel.index");
527     return types;
528 }
529 
530 QMimeData*
mimeData(const QModelIndexList & indexes) const531 Dynamic::DynamicModel::mimeData(const QModelIndexList &indexes) const
532 {
533     // note: we only use the first index
534 
535     if( indexes.isEmpty() )
536         return new QMimeData();
537 
538     QModelIndex index = indexes.first();
539     if( !index.isValid() )
540         return new QMimeData();
541 
542     // store the index in the mime data
543     QByteArray bytes;
544     QDataStream stream( &bytes, QIODevice::WriteOnly );
545     serializeIndex( &stream, index );
546     QMimeData *mimeData = new QMimeData();
547     mimeData->setData(QStringLiteral("application/amarok.biasModel.index"), bytes);
548     return mimeData;
549 }
550 
551 bool
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & _parent)552 Dynamic::DynamicModel::dropMimeData(const QMimeData *data,
553                                     Qt::DropAction action,
554                                     int row, int column, const QModelIndex &_parent)
555 {
556     Q_UNUSED( column );
557 
558     QModelIndex parent = _parent;
559 
560     if( action == Qt::IgnoreAction )
561         return true;
562 
563     if( data->hasFormat(QStringLiteral("application/amarok.biasModel.index")) )
564     {
565         // get the source index from the mime data
566         QByteArray bytes = data->data(QStringLiteral("application/amarok.biasModel.index"));
567         QDataStream stream( &bytes, QIODevice::ReadOnly );
568         QModelIndex index = unserializeIndex( &stream );
569 
570         if( !index.isValid() )
571             return false;
572 
573         QObject* o = static_cast<QObject*>(index.internalPointer());
574         BiasedPlaylist* indexPlaylist = qobject_cast<BiasedPlaylist*>(o);
575         BiasPtr indexBias( qobject_cast<Dynamic::AbstractBias*>(o) );
576 
577         // in case of moving or inserting a playlist, we
578         // move to the top level
579         if( indexPlaylist )
580         {
581             while( parent.isValid() )
582             {
583                 row = parent.row() + 1;
584                 column = parent.column();
585                 parent = parent.parent();
586             }
587         }
588 
589 debug() << "dropMimeData action" << action;
590 
591         // -- insert
592         if( action == Qt::CopyAction )
593         {
594             // -- playlist
595             if( indexPlaylist )
596             {
597                 insertPlaylist( row, cloneList( indexPlaylist ) );
598                 return true;
599             }
600             // -- bias
601             else if( indexBias )
602             {
603                 insertBias( row, parent, cloneBias( indexBias ) );
604                 return true;
605             }
606         }
607         else if( action == Qt::MoveAction )
608         {
609             // -- playlist
610             if( indexPlaylist )
611             {
612                 insertPlaylist( row, indexPlaylist );
613                 return true;
614             }
615             // -- bias
616             else if( indexBias )
617             {
618                 indexBias->replace( Dynamic::BiasPtr() );
619                 insertBias( row, parent, Dynamic::BiasPtr(indexBias) );
620                 return true;
621             }
622         }
623     }
624 
625     return false;
626 }
627 
628 
629 QModelIndex
index(const Dynamic::BiasPtr & bias) const630 Dynamic::DynamicModel::index( const Dynamic::BiasPtr &bias ) const
631 {
632     QModelIndex res;
633 
634     // search for the parent
635     for( int i = 0; i < m_playlists.count(); i++ )
636     {
637         res = parent( i, qobject_cast<BiasedPlaylist*>(m_playlists[i]), bias );
638         if( res.isValid() )
639             break;
640     }
641 
642     if( !res.isValid() )
643         return res;
644 
645     QObject* o = static_cast<QObject*>(res.internalPointer());
646     BiasedPlaylist* parentPlaylist = qobject_cast<BiasedPlaylist*>(o);
647     AndBias* parentBias = qobject_cast<Dynamic::AndBias*>(o);
648 
649     // level 1
650     if( parentPlaylist )
651     {
652         return createIndex( 0, 0, bias.data() );
653     }
654     // level > 1
655     else if( parentBias )
656     {
657         return createIndex( parentBias->biases().indexOf( bias ), 0, bias.data() );
658     }
659     else
660     {
661         return QModelIndex();
662     }
663 }
664 
665 QModelIndex
index(Dynamic::DynamicPlaylist * playlist) const666 Dynamic::DynamicModel::index( Dynamic::DynamicPlaylist* playlist ) const
667 {
668     return createIndex( playlistIndex( playlist ), 0, playlist );
669 }
670 
671 
672 void
savePlaylists()673 Dynamic::DynamicModel::savePlaylists()
674 {
675     savePlaylists( QStringLiteral("dynamic.xml") );
676 }
677 
678 void
loadPlaylists()679 Dynamic::DynamicModel::loadPlaylists()
680 {
681     loadPlaylists( QStringLiteral("dynamic.xml") );
682 }
683 
684 void
removeAt(const QModelIndex & index)685 Dynamic::DynamicModel::removeAt( const QModelIndex& index )
686 {
687     if( !index.isValid() )
688         return;
689 
690     QObject* o = static_cast<QObject*>(index.internalPointer());
691     BiasedPlaylist* indexPlaylist = qobject_cast<BiasedPlaylist*>(o);
692     AbstractBias* indexBias = qobject_cast<Dynamic::AbstractBias*>(o);
693 
694     // remove a playlist
695     if( indexPlaylist )
696     {
697         if( !indexPlaylist || !m_playlists.contains( indexPlaylist ) )
698             return;
699 
700         int i = playlistIndex( indexPlaylist );
701 
702         beginRemoveRows( QModelIndex(), i, i );
703         m_playlists.removeAt(i);
704         endRemoveRows();
705 
706         delete indexPlaylist;
707 
708         if( m_playlists.isEmpty() )
709         {
710             The::playlistActions()->enableDynamicMode( false );
711             m_activePlaylistIndex = 0;
712         }
713         else
714         {
715             setActivePlaylist( qBound(0, m_activePlaylistIndex, m_playlists.count() - 1 ) );
716         }
717     }
718     // remove a bias
719     else if( indexBias )
720     {
721         QModelIndex parentIndex = parent( index );
722 
723         QObject* o2 = static_cast<QObject*>(parentIndex.internalPointer());
724         BiasedPlaylist* parentPlaylist = qobject_cast<BiasedPlaylist*>(o2);
725         AndBias* parentBias = qobject_cast<Dynamic::AndBias*>(o2);
726 
727         // parent of the bias is a playlist
728         if( parentPlaylist )
729         {
730             // a playlist always needs a bias, so we can only remove this one
731             // if we can come up with a replacement
732             AndBias* andBias = qobject_cast<Dynamic::AndBias*>(indexBias);
733             if( andBias && !andBias->biases().isEmpty() )
734                 andBias->replace( andBias->biases().first() ); // replace by the first sub-bias
735             else
736             {
737                 ; // can't remove the last bias directly under a playlist
738             }
739         }
740         // parent of the bias is another bias
741         else if( parentBias )
742         {
743             indexBias->replace( Dynamic::BiasPtr() ); // replace by nothing
744         }
745     }
746 
747     savePlaylists();
748 }
749 
750 
751 QModelIndex
cloneAt(const QModelIndex & index)752 Dynamic::DynamicModel::cloneAt( const QModelIndex& index )
753 {
754     DEBUG_BLOCK;
755 
756     QObject* o = static_cast<QObject*>(index.internalPointer());
757     BiasedPlaylist* indexPlaylist = qobject_cast<BiasedPlaylist*>(o);
758     BiasPtr indexBias( qobject_cast<Dynamic::AbstractBias*>(o) );
759 
760     if( indexPlaylist )
761     {
762         return insertPlaylist( m_playlists.count(), cloneList( indexPlaylist ) );
763     }
764     else if( indexBias )
765     {
766         return insertBias( -1, index.parent(), cloneBias( indexBias ) );
767     }
768 
769     return QModelIndex();
770 }
771 
772 
773 QModelIndex
newPlaylist()774 Dynamic::DynamicModel::newPlaylist()
775 {
776     Dynamic::BiasedPlaylist *playlist = new Dynamic::BiasedPlaylist( this );
777     Dynamic::BiasPtr bias( new Dynamic::SearchQueryBias() );
778     playlist->setTitle( i18nc( "Default name for new playlists", "New playlist") );
779     playlist->bias()->replace( bias );
780 
781     return insertPlaylist( m_playlists.count(), playlist );
782 }
783 
784 
785 bool
savePlaylists(const QString & filename)786 Dynamic::DynamicModel::savePlaylists( const QString &filename )
787 {
788     DEBUG_BLOCK;
789 
790     QFile xmlFile( Amarok::saveLocation() + filename );
791     if( !xmlFile.open( QIODevice::WriteOnly ) )
792     {
793         error() << "Can not write" << xmlFile.fileName();
794         return false;
795     }
796 
797     QXmlStreamWriter xmlWriter( &xmlFile );
798     xmlWriter.setAutoFormatting( true );
799     xmlWriter.writeStartDocument();
800     xmlWriter.writeStartElement(QStringLiteral("biasedPlaylists"));
801     xmlWriter.writeAttribute(QStringLiteral("version"), QStringLiteral("2") );
802     xmlWriter.writeAttribute(QStringLiteral("current"), QString::number( m_activePlaylistIndex ) );
803 
804     foreach( Dynamic::DynamicPlaylist *playlist, m_playlists )
805     {
806         xmlWriter.writeStartElement(QStringLiteral("playlist"));
807         playlist->toXml( &xmlWriter );
808         xmlWriter.writeEndElement();
809     }
810 
811     xmlWriter.writeEndElement();
812     xmlWriter.writeEndDocument();
813 
814     return true;
815 }
816 
817 bool
loadPlaylists(const QString & filename)818 Dynamic::DynamicModel::loadPlaylists( const QString &filename )
819 {
820     // -- clear all the old playlists
821     beginResetModel();
822     foreach( Dynamic::DynamicPlaylist* playlist, m_playlists )
823         delete playlist;
824     m_playlists.clear();
825 
826     // -- open the file
827     QFile xmlFile( Amarok::saveLocation() + filename );
828     if( !xmlFile.open( QIODevice::ReadOnly ) )
829     {
830         error() << "Can not read" << xmlFile.fileName();
831         initPlaylists();
832         return false;
833     }
834 
835     QXmlStreamReader xmlReader( &xmlFile );
836 
837     // -- check the version
838     xmlReader.readNextStartElement();
839     if( xmlReader.atEnd() ||
840         !xmlReader.isStartElement() ||
841         xmlReader.name() != QLatin1String("biasedPlaylists") ||
842         xmlReader.attributes().value( QLatin1String("version") ) != QLatin1String("2") )
843     {
844         error() << "Playlist file" << xmlFile.fileName() << "is invalid or has wrong version";
845         initPlaylists();
846         return false;
847     }
848 
849     int newPlaylistIndex = xmlReader.attributes().value( QLatin1String("current") ).toString().toInt();
850 
851     while (!xmlReader.atEnd()) {
852         xmlReader.readNext();
853 
854         if( xmlReader.isStartElement() )
855         {
856             QStringRef name = xmlReader.name();
857             if( name == QLatin1String("playlist") )
858             {
859                 Dynamic::BiasedPlaylist *playlist =  new Dynamic::BiasedPlaylist( &xmlReader, this );
860                 if( playlist->bias() )
861                 {
862                     insertPlaylist( m_playlists.count(), playlist );
863                 }
864                 else
865                 {
866                     delete playlist;
867                     warning() << "Just read a playlist without bias from"<<xmlFile.fileName();
868                 }
869             }
870             else
871             {
872                 debug() << "Unexpected xml start element"<<name<<"in input";
873                 xmlReader.skipCurrentElement();
874             }
875         }
876 
877         else if( xmlReader.isEndElement() )
878         {
879             break;
880         }
881     }
882 
883     // -- validate the index
884     if( m_playlists.isEmpty() ) {
885         error() << "Could not read the default playlist from" << xmlFile.fileName();
886         initPlaylists();
887         return false;
888     }
889 
890     m_activePlaylistIndex = qBound( 0, newPlaylistIndex, m_playlists.count()-1 );
891 
892     Q_EMIT activeChanged( m_activePlaylistIndex );
893     endResetModel();
894 
895     return true;
896 }
897 
898 void
initPlaylists()899 Dynamic::DynamicModel::initPlaylists()
900 {
901     // -- clear all the old playlists
902     beginResetModel();
903     foreach( Dynamic::DynamicPlaylist* playlist, m_playlists )
904         delete playlist;
905     m_playlists.clear();
906 
907     Dynamic::BiasedPlaylist *playlist;
908 
909     // -- create the empty default random playlists
910 
911     // - first one random playlist
912     playlist = new Dynamic::BiasedPlaylist( this );
913     insertPlaylist( 0, playlist );
914 
915     // - a playlist demonstrating the SearchQueryBias
916     playlist = new Dynamic::BiasedPlaylist( this );
917     playlist->setTitle( i18n("Rock and Pop") );
918     QString query = Meta::shortI18nForField( Meta::valGenre ) + QLatin1Char(':') + i18n( "Rock" );
919     /* following cannot be currently translated, see ExpressionParser::isAdvancedExpression()
920      * and ExpressionParser::finishedToken() */
921     query += QLatin1String(" AND ");
922     query += Meta::shortI18nForField( Meta::valGenre ) + QLatin1Char(':') + i18n( "Pop" );
923     playlist->bias()->replace( Dynamic::BiasPtr( new Dynamic::SearchQueryBias( query ) ) );
924     insertPlaylist( 1, playlist );
925 
926     // - a complex playlist demonstrating AlbumPlay and IfElse
927     playlist = new Dynamic::BiasedPlaylist( this );
928     playlist->setTitle( i18n("Album play") );
929     Dynamic::IfElseBias *ifElse = new Dynamic::IfElseBias();
930     playlist->bias()->replace( Dynamic::BiasPtr( ifElse ) );
931     ifElse->appendBias( Dynamic::BiasPtr( new Dynamic::AlbumPlayBias() ) );
932     query = Meta::shortI18nForField( Meta::valTrackNr ) + ":1";
933     ifElse->appendBias( Dynamic::BiasPtr( new Dynamic::SearchQueryBias( query ) ) );
934     insertPlaylist( 2, playlist );
935 
936     // - a complex playlist demonstrating PartBias and TagMatchBias
937     playlist = new Dynamic::BiasedPlaylist( this );
938     playlist->setTitle( i18nc( "Name of a dynamic playlist", "Rating" ) );
939     Dynamic::PartBias *part = new Dynamic::PartBias();
940     playlist->bias()->replace( Dynamic::BiasPtr( part ) );
941 
942     part->appendBias( Dynamic::BiasPtr( new Dynamic::RandomBias() ) );
943 
944     MetaQueryWidget::Filter ratingFilter;
945     ratingFilter.setField( Meta::valRating );
946     ratingFilter.numValue = 5;
947     ratingFilter.condition = MetaQueryWidget::GreaterThan;
948 
949     Dynamic::TagMatchBias* ratingBias1 = new Dynamic::TagMatchBias();
950     Dynamic::BiasPtr ratingBias1Ptr( ratingBias1 );
951     ratingBias1->setFilter( ratingFilter );
952     part->appendBias( ratingBias1Ptr );
953 
954     ratingFilter.numValue = 8;
955     Dynamic::TagMatchBias* ratingBias2 = new Dynamic::TagMatchBias();
956     Dynamic::BiasPtr ratingBias2Ptr( ratingBias2 );
957     ratingBias2->setFilter( ratingFilter );
958     part->appendBias( ratingBias2Ptr );
959 
960     part->changeBiasWeight( 2, 0.2 );
961     part->changeBiasWeight( 1, 0.5 );
962 
963     insertPlaylist( 3, playlist );
964 
965 
966     m_activePlaylistIndex = 0;
967 
968     Q_EMIT activeChanged( m_activePlaylistIndex );
969     endResetModel();
970 }
971 
972 void
serializeIndex(QDataStream * stream,const QModelIndex & index) const973 Dynamic::DynamicModel::serializeIndex( QDataStream *stream, const QModelIndex& index ) const
974 {
975     QList<int> rows;
976     QModelIndex current = index;
977     while( current.isValid() )
978     {
979         rows.prepend( current.row() );
980         current = current.parent();
981     }
982 
983     foreach( int row, rows )
984         *stream << row;
985     *stream << -1;
986 }
987 
988 QModelIndex
unserializeIndex(QDataStream * stream) const989 Dynamic::DynamicModel::unserializeIndex( QDataStream *stream ) const
990 {
991     QModelIndex result;
992     do
993     {
994         int row;
995         *stream >> row;
996         if( row < 0 )
997             break;
998         result = index( row, 0, result );
999     } while( result.isValid() );
1000     return result;
1001 }
1002 
1003 Dynamic::BiasedPlaylist*
cloneList(Dynamic::BiasedPlaylist * list)1004 Dynamic::DynamicModel::cloneList( Dynamic::BiasedPlaylist* list )
1005 {
1006     QByteArray bytes;
1007     QBuffer buffer( &bytes, 0 );
1008     buffer.open( QIODevice::ReadWrite );
1009 
1010     // write the list
1011     QXmlStreamWriter xmlWriter( &buffer );
1012     xmlWriter.writeStartElement( QStringLiteral("playlist") );
1013     list->toXml( &xmlWriter );
1014     xmlWriter.writeEndElement();
1015 
1016     // and read a new list
1017     buffer.seek( 0 );
1018     QXmlStreamReader xmlReader( &buffer );
1019     while( !xmlReader.isStartElement() )
1020         xmlReader.readNext();
1021     return new Dynamic::BiasedPlaylist( &xmlReader, this );
1022 }
1023 
1024 Dynamic::BiasPtr
cloneBias(Dynamic::BiasPtr bias)1025 Dynamic::DynamicModel::cloneBias( Dynamic::BiasPtr bias )
1026 {
1027     return bias->clone();
1028 }
1029 
1030 void
playlistChanged(Dynamic::DynamicPlaylist * p)1031 Dynamic::DynamicModel::playlistChanged( Dynamic::DynamicPlaylist* p )
1032 {
1033     DEBUG_BLOCK;
1034     QModelIndex index = this->index( p );
1035     Q_EMIT dataChanged( index, index );
1036 }
1037 
1038 void
biasChanged(const Dynamic::BiasPtr & b)1039 Dynamic::DynamicModel::biasChanged( const Dynamic::BiasPtr &b )
1040 {
1041     QModelIndex index = this->index( b );
1042     Q_EMIT dataChanged( index, index );
1043 }
1044 
1045 void
beginRemoveBias(Dynamic::BiasedPlaylist * parent)1046 Dynamic::DynamicModel::beginRemoveBias( Dynamic::BiasedPlaylist* parent )
1047 {
1048     QModelIndex index = this->index( parent );
1049     beginRemoveRows( index, 0, 0 );
1050 }
1051 
1052 void
beginRemoveBias(const Dynamic::BiasPtr & parent,int index)1053 Dynamic::DynamicModel::beginRemoveBias( const Dynamic::BiasPtr &parent, int index )
1054 {
1055     QModelIndex parentIndex = this->index( parent );
1056     beginRemoveRows( parentIndex, index, index );
1057 }
1058 
1059 void
endRemoveBias()1060 Dynamic::DynamicModel::endRemoveBias()
1061 {
1062     endRemoveRows();
1063 }
1064 
1065 void
beginInsertBias(Dynamic::BiasedPlaylist * parent)1066 Dynamic::DynamicModel::beginInsertBias( Dynamic::BiasedPlaylist* parent )
1067 {
1068     QModelIndex index = this->index( parent );
1069     beginInsertRows( index, 0, 0 );
1070 }
1071 
1072 
1073 void
beginInsertBias(const Dynamic::BiasPtr & parent,int index)1074 Dynamic::DynamicModel::beginInsertBias( const Dynamic::BiasPtr &parent, int index )
1075 {
1076     QModelIndex parentIndex = this->index( parent );
1077     beginInsertRows( parentIndex, index, index );
1078 }
1079 
1080 void
endInsertBias()1081 Dynamic::DynamicModel::endInsertBias()
1082 {
1083     endInsertRows();
1084 }
1085 
1086 void
beginMoveBias(const Dynamic::BiasPtr & parent,int from,int to)1087 Dynamic::DynamicModel::beginMoveBias( const Dynamic::BiasPtr &parent, int from, int to )
1088 {
1089     QModelIndex parentIndex = this->index( parent );
1090     beginMoveRows( parentIndex, from, from, parentIndex, to );
1091 }
1092 
1093 void
endMoveBias()1094 Dynamic::DynamicModel::endMoveBias()
1095 {
1096     endMoveRows();
1097 }
1098 
1099 
1100 // --- debug methods
1101 
1102 static QString
biasToString(Dynamic::BiasPtr bias,int level)1103 biasToString( Dynamic::BiasPtr bias, int level )
1104 {
1105     QString result;
1106     result += QStringLiteral(" ").repeated(level) + bias->toString() + ' ' + QString::number(quintptr(bias.data()), 16) + '\n';
1107     if( Dynamic::AndBias* aBias = qobject_cast<Dynamic::AndBias*>(bias.data()) )
1108     {
1109         foreach( Dynamic::BiasPtr bias2, aBias->biases() )
1110             result += biasToString( bias2, level + 1 );
1111     }
1112     return result;
1113 }
1114 
1115 QString
toString()1116 Dynamic::DynamicModel::toString()
1117 {
1118     QString result;
1119 
1120     foreach( Dynamic::DynamicPlaylist* playlist, m_playlists )
1121     {
1122         result += playlist->title() + ' ' + QString::number(quintptr(playlist), 16) + '\n';
1123         if( Dynamic::BiasedPlaylist* bPlaylist = qobject_cast<Dynamic::BiasedPlaylist*>(playlist ) )
1124             result += biasToString( bPlaylist->bias(), 1 );
1125     }
1126     return result;
1127 }
1128 
1129