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