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