1 /****************************************************************************************
2 * Copyright (c) 2008-2012 Soren Harward <stharward@gmail.com> *
3 * *
4 * This program is free software; you can redistribute it and/or modify it under *
5 * the terms of the GNU General Public License as published by the Free Software *
6 * Foundation; either version 2 of the License, or (at your option) any later *
7 * version. *
8 * *
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY *
10 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
11 * PARTICULAR PURPOSE. See the GNU General Public License for more details. *
12 * *
13 * You should have received a copy of the GNU General Public License along with *
14 * this program. If not, see <http://www.gnu.org/licenses/>. *
15 ****************************************************************************************/
16
17 #define DEBUG_PREFIX "APG::PresetModel"
18
19 #include "PresetModel.h"
20
21 #include "amarokconfig.h"
22 #include "core/logger/Logger.h"
23 #include "core/collections/Collection.h"
24 #include "core/support/Amarok.h"
25 #include "core/support/Components.h"
26 #include "core/support/Debug.h"
27 #include "core-impl/collections/support/CollectionManager.h"
28 #include "playlistgenerator/Preset.h"
29 #include "playlistgenerator/PresetEditDialog.h"
30
31 #include <QAbstractItemModel>
32 #include <QDesktopServices>
33 #include <QDialog>
34 #include <QDomDocument>
35 #include <QDomElement>
36 #include <QFile>
37 #include <QList>
38 #include <QUrl>
39 #include <QVariant>
40
41 APG::PresetModel* APG::PresetModel::s_instance = nullptr;
42
instance()43 APG::PresetModel* APG::PresetModel::instance()
44 {
45 if ( s_instance == nullptr ) {
46 s_instance = new PresetModel();
47 }
48
49 return s_instance;
50 }
51
52 void
destroy()53 APG::PresetModel::destroy()
54 {
55 s_instance->savePresetsToXml( Amarok::saveLocation() + "playlistgenerator.xml", s_instance->m_presetList );
56 delete s_instance;
57 s_instance = nullptr;
58 }
59
PresetModel()60 APG::PresetModel::PresetModel()
61 : QAbstractListModel()
62 , m_activePresetIndex( 0 )
63 {
64 loadPresetsFromXml( Amarok::saveLocation() + "playlistgenerator.xml", true );
65 }
66
~PresetModel()67 APG::PresetModel::~PresetModel()
68 {
69 while ( m_presetList.size() > 0 ) {
70 m_presetList.takeFirst()->deleteLater();
71 }
72 }
73
74 QVariant
data(const QModelIndex & idx,int role) const75 APG::PresetModel::data( const QModelIndex& idx, int role ) const
76 {
77 if ( !idx.isValid() )
78 return QVariant();
79
80 if ( idx.row() >= m_presetList.size() )
81 return QVariant();
82
83 APG::PresetPtr item = m_presetList.at( idx.row() );
84
85 switch ( role ) {
86 case Qt::DisplayRole:
87 case Qt::EditRole:
88 return item->title();
89 break;
90 default:
91 return QVariant();
92 }
93
94 return QVariant();
95 }
96
97 QModelIndex
index(int row,int column,const QModelIndex &) const98 APG::PresetModel::index( int row, int column, const QModelIndex& ) const
99 {
100 if ( rowCount() <= row )
101 return QModelIndex();
102
103 return createIndex( row, column);
104 }
105
106 int
rowCount(const QModelIndex &) const107 APG::PresetModel::rowCount( const QModelIndex& ) const
108 {
109 return m_presetList.size();
110 }
111
112 APG::PresetPtr
activePreset() const113 APG::PresetModel::activePreset() const
114 {
115 if ( m_activePresetIndex && m_activePresetIndex->isValid() )
116 return m_presetList.at( m_activePresetIndex->row() );
117 else
118 return APG::PresetPtr();
119 }
120
121 void
addNew()122 APG::PresetModel::addNew()
123 {
124 insertPreset( APG::Preset::createNew() );
125 }
126
127 void
edit()128 APG::PresetModel::edit()
129 {
130 editPreset( createIndex( m_activePresetIndex->row(), 0 ) );
131 }
132
133 void
editPreset(const QModelIndex & index)134 APG::PresetModel::editPreset( const QModelIndex& index )
135 {
136 // TODO: possible enhancement: instead of using a modal dialog, use a QMap that allows
137 // only one dialog per preset to be open at once
138 PresetPtr ps = m_presetList.at( index.row() );
139 QDialog* d = new PresetEditDialog( ps );
140 d->exec();
141 }
142
143 void
exportActive()144 APG::PresetModel::exportActive()
145 {
146 auto d = new ExportDialog( activePreset() );
147 connect( d, &ExportDialog::pleaseExport, this, &PresetModel::savePresetsToXml );
148 d->exec();
149 }
150
151 void
import()152 APG::PresetModel::import()
153 {
154 const QString filename = QFileDialog::getOpenFileName( nullptr, i18n("Import preset"),
155 QStandardPaths::writableLocation( QStandardPaths::MusicLocation ),
156 QStringLiteral("%1 (*.xml)").arg(i18n("Preset files") ));
157 if( !filename.isEmpty() )
158 loadPresetsFromXml( filename );
159 }
160
161 void
removeActive()162 APG::PresetModel::removeActive()
163 {
164 if ( m_presetList.size() < 1 )
165 return;
166
167 if ( m_activePresetIndex && m_activePresetIndex->isValid() ) {
168 int row = m_activePresetIndex->row();
169 beginRemoveRows( QModelIndex(), row, row );
170 APG::PresetPtr p = m_presetList.takeAt( row );
171 p->deleteLater();
172 endRemoveRows();
173 }
174 }
175
176 void
runGenerator(int q)177 APG::PresetModel::runGenerator( int q )
178 {
179 activePreset()->generate( q );
180 }
181
182 void
setActivePreset(const QModelIndex & index)183 APG::PresetModel::setActivePreset( const QModelIndex& index )
184 {
185 if ( m_activePresetIndex )
186 delete m_activePresetIndex;
187 m_activePresetIndex = new QPersistentModelIndex( index );
188 }
189
190 void
savePresetsToXmlDefault() const191 APG::PresetModel::savePresetsToXmlDefault() const
192 {
193 savePresetsToXml( Amarok::saveLocation() + "playlistgenerator.xml", m_presetList );
194 }
195
196 void
savePresetsToXml(const QString & filename,const QList<APG::PresetPtr> & pl) const197 APG::PresetModel::savePresetsToXml( const QString& filename, const QList<APG::PresetPtr> &pl ) const
198 {
199 QDomDocument xmldoc;
200 QDomElement base = xmldoc.createElement( QStringLiteral("playlistgenerator") );
201 QList<QDomNode*> nodes;
202 foreach ( APG::PresetPtr ps, pl ) {
203 QDomElement* elemPtr = ps->toXml( xmldoc );
204 base.appendChild( (*elemPtr) );
205 nodes << elemPtr;
206 }
207
208 xmldoc.appendChild( base );
209 QFile file( filename );
210 if ( file.open( QIODevice::WriteOnly | QIODevice::Text ) ) {
211 QTextStream out( &file );
212 out.setCodec( "UTF-8" );
213 xmldoc.save( out, 2, QDomNode::EncodingFromTextStream );
214 if( !filename.contains( QLatin1String("playlistgenerator.xml") ) )
215 {
216 Amarok::Logger::longMessage( i18n("Preset exported to %1", filename),
217 Amarok::Logger::Information );
218 }
219 }
220 else
221 {
222 Amarok::Logger::longMessage(
223 i18n("Preset could not be exported to %1", filename), Amarok::Logger::Error );
224 error() << "Can not write presets to " << filename;
225 }
226 qDeleteAll( nodes );
227 }
228
229 void
loadPresetsFromXml(const QString & filename,bool createDefaults)230 APG::PresetModel::loadPresetsFromXml( const QString& filename, bool createDefaults )
231 {
232 QFile file( filename );
233 if ( file.open( QIODevice::ReadOnly ) ) {
234 QDomDocument document;
235 if ( document.setContent( &file ) ) {
236 debug() << "Reading presets from" << filename;
237 parseXmlToPresets( document );
238 } else {
239 error() << "Failed to read" << filename;
240 Amarok::Logger::longMessage(
241 i18n("Presets could not be imported from %1", filename),
242 Amarok::Logger::Error );
243 }
244 file.close();
245 } else {
246 if ( !createDefaults ) {
247 Amarok::Logger::longMessage(
248 i18n("%1 could not be opened for preset import", filename),
249 Amarok::Logger::Error );
250 } else {
251 QDomDocument document;
252 QString translatedPresetExamples( presetExamples.arg(
253 i18n("Example 1: new tracks added this week"),
254 i18n("Example 2: rock or pop music"),
255 i18n("Example 3: about one hour of tracks from different artists"),
256 i18n("Example 4: like my favorite radio station"),
257 i18n("Example 5: an 80-minute CD of rock, metal, and industrial") ) );
258 document.setContent( translatedPresetExamples );
259 debug() << "Reading built-in example presets";
260 parseXmlToPresets( document );
261 }
262 error() << "Can not open" << filename;
263 }
264 }
265
266 void
insertPreset(const APG::PresetPtr & ps)267 APG::PresetModel::insertPreset( const APG::PresetPtr &ps )
268 {
269 if ( ps ) {
270 int row = m_presetList.size();
271 beginInsertRows( QModelIndex(), row, row );
272 m_presetList.append( ps );
273 endInsertRows();
274 connect( ps.data(), &APG::Preset::lock, this, &PresetModel::lock );
275 }
276 }
277
278 void
parseXmlToPresets(QDomDocument & document)279 APG::PresetModel::parseXmlToPresets( QDomDocument& document )
280 {
281 QDomElement rootelem = document.documentElement();
282 for ( int i = 0; i < rootelem.childNodes().count(); i++ ) {
283 QDomElement e = rootelem.childNodes().at( i ).toElement();
284 if ( e.tagName() == QLatin1String("generatorpreset") ) {
285 debug() << "creating a new generator preset";
286 insertPreset( APG::Preset::createFromXml( e ) );
287 } else {
288 debug() << "Don't know what to do with tag: " << e.tagName();
289 }
290 }
291 }
292
293 /*
294 * ExportDialog nested class
295 */
ExportDialog(APG::PresetPtr ps)296 APG::PresetModel::ExportDialog::ExportDialog( APG::PresetPtr ps )
297 : QFileDialog( nullptr, i18n( "Export \"%1\" preset", ps->title() ),
298 QStandardPaths::writableLocation( QStandardPaths::MusicLocation ),
299 i18n("Preset files (*.xml)") )
300 {
301 m_presetsToExportList.append( ps );
302 setFileMode( QFileDialog::AnyFile );
303 selectFile( ps->title() + ".xml" );
304 setAcceptMode( QFileDialog::AcceptSave );
305 connect( this, &ExportDialog::accepted, this, &ExportDialog::recvAccept );
306 }
307
~ExportDialog()308 APG::PresetModel::ExportDialog::~ExportDialog() {}
309
310 void
recvAccept() const311 APG::PresetModel::ExportDialog::recvAccept() const
312 {
313 Q_EMIT pleaseExport( selectedFiles().first(), m_presetsToExportList );
314 }
315
316 const QString APG::PresetModel::presetExamples =
317 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
318 "<playlistgenerator>"
319 " <generatorpreset title=\"%1\">"
320 " <constrainttree>"
321 " <group matchtype=\"all\">"
322 " <constraint field=\"create date\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"7 days\" strictness=\"0.8\"/>"
323 " <constraint field=\"play count\" comparison=\"1\" invert=\"false\" type=\"TagMatch\" value=\"\" strictness=\"1\"/>"
324 " </group>"
325 " </constrainttree>"
326 " </generatorpreset>"
327 " <generatorpreset title=\"%2\">"
328 " <constrainttree>"
329 " <group matchtype=\"any\">"
330 " <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Rock\" strictness=\"1\"/>"
331 " <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Pop\" strictness=\"1\"/>"
332 " </group>"
333 " </constrainttree>"
334 " </generatorpreset>"
335 " <generatorpreset title=\"%3\">"
336 " <constrainttree>"
337 " <group matchtype=\"all\">"
338 " <constraint comparison=\"1\" duration=\"3600000\" type=\"PlaylistDuration\" strictness=\"0.3\"/>"
339 " <constraint field=\"2\" type=\"PreventDuplicates\"/>"
340 " </group>"
341 " </constrainttree>"
342 " </generatorpreset>"
343 " <generatorpreset title=\"%4\">"
344 " <constrainttree>"
345 " <group matchtype=\"all\">"
346 " <constraint field=\"0\" type=\"PreventDuplicates\"/>"
347 " <constraint field=\"last played\" comparison=\"3\" invert=\"true\" type=\"TagMatch\" value=\"7 days\" strictness=\"0.4\"/>"
348 " <constraint field=\"rating\" comparison=\"2\" invert=\"false\" type=\"TagMatch\" value=\"6\" strictness=\"1\"/>"
349 " <constraint comparison=\"1\" duration=\"10800000\" type=\"PlaylistDuration\" strictness=\"0.3\"/>"
350 " </group>"
351 " </constrainttree>"
352 " </generatorpreset>"
353 " <generatorpreset title=\"%5\">"
354 " <constrainttree>"
355 " <group matchtype=\"all\">"
356 " <group matchtype=\"any\">"
357 " <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Rock\" strictness=\"1\"/>"
358 " <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Metal\" strictness=\"1\"/>"
359 " <constraint field=\"genre\" comparison=\"3\" invert=\"false\" type=\"TagMatch\" value=\"Industrial\" strictness=\"1\"/>"
360 " </group>"
361 " <group matchtype=\"all\">"
362 " <constraint comparison=\"2\" duration=\"4500000\" type=\"PlaylistDuration\" strictness=\"0.4\"/>"
363 " <constraint comparison=\"0\" duration=\"4800000\" type=\"PlaylistDuration\" strictness=\"1\"/>"
364 " </group>"
365 " </group>"
366 " </constrainttree>"
367 " </generatorpreset>"
368 "</playlistgenerator>";
369