1 /*
2 * ConfigManager.cpp - implementation of class ConfigManager
3 *
4 * Copyright (c) 2005-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
5 *
6 * This file is part of LMMS - https://lmms.io
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public
10 * License as published by the Free Software Foundation; either
11 * version 2 of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public
19 * License along with this program (see COPYING); if not, write to the
20 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301 USA.
22 *
23 */
24
25 #include <QDomElement>
26 #include <QDir>
27 #include <QMessageBox>
28 #include <QApplication>
29 #if QT_VERSION >= 0x050000
30 #include <QStandardPaths>
31 #else
32 #include <QDesktopServices>
33 #endif
34 #include <QtCore/QTextStream>
35
36 #include "ConfigManager.h"
37 #include "MainWindow.h"
38 #include "ProjectVersion.h"
39 #include "GuiApplication.h"
40
41 #include "lmmsversion.h"
42
ensureTrailingSlash(const QString & s)43 static inline QString ensureTrailingSlash( const QString & s )
44 {
45 if( ! s.isEmpty() && !s.endsWith('/') && !s.endsWith('\\') )
46 {
47 return s + '/';
48 }
49 return s;
50 }
51
52
53 ConfigManager * ConfigManager::s_instanceOfMe = NULL;
54
55
ConfigManager()56 ConfigManager::ConfigManager() :
57 m_lmmsRcFile( QDir::home().absolutePath() +"/.lmmsrc.xml" ),
58 #if QT_VERSION >= 0x050000
59 m_workingDir( QStandardPaths::writableLocation( QStandardPaths::DocumentsLocation ) + "/lmms/"),
60 #else
61 m_workingDir( QDesktopServices::storageLocation( QDesktopServices::DocumentsLocation ) + "/lmms/"),
62 #endif
63 m_dataDir( "data:/" ),
64 m_artworkDir( defaultArtworkDir() ),
65 m_vstDir( m_workingDir + "vst/" ),
66 m_gigDir( m_workingDir + GIG_PATH ),
67 m_sf2Dir( m_workingDir + SF2_PATH ),
68 m_version( defaultVersion() )
69 {
70 // Detect < 1.2.0 working directory as a courtesy
71 if ( QFileInfo( QDir::home().absolutePath() + "/lmms/projects/" ).exists() )
72 m_workingDir = QDir::home().absolutePath() + "/lmms/";
73
74 if (! qgetenv("LMMS_DATA_DIR").isEmpty())
75 QDir::addSearchPath("data", QString::fromLocal8Bit(qgetenv("LMMS_DATA_DIR")));
76
77 // If we're in development (lmms is not installed) let's get the source and
78 // binary directories by reading the CMake Cache
79 QDir appPath = qApp->applicationDirPath();
80 // If in tests, get parent directory
81 if (appPath.dirName() == "tests") {
82 appPath.cdUp();
83 }
84 QFile cmakeCache(appPath.absoluteFilePath("CMakeCache.txt"));
85 if (cmakeCache.exists()) {
86 cmakeCache.open(QFile::ReadOnly);
87 QTextStream stream(&cmakeCache);
88
89 // Find the lines containing something like lmms_SOURCE_DIR:static=<dir>
90 // and lmms_BINARY_DIR:static=<dir>
91 int done = 0;
92 while(! stream.atEnd())
93 {
94 QString line = stream.readLine();
95
96 if (line.startsWith("lmms_SOURCE_DIR:")) {
97 QString srcDir = line.section('=', -1).trimmed();
98 QDir::addSearchPath("data", srcDir + "/data/");
99 done++;
100 }
101 if (line.startsWith("lmms_BINARY_DIR:")) {
102 m_lmmsRcFile = line.section('=', -1).trimmed() + QDir::separator() +
103 ".lmmsrc.xml";
104 done++;
105 }
106 if (done == 2)
107 {
108 break;
109 }
110 }
111
112 cmakeCache.close();
113 }
114
115 #ifdef LMMS_BUILD_WIN32
116 QDir::addSearchPath("data", qApp->applicationDirPath() + "/data/");
117 #else
118 QDir::addSearchPath("data", qApp->applicationDirPath().section('/', 0, -2) + "/share/lmms/");
119 #endif
120
121
122 }
123
124
125
126
~ConfigManager()127 ConfigManager::~ConfigManager()
128 {
129 saveConfigFile();
130 }
131
132
upgrade_1_1_90()133 void ConfigManager::upgrade_1_1_90()
134 {
135 // Remove trailing " (bad latency!)" string which was once saved with PulseAudio
136 if( value( "mixer", "audiodev" ).startsWith( "PulseAudio (" ) )
137 {
138 setValue("mixer", "audiodev", "PulseAudio");
139 }
140
141 // MidiAlsaRaw used to store the device info as "Device" instead of "device"
142 if ( value( "MidiAlsaRaw", "device" ).isNull() )
143 {
144 // copy "device" = "Device" and then delete the old "Device" (further down)
145 QString oldDevice = value( "MidiAlsaRaw", "Device" );
146 setValue("MidiAlsaRaw", "device", oldDevice);
147 }
148 if ( !value( "MidiAlsaRaw", "device" ).isNull() )
149 {
150 // delete the old "Device" in the case that we just copied it to "device"
151 // or if the user somehow set both the "Device" and "device" fields
152 deleteValue("MidiAlsaRaw", "Device");
153 }
154 }
155
156
upgrade_1_1_91()157 void ConfigManager::upgrade_1_1_91()
158 {
159 // rename displaydbv to displaydbfs
160 if ( !value( "app", "displaydbv" ).isNull() ) {
161 setValue( "app", "displaydbfs", value( "app", "displaydbv" ) );
162 deleteValue( "app", "displaydbv" );
163 }
164 }
165
166
upgrade()167 void ConfigManager::upgrade()
168 {
169 // Skip the upgrade if versions match
170 if ( m_version == LMMS_VERSION )
171 {
172 return;
173 }
174
175 ProjectVersion createdWith = m_version;
176
177 if ( createdWith.setCompareType(ProjectVersion::Build) < "1.1.90" )
178 {
179 upgrade_1_1_90();
180 }
181
182 if ( createdWith.setCompareType(ProjectVersion::Build) < "1.1.91" )
183 {
184 upgrade_1_1_91();
185 }
186
187 // Don't use old themes as they break the UI (i.e. 0.4 != 1.0, etc)
188 if ( createdWith.setCompareType(ProjectVersion::Minor) != LMMS_VERSION )
189 {
190 m_artworkDir = defaultArtworkDir();
191 }
192
193 // Bump the version, now that we are upgraded
194 m_version = LMMS_VERSION;
195 }
196
defaultVersion() const197 QString ConfigManager::defaultVersion() const
198 {
199 return LMMS_VERSION;
200 }
201
availabeVstEmbedMethods()202 QStringList ConfigManager::availabeVstEmbedMethods()
203 {
204 QStringList methods;
205 methods.append("none");
206 #if QT_VERSION >= 0x050100
207 methods.append("qt");
208 #endif
209 #ifdef LMMS_BUILD_WIN32
210 methods.append("win32");
211 #endif
212 #ifdef LMMS_BUILD_LINUX
213 #if QT_VERSION >= 0x050000
214 if (static_cast<QGuiApplication*>(QApplication::instance())->
215 platformName() == "xcb")
216 #else
217 if (qgetenv("QT_QPA_PLATFORM").isNull()
218 || qgetenv("QT_QPA_PLATFORM") == "xcb")
219 #endif
220 {
221 methods.append("xembed");
222 }
223 #endif
224 return methods;
225 }
226
vstEmbedMethod() const227 QString ConfigManager::vstEmbedMethod() const
228 {
229 QStringList methods = availabeVstEmbedMethods();
230 QString defaultMethod = *(methods.end() - 1);
231 QString currentMethod = value( "ui", "vstembedmethod", defaultMethod );
232 return methods.contains(currentMethod) ? currentMethod : defaultMethod;
233 }
234
hasWorkingDir() const235 bool ConfigManager::hasWorkingDir() const
236 {
237 return QDir( m_workingDir ).exists();
238 }
239
240
setWorkingDir(const QString & wd)241 void ConfigManager::setWorkingDir( const QString & wd )
242 {
243 m_workingDir = ensureTrailingSlash( QDir::cleanPath( wd ) );
244 }
245
246
247
248
setVSTDir(const QString & _vd)249 void ConfigManager::setVSTDir( const QString & _vd )
250 {
251 m_vstDir = ensureTrailingSlash( _vd );
252 }
253
254
255
256
setArtworkDir(const QString & _ad)257 void ConfigManager::setArtworkDir( const QString & _ad )
258 {
259 m_artworkDir = ensureTrailingSlash( _ad );
260 }
261
262
263
264
setLADSPADir(const QString & _fd)265 void ConfigManager::setLADSPADir( const QString & _fd )
266 {
267 m_ladDir = _fd;
268 }
269
270
271
272
setSTKDir(const QString & _fd)273 void ConfigManager::setSTKDir( const QString & _fd )
274 {
275 #ifdef LMMS_HAVE_STK
276 m_stkDir = ensureTrailingSlash( _fd );
277 #endif
278 }
279
280
281
282
setDefaultSoundfont(const QString & _sf)283 void ConfigManager::setDefaultSoundfont( const QString & _sf )
284 {
285 #ifdef LMMS_HAVE_FLUIDSYNTH
286 m_defaultSoundfont = _sf;
287 #endif
288 }
289
290
291
292
setBackgroundArtwork(const QString & _ba)293 void ConfigManager::setBackgroundArtwork( const QString & _ba )
294 {
295 m_backgroundArtwork = _ba;
296 }
297
setGIGDir(const QString & gd)298 void ConfigManager::setGIGDir(const QString &gd)
299 {
300 m_gigDir = gd;
301 }
302
setSF2Dir(const QString & sfd)303 void ConfigManager::setSF2Dir(const QString &sfd)
304 {
305 m_sf2Dir = sfd;
306 }
307
308
createWorkingDir()309 void ConfigManager::createWorkingDir()
310 {
311 QDir().mkpath( m_workingDir );
312
313 QDir().mkpath( userProjectsDir() );
314 QDir().mkpath( userTemplateDir() );
315 QDir().mkpath( userSamplesDir() );
316 QDir().mkpath( userPresetsDir() );
317 QDir().mkpath( userGigDir() );
318 QDir().mkpath( userSf2Dir() );
319 QDir().mkpath( userVstDir() );
320 QDir().mkpath( userLadspaDir() );
321 }
322
323
324
addRecentlyOpenedProject(const QString & file)325 void ConfigManager::addRecentlyOpenedProject( const QString & file )
326 {
327 QFileInfo recentFile( file );
328 if( recentFile.suffix().toLower() == "mmp" ||
329 recentFile.suffix().toLower() == "mmpz" ||
330 recentFile.suffix().toLower() == "mpt" )
331 {
332 m_recentlyOpenedProjects.removeAll( file );
333 if( m_recentlyOpenedProjects.size() > 50 )
334 {
335 m_recentlyOpenedProjects.removeLast();
336 }
337 m_recentlyOpenedProjects.push_front( file );
338 ConfigManager::inst()->saveConfigFile();
339 }
340 }
341
342
343
344
value(const QString & cls,const QString & attribute) const345 const QString & ConfigManager::value( const QString & cls,
346 const QString & attribute ) const
347 {
348 if( m_settings.contains( cls ) )
349 {
350 for( stringPairVector::const_iterator it =
351 m_settings[cls].begin();
352 it != m_settings[cls].end(); ++it )
353 {
354 if( ( *it ).first == attribute )
355 {
356 return ( *it ).second ;
357 }
358 }
359 }
360 static QString empty;
361 return empty;
362 }
363
364
365
value(const QString & cls,const QString & attribute,const QString & defaultVal) const366 const QString & ConfigManager::value( const QString & cls,
367 const QString & attribute,
368 const QString & defaultVal ) const
369 {
370 const QString & val = value( cls, attribute );
371 return val.isEmpty() ? defaultVal : val;
372 }
373
374
375
376
setValue(const QString & cls,const QString & attribute,const QString & value)377 void ConfigManager::setValue( const QString & cls,
378 const QString & attribute,
379 const QString & value )
380 {
381 if( m_settings.contains( cls ) )
382 {
383 for( QPair<QString, QString>& pair : m_settings[cls])
384 {
385 if( pair.first == attribute )
386 {
387 if ( pair.second != value )
388 {
389 pair.second = value;
390 emit valueChanged( cls, attribute, value );
391 }
392 return;
393 }
394 }
395 }
396 // not in map yet, so we have to add it...
397 m_settings[cls].push_back( qMakePair( attribute, value ) );
398 }
399
400
deleteValue(const QString & cls,const QString & attribute)401 void ConfigManager::deleteValue( const QString & cls, const QString & attribute)
402 {
403 if( m_settings.contains( cls ) )
404 {
405 for( stringPairVector::iterator it = m_settings[cls].begin();
406 it != m_settings[cls].end(); ++it )
407 {
408 if( ( *it ).first == attribute )
409 {
410 m_settings[cls].erase(it);
411 return;
412 }
413 }
414 }
415 }
416
417
loadConfigFile(const QString & configFile)418 void ConfigManager::loadConfigFile( const QString & configFile )
419 {
420 // read the XML file and create DOM tree
421 // Allow configuration file override through --config commandline option
422 if ( !configFile.isEmpty() )
423 {
424 m_lmmsRcFile = configFile;
425 }
426
427 QFile cfg_file( m_lmmsRcFile );
428 QDomDocument dom_tree;
429
430 if( cfg_file.open( QIODevice::ReadOnly ) )
431 {
432 QString errorString;
433 int errorLine, errorCol;
434 if( dom_tree.setContent( &cfg_file, false, &errorString, &errorLine, &errorCol ) )
435 {
436 // get the head information from the DOM
437 QDomElement root = dom_tree.documentElement();
438
439 QDomNode node = root.firstChild();
440
441 // Cache the config version for upgrade()
442 if ( !root.attribute( "version" ).isNull() ) {
443 m_version = root.attribute( "version" );
444 }
445
446 // create the settings-map out of the DOM
447 while( !node.isNull() )
448 {
449 if( node.isElement() &&
450 node.toElement().hasAttributes () )
451 {
452 stringPairVector attr;
453 QDomNamedNodeMap node_attr =
454 node.toElement().attributes();
455 for( int i = 0; i < node_attr.count();
456 ++i )
457 {
458 QDomNode n = node_attr.item( i );
459 if( n.isAttr() )
460 {
461 attr.push_back( qMakePair( n.toAttr().name(),
462 n.toAttr().value() ) );
463 }
464 }
465 m_settings[node.nodeName()] = attr;
466 }
467 else if( node.nodeName() == "recentfiles" )
468 {
469 m_recentlyOpenedProjects.clear();
470 QDomNode n = node.firstChild();
471 while( !n.isNull() )
472 {
473 if( n.isElement() && n.toElement().hasAttributes() )
474 {
475 m_recentlyOpenedProjects <<
476 n.toElement().attribute( "path" );
477 }
478 n = n.nextSibling();
479 }
480 }
481 node = node.nextSibling();
482 }
483
484 if( value( "paths", "artwork" ) != "" )
485 {
486 m_artworkDir = value( "paths", "artwork" );
487 #ifdef LMMS_BUILD_WIN32
488 // Detect a QDir/QFile hang on Windows
489 // see issue #3417 on github
490 bool badPath = ( m_artworkDir == "/" || m_artworkDir == "\\" );
491 #else
492 bool badPath = false;
493 #endif
494
495 if( badPath || !QDir( m_artworkDir ).exists() ||
496 !QFile( m_artworkDir + "/style.css" ).exists() )
497 {
498 m_artworkDir = defaultArtworkDir();
499 }
500 m_artworkDir = ensureTrailingSlash(m_artworkDir);
501 }
502 setWorkingDir( value( "paths", "workingdir" ) );
503
504 setGIGDir( value( "paths", "gigdir" ) == "" ? gigDir() : value( "paths", "gigdir" ) );
505 setSF2Dir( value( "paths", "sf2dir" ) == "" ? sf2Dir() : value( "paths", "sf2dir" ) );
506 setVSTDir( value( "paths", "vstdir" ) );
507 setLADSPADir( value( "paths", "laddir" ) );
508 #ifdef LMMS_HAVE_STK
509 setSTKDir( value( "paths", "stkdir" ) );
510 #endif
511 #ifdef LMMS_HAVE_FLUIDSYNTH
512 setDefaultSoundfont( value( "paths", "defaultsf2" ) );
513 #endif
514 setBackgroundArtwork( value( "paths", "backgroundartwork" ) );
515 }
516 else if( gui )
517 {
518 QMessageBox::warning( NULL, MainWindow::tr( "Configuration file" ),
519 MainWindow::tr( "Error while parsing configuration file at line %1:%2: %3" ).
520 arg( errorLine ).
521 arg( errorCol ).
522 arg( errorString ) );
523 }
524 cfg_file.close();
525 }
526
527 // Plugins are searched recursively, blacklist problematic locations
528 if( m_vstDir.isEmpty() || m_vstDir == QDir::separator() || m_vstDir == "/" ||
529 m_vstDir == ensureTrailingSlash( QDir::homePath() ) ||
530 !QDir( m_vstDir ).exists() )
531 {
532 #ifdef LMMS_BUILD_WIN32
533 QString programFiles = QString::fromLocal8Bit( getenv( "ProgramFiles" ) );
534 m_vstDir = programFiles + "/VstPlugins/";
535 #else
536 m_vstDir = m_workingDir + "plugins/vst/";
537 #endif
538 }
539
540 if( m_ladDir.isEmpty() )
541 {
542 m_ladDir = userLadspaDir();
543 }
544
545 #ifdef LMMS_HAVE_STK
546 if( m_stkDir.isEmpty() || m_stkDir == QDir::separator() || m_stkDir == "/" ||
547 !QDir( m_stkDir ).exists() )
548 {
549 #if defined(LMMS_BUILD_WIN32)
550 m_stkDir = m_dataDir + "stk/rawwaves/";
551 #elif defined(LMMS_BUILD_APPLE)
552 m_stkDir = qApp->applicationDirPath() + "/../share/stk/rawwaves/";
553 #else
554 if ( qApp->applicationDirPath().startsWith("/tmp/") )
555 {
556 // Assume AppImage bundle
557 m_stkDir = qApp->applicationDirPath() + "/../share/stk/rawwaves/";
558 }
559 else
560 {
561 // Fallback to system provided location
562 m_stkDir = "/usr/local/share/stk/rawwaves/";
563 }
564 #endif
565 }
566 #endif
567
568 upgrade();
569
570 QStringList searchPaths;
571 if(! qgetenv("LMMS_THEME_PATH").isNull())
572 searchPaths << qgetenv("LMMS_THEME_PATH");
573 searchPaths << artworkDir() << defaultArtworkDir();
574 QDir::setSearchPaths( "resources", searchPaths);
575
576 // Create any missing subdirectories in the working dir, but only if the working dir exists
577 if( hasWorkingDir() )
578 {
579 createWorkingDir();
580 }
581 }
582
583
584
585
saveConfigFile()586 void ConfigManager::saveConfigFile()
587 {
588 setValue( "paths", "artwork", m_artworkDir );
589 setValue( "paths", "workingdir", m_workingDir );
590 setValue( "paths", "vstdir", m_vstDir );
591 setValue( "paths", "gigdir", m_gigDir );
592 setValue( "paths", "sf2dir", m_sf2Dir );
593 setValue( "paths", "laddir", m_ladDir );
594 #ifdef LMMS_HAVE_STK
595 setValue( "paths", "stkdir", m_stkDir );
596 #endif
597 #ifdef LMMS_HAVE_FLUIDSYNTH
598 setValue( "paths", "defaultsf2", m_defaultSoundfont );
599 #endif
600 setValue( "paths", "backgroundartwork", m_backgroundArtwork );
601
602 QDomDocument doc( "lmms-config-file" );
603
604 QDomElement lmms_config = doc.createElement( "lmms" );
605 lmms_config.setAttribute( "version", m_version );
606 doc.appendChild( lmms_config );
607
608 for( settingsMap::iterator it = m_settings.begin();
609 it != m_settings.end(); ++it )
610 {
611 QDomElement n = doc.createElement( it.key() );
612 for( stringPairVector::iterator it2 = ( *it ).begin();
613 it2 != ( *it ).end(); ++it2 )
614 {
615 n.setAttribute( ( *it2 ).first, ( *it2 ).second );
616 }
617 lmms_config.appendChild( n );
618 }
619
620 QDomElement recent_files = doc.createElement( "recentfiles" );
621
622 for( QStringList::iterator it = m_recentlyOpenedProjects.begin();
623 it != m_recentlyOpenedProjects.end(); ++it )
624 {
625 QDomElement n = doc.createElement( "file" );
626 n.setAttribute( "path", *it );
627 recent_files.appendChild( n );
628 }
629 lmms_config.appendChild( recent_files );
630
631 QString xml = "<?xml version=\"1.0\"?>\n" + doc.toString( 2 );
632
633 QFile outfile( m_lmmsRcFile );
634 if( !outfile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
635 {
636 QString title, message;
637 title = MainWindow::tr( "Could not open file" );
638 message = MainWindow::tr( "Could not open file %1 "
639 "for writing.\nPlease make "
640 "sure you have write "
641 "permission to the file and "
642 "the directory containing the "
643 "file and try again!"
644 ).arg( m_lmmsRcFile );
645 if( gui )
646 {
647 QMessageBox::critical( NULL, title, message,
648 QMessageBox::Ok,
649 QMessageBox::NoButton );
650 }
651 return;
652 }
653
654 outfile.write( xml.toUtf8() );
655 outfile.close();
656 }
657