1 /****************************************************************************************
2 * Copyright (c) 2008-2009 Nikolaj Hald Nielsen <nhn@kde.org> *
3 * Copyright (c) 2009 Seb Ruiz <ruiz@kde.org> *
4 * Copyright (c) 2010 Oleksandr Khayrullin <saniokh@gmail.com> *
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 #include "LayoutManager.h"
20
21 #include "core/support/Amarok.h"
22 #include "core/support/Components.h"
23 #include "core/support/Debug.h"
24 #include "core/logger/Logger.h"
25 #include "playlist/PlaylistDefines.h"
26 #include "playlist/PlaylistModelStack.h"
27
28 #include <KConfigGroup>
29 #include <KMessageBox>
30
31 #include <QDir>
32 #include <QDomDocument>
33 #include <QFile>
34 #include <QStandardPaths>
35 #include <QStringList>
36
37
38 namespace Playlist {
39
40 static const QString PREVIEW_LAYOUT = QStringLiteral("%%PREVIEW%%");
41 LayoutManager* LayoutManager::s_instance = nullptr;
42
instance()43 LayoutManager* LayoutManager::instance()
44 {
45 if( !s_instance )
46 s_instance = new LayoutManager();
47 return s_instance;
48 }
49
LayoutManager()50 LayoutManager::LayoutManager()
51 : QObject()
52 {
53 DEBUG_BLOCK
54
55 loadDefaultLayouts();
56 loadUserLayouts();
57 orderLayouts();
58
59 KConfigGroup config = Amarok::config(QStringLiteral("Playlist Layout"));
60 m_activeLayout = config.readEntry( "CurrentLayout", "Default" );
61 if( !layouts().contains( m_activeLayout ) )
62 m_activeLayout = QStringLiteral("Default");
63 Playlist::ModelStack::instance()->groupingProxy()->setGroupingCategory( activeLayout().groupBy() );
64 }
65
layouts() const66 QStringList LayoutManager::layouts() const
67 {
68 return m_layoutNames;
69 }
70
setActiveLayout(const QString & layout)71 void LayoutManager::setActiveLayout( const QString &layout )
72 {
73 m_activeLayout = layout;
74 Amarok::config( QStringLiteral("Playlist Layout") ).writeEntry( "CurrentLayout", m_activeLayout );
75 Q_EMIT( activeLayoutChanged() );
76
77 //Change the grouping style to that of this layout.
78 Playlist::ModelStack::instance()->groupingProxy()->setGroupingCategory( activeLayout().groupBy() );
79
80 }
81
setPreviewLayout(const PlaylistLayout & layout)82 void LayoutManager::setPreviewLayout( const PlaylistLayout &layout )
83 {
84 DEBUG_BLOCK
85 m_activeLayout = PREVIEW_LAYOUT;
86 m_previewLayout = layout;
87 Q_EMIT( activeLayoutChanged() );
88
89 //Change the grouping style to that of this layout.
90 Playlist::ModelStack::instance()->groupingProxy()->setGroupingCategory( activeLayout().groupBy() );
91 }
92
updateCurrentLayout(const PlaylistLayout & layout)93 void LayoutManager::updateCurrentLayout( const PlaylistLayout &layout )
94 {
95 //Do not store preview layouts.
96 if ( m_activeLayout == PREVIEW_LAYOUT )
97 return;
98
99 if ( m_layouts.value( m_activeLayout ).isEditable() )
100 {
101 addUserLayout( m_activeLayout, layout );
102 setActiveLayout( m_activeLayout );
103 }
104 else
105 {
106 //create a writable copy of this layout. (Copy on Write)
107 QString newLayoutName = i18n( "copy of %1", m_activeLayout );
108 QString orgCopyName = newLayoutName;
109
110 int copyNumber = 1;
111 QStringList existingLayouts = LayoutManager::instance()->layouts();
112 while( existingLayouts.contains( newLayoutName ) )
113 {
114 copyNumber++;
115 newLayoutName = i18nc( "adds a copy number to a generated name if the name already exists, for instance 'copy of Foo 2' if 'copy of Foo' is taken", "%1 %2", orgCopyName, copyNumber );
116 }
117
118
119 Amarok::Logger::longMessage( i18n( "Current layout '%1' is read only. " \
120 "Creating a new layout '%2' with your changes and setting this as active",
121 m_activeLayout, newLayoutName )
122 );
123
124 addUserLayout( newLayoutName, layout );
125 setActiveLayout( newLayoutName );
126 }
127 }
128
activeLayout() const129 PlaylistLayout LayoutManager::activeLayout() const
130 {
131 if ( m_activeLayout == PREVIEW_LAYOUT )
132 return m_previewLayout;
133 return m_layouts.value( m_activeLayout );
134 }
135
loadUserLayouts()136 void LayoutManager::loadUserLayouts()
137 {
138 QDir layoutsDir = QDir( Amarok::saveLocation( QStringLiteral("playlist_layouts/") ) );
139
140 layoutsDir.setSorting( QDir::Name );
141
142 QStringList filters;
143 filters << QStringLiteral("*.xml") << QStringLiteral("*.XML");
144 layoutsDir.setNameFilters( filters );
145 layoutsDir.setSorting( QDir::Name );
146
147 QFileInfoList list = layoutsDir.entryInfoList();
148
149 for ( int i = 0; i < list.size(); ++i )
150 {
151 QFileInfo fileInfo = list.at(i);
152 loadLayouts( layoutsDir.filePath( fileInfo.fileName() ), true );
153 }
154 }
155
loadDefaultLayouts()156 void LayoutManager::loadDefaultLayouts()
157 {
158 const QString dataLocation = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
159 QStringLiteral("amarok/data"),
160 QStandardPaths::LocateDirectory);
161
162
163 QString configFile = dataLocation + QStringLiteral("/DefaultPlaylistLayouts.xml");
164 loadLayouts( configFile, false );
165 }
166
167
loadLayouts(const QString & fileName,bool user)168 void LayoutManager::loadLayouts( const QString &fileName, bool user )
169 {
170 DEBUG_BLOCK
171 QDomDocument doc( QStringLiteral("layouts") );
172
173 if ( !QFile::exists( fileName ) )
174 {
175 debug() << "file " << fileName << "does not exist";
176 return;
177 }
178
179 QFile *file = new QFile( fileName );
180 if( !file || !file->open( QIODevice::ReadOnly ) )
181 {
182 debug() << "error reading file " << fileName;
183 return;
184 }
185 if ( !doc.setContent( file ) )
186 {
187 debug() << "error parsing file " << fileName;
188 file->close();
189 return ;
190 }
191 file->close();
192 delete file;
193
194 QDomElement layouts_element = doc.firstChildElement( QStringLiteral("playlist_layouts") );
195 QDomNodeList layouts = layouts_element.elementsByTagName(QStringLiteral("layout"));
196
197 int index = 0;
198 while ( index < layouts.size() )
199 {
200 QDomNode layout = layouts.item( index );
201 index++;
202
203 QString layoutName = layout.toElement().attribute( QStringLiteral("name"), QLatin1String("") );
204 debug() << "loading layout " << layoutName;
205
206 PlaylistLayout currentLayout;
207 currentLayout.setEditable( user );
208 currentLayout.setInlineControls( layout.toElement().attribute( QStringLiteral("inline_controls"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 );
209 currentLayout.setTooltips( layout.toElement().attribute( QStringLiteral("tooltips"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 );
210
211 //For backwards compatibility, if a grouping is not set in the XML file assume "group by album" (which was previously the default)
212 currentLayout.setGroupBy( layout.toElement().attribute( QStringLiteral("group_by"), QStringLiteral("Album") ) );
213 debug() << "grouping mode is: " << layout.toElement().attribute( QStringLiteral("group_by"), QStringLiteral("Album") );
214
215
216 currentLayout.setLayoutForPart( PlaylistLayout::Head, parseItemConfig( layout.toElement().firstChildElement( QStringLiteral("group_head") ) ) );
217 currentLayout.setLayoutForPart( PlaylistLayout::StandardBody, parseItemConfig( layout.toElement().firstChildElement( QStringLiteral("group_body") ) ) );
218 QDomElement variousArtistsXML = layout.toElement().firstChildElement( QStringLiteral("group_variousArtistsBody") );
219 if ( !variousArtistsXML.isNull() )
220 currentLayout.setLayoutForPart( PlaylistLayout::VariousArtistsBody, parseItemConfig( variousArtistsXML ) );
221 else // Handle old custom layout XMLs
222 currentLayout.setLayoutForPart( PlaylistLayout::VariousArtistsBody, parseItemConfig( layout.toElement().firstChildElement( QStringLiteral("group_body") ) ) );
223 currentLayout.setLayoutForPart( PlaylistLayout::Single, parseItemConfig( layout.toElement().firstChildElement( QStringLiteral("single_track") ) ) );
224
225 if ( !layoutName.isEmpty() )
226 m_layouts.insert( layoutName, currentLayout );
227 }
228 }
229
parseItemConfig(const QDomElement & elem) const230 LayoutItemConfig LayoutManager::parseItemConfig( const QDomElement &elem ) const
231 {
232 const bool showCover = ( elem.attribute( QStringLiteral("show_cover"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 );
233 const int activeIndicatorRow = elem.attribute( QStringLiteral("active_indicator_row"), QStringLiteral("0") ).toInt();
234
235 LayoutItemConfig config;
236 config.setShowCover( showCover );
237 config.setActiveIndicatorRow( activeIndicatorRow );
238
239 QDomNodeList rows = elem.elementsByTagName(QStringLiteral("row"));
240
241 int index = 0;
242 while ( index < rows.size() )
243 {
244 QDomNode rowNode = rows.item( index );
245 index++;
246
247 LayoutItemConfigRow row;
248
249 QDomNodeList elements = rowNode.toElement().elementsByTagName(QStringLiteral("element"));
250
251 int index2 = 0;
252 while ( index2 < elements.size() )
253 {
254 QDomNode elementNode = elements.item( index2 );
255 index2++;
256
257 int value = columnForName( elementNode.toElement().attribute( QStringLiteral("value"), QStringLiteral("Title") ) );
258 QString prefix = elementNode.toElement().attribute( QStringLiteral("prefix"), QString() );
259 QString sufix = elementNode.toElement().attribute( QStringLiteral("suffix"), QString() );
260 qreal size = elementNode.toElement().attribute( QStringLiteral("size"), QStringLiteral("0.0") ).toDouble();
261 bool bold = ( elementNode.toElement().attribute( QStringLiteral("bold"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 );
262 bool italic = ( elementNode.toElement().attribute( QStringLiteral("italic"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 );
263 bool underline = ( elementNode.toElement().attribute( QStringLiteral("underline"), QStringLiteral("false") ).compare( QLatin1String("true"), Qt::CaseInsensitive ) == 0 );
264 QString alignmentString = elementNode.toElement().attribute( QStringLiteral("alignment"), QStringLiteral("left") );
265 Qt::Alignment alignment;
266
267
268 if ( alignmentString.compare( QLatin1String("left"), Qt::CaseInsensitive ) == 0 )
269 alignment = Qt::AlignLeft | Qt::AlignVCenter;
270 else if ( alignmentString.compare( QLatin1String("right"), Qt::CaseInsensitive ) == 0 )
271 alignment = Qt::AlignRight| Qt::AlignVCenter;
272 else
273 alignment = Qt::AlignCenter| Qt::AlignVCenter;
274
275 row.addElement( LayoutItemConfigRowElement( value, size, bold, italic, underline,
276 alignment, prefix, sufix ) );
277 }
278
279 config.addRow( row );
280 }
281
282 return config;
283 }
284
layout(const QString & layout) const285 PlaylistLayout LayoutManager::layout( const QString &layout ) const
286 {
287 return m_layouts.value( layout );
288 }
289
addUserLayout(const QString & name,PlaylistLayout layout)290 void LayoutManager::addUserLayout( const QString &name, PlaylistLayout layout )
291 {
292 layout.setEditable( true );
293 if( m_layouts.find( name ) != m_layouts.end() )
294 m_layouts.remove( name );
295 else
296 m_layoutNames.append( name );
297
298 m_layouts.insert( name, layout );
299
300
301 QDomDocument doc( QStringLiteral("layouts") );
302 QDomElement layouts_element = doc.createElement( QStringLiteral("playlist_layouts") );
303 QDomElement newLayout = doc.createElement( ("layout" ) );
304 newLayout.setAttribute( QStringLiteral("name"), name );
305
306 doc.appendChild( layouts_element );
307 layouts_element.appendChild( newLayout );
308
309 Q_EMIT( layoutListChanged() );
310
311 QDomElement body = doc.createElement( QStringLiteral("body") );
312 QDomElement single = doc.createElement( QStringLiteral("single") );
313
314 newLayout.appendChild( createItemElement( doc, QStringLiteral("single_track"), layout.layoutForPart( PlaylistLayout::Single ) ) );
315 newLayout.appendChild( createItemElement( doc, QStringLiteral("group_head"), layout.layoutForPart( PlaylistLayout::Head ) ) );
316 newLayout.appendChild( createItemElement( doc, QStringLiteral("group_body"), layout.layoutForPart( PlaylistLayout::StandardBody ) ) );
317 newLayout.appendChild( createItemElement( doc, QStringLiteral("group_variousArtistsBody"), layout.layoutForPart( PlaylistLayout::VariousArtistsBody ) ) );
318
319 if( layout.inlineControls() )
320 newLayout.setAttribute( QStringLiteral("inline_controls"), QStringLiteral("true") );
321
322 if( layout.tooltips() )
323 newLayout.setAttribute( QStringLiteral("tooltips"), QStringLiteral("true") );
324
325 newLayout.setAttribute( QStringLiteral("group_by"), layout.groupBy() );
326
327 QDir layoutsDir = QDir( Amarok::saveLocation( QStringLiteral("playlist_layouts/") ) );
328
329 //make sure that this directory exists
330 if ( !layoutsDir.exists() )
331 layoutsDir.mkpath( Amarok::saveLocation( QStringLiteral("playlist_layouts/") ) );
332
333 QFile file( layoutsDir.filePath( name + ".xml" ) );
334 if ( !file.open(QIODevice::WriteOnly | QIODevice::Text) )
335 return;
336
337 QTextStream out( &file );
338 out << doc.toString();
339 }
340
createItemElement(QDomDocument doc,const QString & name,const LayoutItemConfig & item) const341 QDomElement LayoutManager::createItemElement( QDomDocument doc, const QString &name, const LayoutItemConfig & item ) const
342 {
343 QDomElement element = doc.createElement( name );
344
345 QString showCover = item.showCover() ? "true" : "false";
346 element.setAttribute ( QStringLiteral("show_cover"), showCover );
347 element.setAttribute ( QStringLiteral("active_indicator_row"), QString::number( item.activeIndicatorRow() ) );
348
349 for( int i = 0; i < item.rows(); i++ )
350 {
351 LayoutItemConfigRow row = item.row( i );
352
353 QDomElement rowElement = doc.createElement( QStringLiteral("row") );
354 element.appendChild( rowElement );
355
356 for( int j = 0; j < row.count(); j++ ) {
357 LayoutItemConfigRowElement element = row.element( j );
358 QDomElement elementElement = doc.createElement( QStringLiteral("element") );
359
360 elementElement.setAttribute ( QStringLiteral("prefix"), element.prefix() );
361 elementElement.setAttribute ( QStringLiteral("suffix"), element.suffix() );
362 elementElement.setAttribute ( QStringLiteral("value"), internalColumnName( static_cast<Playlist::Column>( element.value() ) ) );
363 elementElement.setAttribute ( QStringLiteral("size"), QString::number( element.size() ) );
364 elementElement.setAttribute ( QStringLiteral("bold"), element.bold() ? "true" : "false" );
365 elementElement.setAttribute ( QStringLiteral("italic"), element.italic() ? "true" : "false" );
366 elementElement.setAttribute ( QStringLiteral("underline"), element.underline() ? "true" : "false" );
367
368 QString alignmentString;
369 if ( element.alignment() & Qt::AlignLeft )
370 alignmentString = QStringLiteral("left");
371 else if ( element.alignment() & Qt::AlignRight )
372 alignmentString = QStringLiteral("right");
373 else
374 alignmentString = QStringLiteral("center");
375
376 elementElement.setAttribute ( QStringLiteral("alignment"), alignmentString );
377
378 rowElement.appendChild( elementElement );
379 }
380 }
381
382 return element;
383 }
384
isDefaultLayout(const QString & layout) const385 bool LayoutManager::isDefaultLayout( const QString & layout ) const
386 {
387 if ( m_layouts.keys().contains( layout ) )
388 return !m_layouts.value( layout ).isEditable();
389
390 return false;
391 }
392
activeLayoutName() const393 QString LayoutManager::activeLayoutName() const
394 {
395 return m_activeLayout;
396 }
397
deleteLayout(const QString & layout)398 void LayoutManager::deleteLayout( const QString &layout )
399 {
400 //check if layout is editable
401 if ( m_layouts.value( layout ).isEditable() )
402 {
403 QDir layoutsDir = QDir( Amarok::saveLocation( QStringLiteral("playlist_layouts/") ) );
404 QString xmlFile = layoutsDir.path() + QLatin1Char('/') + layout + ".xml";
405
406 if ( !QFile::remove( xmlFile ) )
407 debug() << "error deleting file" << xmlFile;
408
409 m_layouts.remove( layout );
410 m_layoutNames.removeAll( layout );
411 Q_EMIT( layoutListChanged() );
412
413 if ( layout == m_activeLayout )
414 setActiveLayout( QStringLiteral("Default") );
415 }
416 else
417 KMessageBox::sorry( nullptr, i18n( "The layout '%1' is one of the default layouts and cannot be deleted.", layout ), i18n( "Cannot Delete Default Layouts" ) );
418 }
419
isDeleteable(const QString & layout) const420 bool LayoutManager::isDeleteable( const QString &layout ) const
421 {
422 return m_layouts.value( layout ).isEditable();
423 }
424
moveUp(const QString & layout)425 int LayoutManager::moveUp( const QString &layout )
426 {
427 int index = m_layoutNames.indexOf( layout );
428 if ( index > 0 ) {
429 m_layoutNames.swap ( index, index - 1 );
430 Q_EMIT( layoutListChanged() );
431 storeLayoutOrdering();
432 return index - 1;
433 }
434
435 return index;
436 }
437
moveDown(const QString & layout)438 int LayoutManager::moveDown( const QString &layout )
439 {
440 int index = m_layoutNames.indexOf( layout );
441 if ( index < m_layoutNames.size() -1 ) {
442 m_layoutNames.swap ( index, index + 1 );
443 Q_EMIT( layoutListChanged() );
444 storeLayoutOrdering();
445 return index + 1;
446 }
447
448 return index;
449 }
450
orderLayouts()451 void LayoutManager::orderLayouts()
452 {
453 KConfigGroup config = Amarok::config( QStringLiteral("Playlist Layout") );
454 QString orderString = config.readEntry( "Order", "Default" );
455
456 QStringList knownLayouts = m_layouts.keys();
457
458 QStringList orderingList = orderString.split( QLatin1Char(';'), QString::SkipEmptyParts );
459
460 foreach( const QString &layout, orderingList )
461 {
462 if ( knownLayouts.contains( layout ) )
463 {
464 //skip any layout names that are in config but that we don't know. Perhaps someone manually deleted a layout file
465 m_layoutNames.append( layout );
466 knownLayouts.removeAll( layout );
467 }
468 }
469
470 //now add any layouts that were not in the order config to end of list:
471 foreach( const QString &layout, knownLayouts )
472 m_layoutNames.append( layout );
473 }
474
475 } //namespace Playlist
476
storeLayoutOrdering()477 void Playlist::LayoutManager::storeLayoutOrdering()
478 {
479
480 QString ordering;
481
482 foreach( const QString &name, m_layoutNames )
483 {
484 ordering += name;
485 ordering += ';';
486 }
487
488 if ( !ordering.isEmpty() )
489 ordering.chop( 1 ); //remove trailing;
490
491 KConfigGroup config = Amarok::config(QStringLiteral("Playlist Layout"));
492 config.writeEntry( "Order", ordering );
493 }
494
495
496
497
498