1 /*
2  * KMix -- KDE's full featured mini mixer
3  *
4  * Copyright 2006-2007 Christian Esken <esken@kde.org>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Library General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Library General Public License for more details.
15  *
16  * You should have received a copy of the GNU Library General Public
17  * License along with this program; if not, write to the Free
18  * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 
21 #include "gui/guiprofile.h"
22 
23 // Qt
24 #include <QDir>
25 #include <QSaveFile>
26 #include <QXmlStreamWriter>
27 #include <QXmlStreamReader>
28 #include <QStandardPaths>
29 // KMix
30 #include "core/mixer.h"
31 
32 
33 #undef DEBUG_XMLREADER
34 
35 
36 static QMap<QString, GUIProfile *> s_profiles;
37 static const QString s_profileDir("profiles");
38 
39 
visibilityToString(GuiVisibility vis)40 static QString visibilityToString(GuiVisibility vis)
41 {
42 	switch (vis)
43 	{
44 case GuiVisibility::Simple:	return ("simple");
45 case GuiVisibility::Extended:	return ("extended");
46 // For backwards compatibility, 'Full' has the ID "all" and not "full"
47 case GuiVisibility::Full:	return ("all");
48 case GuiVisibility::Custom:	return ("custom");
49 case GuiVisibility::Never:	return ("never");
50 case GuiVisibility::Default:	return ("default");
51 default:			return ("unknown");
52 	}
53 }
54 
55 
visibilityFromString(const QString & str)56 static GuiVisibility visibilityFromString(const QString &str)
57 {
58 	if (str=="simple") return (GuiVisibility::Simple);
59 	else if (str=="extended") return (GuiVisibility::Extended);
60 	else if (str=="all") return (GuiVisibility::Full);
61 	else if (str=="custom") return (GuiVisibility::Custom);
62 	else if (str=="never") return (GuiVisibility::Never);
63 
64 	qCWarning(KMIX_LOG) << "Unknown string value" << str;
65 	return (GuiVisibility::Full);
66 }
67 
68 
69 /**
70  * Product comparator for sorting:
71  * We want the comparator to sort ascending by Vendor. "Inside" the Vendors, we sort by Product Name.
72  */
operator ()(const ProfProduct * p1,const ProfProduct * p2) const73 bool ProductComparator::operator()(const ProfProduct* p1, const ProfProduct* p2) const {
74 	if ( p1->vendor < p2->vendor ) {
75 		return ( true );
76 	}
77 	else if ( p1->vendor > p2->vendor ) {
78 		return ( false );
79 	}
80 	else if ( p1->productName < p2->productName ) {
81 		return ( true );
82 	}
83 	else if ( p1->productName > p2->productName ) {
84 		return ( false );
85 	}
86 	else {
87 		/**
88 		 * We reach this point, if vendor and product name is identical.
89 		 * Actually we don't care about the order then, so we decide that "p1" comes first.
90 		 *
91 		 * (Hint: As this is a set comparator, the return value HERE doesn't matter that
92 		 * much. But if we would decide later to change this Comparator to be a Map Comparator,
93 		 *  we must NOT return a "0" for identity - this would lead to non-insertion on insert())
94 		 */
95 		return true;
96 	}
97 }
98 
GUIProfile()99 GUIProfile::GUIProfile()
100 {
101     _dirty = false;
102     _driverVersionMin = 0;
103     _driverVersionMax = 0;
104     _generation = 1;
105 }
106 
~GUIProfile()107 GUIProfile::~GUIProfile()
108 {
109     qCWarning(KMIX_LOG) << "Thou shalt not delete any GUI profile. This message is only OK, when quitting KMix";
110     qDeleteAll(_controls);
111     qDeleteAll(_products);
112 }
113 
114 /**
115  * Clears the GUIProfile cache. You must only call this
116  * before termination of the application, as GUIProfile instances are used in other classes, especially the views.
117  * There is no need to call this in non-GUI applications like kmixd and kmixctrl.
118  */
clearCache()119 void GUIProfile::clearCache()
120 {
121 	qDeleteAll(s_profiles);
122 	s_profiles.clear();
123 }
124 
125 
126 /**
127  * Build a profile name. Suitable to use as primary key and to build filenames.
128  * @arg mixer         The mixer
129  * @arg profileName   The profile name (e.g. "capture", "playback", "my-cool-profile", or "any"
130  * @return            The profile name
131  */
buildProfileName(const Mixer * mixer,const QString & profileName,bool ignoreCard)132 static QString buildProfileName(const Mixer *mixer, const QString &profileName, bool ignoreCard)
133 {
134     QString fname;
135     fname += mixer->getDriverName();
136     if (!ignoreCard) {
137         fname += ".%1.%2";
138         fname = fname.arg(mixer->getBaseName()).arg(mixer->getCardInstance());
139     }
140     fname += '.' + profileName;
141 
142     fname.replace(' ','_');
143     return fname;
144 }
145 
146 
147 /**
148  * Generate a readable profile name (for presenting to the user).
149  * Hint: Currently used as Tab label.
150  */
buildReadableProfileName(const Mixer * mixer,const QString & profileName)151 static QString buildReadableProfileName(const Mixer *mixer, const QString &profileName)
152 {
153     QString fname;
154     fname += mixer->getBaseName();
155     if ( mixer->getCardInstance() > 1 ) {
156         fname += " %1";
157         fname = fname.arg(mixer->getCardInstance());
158     }
159     if ( profileName != "default" ) {
160         fname += ' ' + profileName;
161     }
162 
163     qCDebug(KMIX_LOG) << fname;
164     return fname;
165 }
166 
167 /**
168  * Returns the GUIProfile for the given ID (= "fullyQualifiedName").
169  * If not found 0 is returned. There is no try to load it.
170  *
171  * @returns The loaded GUIProfile for the given ID
172  */
find(const QString & id)173 GUIProfile *GUIProfile::find(const QString &id)
174 {
175 	// Return found value or default-constructed one (nullptr).
176 	// Does not insert into map.  Now thread-safe.
177 	return (s_profiles.value(id));
178 }
179 
180 
createNormalizedFilename(const QString & profileId)181 static QString createNormalizedFilename(const QString &profileId)
182 {
183 	QString profileIdNormalized(profileId);
184 	profileIdNormalized.replace(':', '.');
185 
186 	return profileIdNormalized + ".xml";
187 }
188 
189 
190 /**
191  * Loads a GUI Profile from disk (XML profile file).
192  * It tries to load the Soundcard specific file first (a).
193  * If it doesn't exist, it will load the default profile corresponding to the soundcard driver (b).
194  */
loadProfileFromXMLfiles(const Mixer * mixer,const QString & profileName)195 static GUIProfile *loadProfileFromXMLfiles(const Mixer *mixer, const QString &profileName)
196 {
197     GUIProfile* guiprof = nullptr;
198     QString fileName = s_profileDir + '/' + createNormalizedFilename(profileName);
199     QString fileNameFQ = QStandardPaths::locate(QStandardPaths::AppDataLocation, fileName );
200 
201     if ( ! fileNameFQ.isEmpty() ) {
202         guiprof = new GUIProfile();
203         if ( guiprof->readProfile(fileNameFQ) && ( guiprof->match(mixer) > 0) ) {
204             // loaded
205         }
206         else {
207             delete guiprof; // not good (e.g. Parsing error => drop this profile silently)
208             guiprof = nullptr;
209         }
210     }
211     else {
212         qCDebug(KMIX_LOG) << "Ignore file " <<fileName<< " (does not exist)";
213     }
214     return guiprof;
215 }
216 
217 
218 /*
219  * Add the profile to the internal list of profiles (Profile caching).
220  */
addProfile(GUIProfile * guiprof)221 static void addProfile(GUIProfile *guiprof)
222 {
223 	// Possible TODO: Delete old mapped GUIProfile, if it exists. Otherwise we might leak one GUIProfile instance
224 	//                per unplug/plug sequence. Its quite likely possible that currently no Backend leads to a
225 	//                leak: This is because they either don't hotplug cards (PulseAudio, MPRIS2), or they ship
226 	//                a XML gui profile (so the Cached version is retrieved, and addProfile() is not called).
227 
228     s_profiles[guiprof->getId()] = guiprof;
229     qCDebug(KMIX_LOG) << "I have added" << guiprof->getId() << "; Number of profiles is now " <<  s_profiles.size() ;
230 }
231 
232 
233 /**
234  * Finds the correct profile for the given mixer.
235  * If already loaded from disk, returns the cached version.
236  * Otherwise load profile from disk: Priority: Card specific profile, Card unspecific profile
237  *
238  * @arg mixer         The mixer
239  * @arg profileName   The profile name (e.g. "ALSA.X-Fi.default", or "OSS.intel-cha51.playback")
240  *                    A special case is "", which means that a card specific name should be generated.
241  * @arg profileNameIsFullyQualified If true, an exact match will be searched. Otherwise it is a simple name like "playback" or "capture"
242  * @arg ignoreCardName If profileName not fully qualified, this is used in building the requestedProfileName
243  * @return GUIProfile*  The loaded GUIProfile, or 0 if no profile matched. Hint: if you use allowFallback==true, this should never return 0.
244  */
find(const Mixer * mixer,const QString & profileName,bool profileNameIsFullyQualified,bool ignoreCardName)245 GUIProfile *GUIProfile::find(const Mixer *mixer, const QString &profileName, bool profileNameIsFullyQualified, bool ignoreCardName)
246 {
247     GUIProfile *guiprof = nullptr;
248 
249     if (mixer==nullptr || profileName.isEmpty()) return (nullptr);
250 
251 //    if ( mixer->isDynamic() ) {
252 //        qCDebug(KMIX_LOG) << "GUIProfile::find() Not loading GUIProfile for Dynamic Mixer (e.g. PulseAudio)";
253 //        return 0;
254 //    }
255 
256     QString requestedProfileName;
257     QString fullQualifiedProfileName;
258     if ( profileNameIsFullyQualified ) {
259         requestedProfileName     = profileName;
260         fullQualifiedProfileName = profileName;
261     }
262     else {
263         requestedProfileName     = buildProfileName(mixer, profileName, ignoreCardName);
264         fullQualifiedProfileName = buildProfileName(mixer, profileName, false);
265     }
266 
267     if ( s_profiles.contains(fullQualifiedProfileName) ) {
268         guiprof = s_profiles.value(fullQualifiedProfileName);  // Cached
269     }
270     else {
271         guiprof = loadProfileFromXMLfiles(mixer, requestedProfileName);  // Load from XML ###Card specific profile###
272         if ( guiprof!=nullptr) {
273             guiprof->_mixerId = mixer->id();
274             guiprof->setId(fullQualifiedProfileName); // this one contains some soundcard id (basename + instance)
275 
276             if ( guiprof->getName().isEmpty() ) {
277                 // If the profile didn't contain a name then lets define one
278                 guiprof->setName(buildReadableProfileName(mixer,profileName)); // The caller can rename this if he likes
279                 guiprof->setDirty();
280             }
281 
282             if ( requestedProfileName != fullQualifiedProfileName) {
283                 // This is very important!
284                 // When the final profileName (fullQualifiedProfileName) is different from
285                 // what we have loaded (requestedProfileName, e.g. "default"), we MUST
286                 // set the profile dirty, so it gets saved. Otherwise we would write the
287                 // fullQualifiedProfileName in the kmixrc, and will not find it on the next
288                 // start of KMix.
289                 guiprof->setDirty();
290             }
291             addProfile(guiprof);
292         }
293     }
294 
295     return (guiprof);
296 }
297 
298 
299 /**
300  * Returns a fallback GUIProfile. You can call this if the backends ships no profile files.
301  * The returned GUIProfile is also added to the static Map of all GUIProfile instances.
302  */
fallbackProfile(const Mixer * mixer)303 GUIProfile* GUIProfile::fallbackProfile(const Mixer *mixer)
304 {
305 	// -1- Get name
306     QString fullQualifiedProfileName = buildProfileName(mixer, QString("default"), false);
307 
308     GUIProfile *fallback = new GUIProfile();
309 
310     // -2- Fill details
311     ProfProduct* prd = new ProfProduct();
312     prd->vendor         = mixer->getDriverName();
313     prd->productName    = mixer->readableName();
314     prd->productRelease = "1.0";
315     fallback->_products.insert(prd);
316 
317     static QString matchAll(".*");
318     static QString matchAllSctl(".*");
319     ProfControl* ctl = new ProfControl(matchAll, matchAllSctl);
320     //ctl->regexp      = matchAll;   // make sure id matches the regexp
321     ctl->setMandatory(true);
322     fallback->_controls.push_back(ctl);
323 
324     fallback->_soundcardDriver = mixer->getDriverName();
325     fallback->_soundcardName   = mixer->readableName();
326 
327     fallback->_mixerId = mixer->id();
328     fallback->setId(fullQualifiedProfileName); // this one contains some soundcard id (basename + instance)
329     fallback->setName(buildReadableProfileName(mixer, QString("default"))); // The caller can rename this if he likes
330     fallback->setDirty();
331 
332     /* -3- Add the profile to the static list
333      *     Hint: This looks like a memory leak, as we never remove profiles from memory while KMix runs.
334      *           Especially with application streams it looks suspicious. But please be aware that this method is only
335      *           called for soundcard hotplugs, and not on stream hotplugs. At least it is supposed to be like that.
336      *
337      *           Please also see the docs at addProfile(), they also address the possible memory leakage.
338      */
339     addProfile(fallback);
340 
341     return fallback;
342 }
343 
344 
345 /**
346  * Fill the profile with the data from the given XML profile file.
347  * @par  fileName: Full qualified filename (with path).
348  * @return bool True, if the profile was successfully created. False if not (e.g. parsing error).
349  */
readProfile(const QString & fileName)350 bool GUIProfile::readProfile(const QString &fileName)
351 {
352     qCDebug(KMIX_LOG) << "reading" << fileName;
353 
354     QFile xmlFile(fileName);
355     bool ok = xmlFile.open(QIODevice::ReadOnly);
356     if (ok)
357     {
358         GUIProfileParser gpp(this);
359 
360         QXmlStreamReader reader(&xmlFile);
361         while (!reader.atEnd())
362         {
363             bool startOk = reader.readNextStartElement();
364             if (!startOk)
365             {
366 #ifdef DEBUG_XMLREADER
367                 qCDebug(KMIX_LOG) << "  no more start elements";
368 #endif
369                 break;
370             }
371 
372             const QString &name = reader.name().toString().toLower();
373             const QXmlStreamAttributes attrs = reader.attributes();
374 #ifdef DEBUG_XMLREADER
375             qCDebug(KMIX_LOG) << "  element" << name << "has" << attrs.count() << "attributes:";
376             for (const QXmlStreamAttribute &attr : qAsConst(attrs))
377             {
378                 qCDebug(KMIX_LOG) << "    " << attr.name() << "=" << attr.value();
379             }
380 #endif
381             if (name=="soundcard")
382             {
383                 gpp.addSoundcard(attrs);
384                 continue;				// then read contained elements
385             }
386             else if (name=="control") gpp.addControl(attrs);
387             else if (name=="product") gpp.addProduct(attrs);
388             else if (name=="profile") gpp.addProfileInfo(attrs);
389             else qCDebug(KMIX_LOG) << "Unknown XML tag" << name << "at line" << reader.lineNumber();
390 
391             reader.skipCurrentElement();
392         }
393 
394         if (reader.hasError())
395         {
396             qCWarning(KMIX_LOG) << "XML parse error at line" << reader.lineNumber() << "-" << reader.errorString();
397             ok = false;
398         }
399     }
400 
401     return (ok);
402 }
403 
404 
writeProfile()405 bool GUIProfile::writeProfile()
406 {
407    QString profileId = getId();
408    QDir profileDir = QDir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + '/' + s_profileDir);
409    QString fileName = createNormalizedFilename(profileId);
410    QString fileNameFQ = profileDir.filePath(fileName);
411 
412    if (!profileDir.exists())
413        profileDir.mkpath(".");
414 
415    qCDebug(KMIX_LOG) << "Write profile" << fileNameFQ;
416    QSaveFile f(fileNameFQ);
417    if (!f.open(QIODevice::WriteOnly|QFile::Truncate))
418    {
419       qCWarning(KMIX_LOG) << "Cannot save profile to" << fileNameFQ;
420       return (false);
421    }
422 
423    QXmlStreamWriter writer(&f);
424    writer.setAutoFormatting(true);
425 
426    // <?xml version="1.0" encoding="utf-8"?>
427    writer.writeStartDocument();
428 
429    //  <soundcard
430    writer.writeStartElement("soundcard");
431    //    driver=	_soundcardDriver
432    writer.writeAttribute("driver", _soundcardDriver);
433    //    version=	(_driverVersionMin << ":" << _driverVersionMax)
434    writer.writeAttribute("version", QString("%1:%2").arg(_driverVersionMin).arg(_driverVersionMax));
435    //    name=		guiprof._soundcardName
436    writer.writeAttribute("name", _soundcardName);
437    //    type=		guiprof._soundcardType
438    writer.writeAttribute("type", _soundcardType);
439    //    generation=	guiprof._generation
440    writer.writeAttribute("generation", QString::number(_generation));
441 
442    //  <profile
443    writer.writeStartElement("profile");
444    //    id=		guiprof._id
445    writer.writeAttribute("id", _id);
446    //    name=		guiprof._name
447    writer.writeAttribute("name", _name);
448    //  />
449    writer.writeEndElement();
450 
451    for (const ProfProduct *prd : qAsConst(_products))
452    {
453       //  <product
454       writer.writeStartElement("product");
455       //    vendor=		prd->vendor
456       writer.writeAttribute("vendor", prd->vendor);
457       //    name=		prd->productName
458       writer.writeAttribute("name", prd->productName);
459       //    release=		prd->productRelease
460       if (!prd->productRelease.isEmpty()) writer.writeAttribute("release", prd->productRelease);
461       //	 comment=	prd->comment
462       if (!prd->comment.isEmpty()) writer.writeAttribute("comment", prd->comment);
463       //  />
464       writer.writeEndElement();
465    }							// for all products
466 
467    for (const ProfControl *profControl : qAsConst(getControls()))
468    {
469       //  <control
470       writer.writeStartElement("control");
471       //    id=			profControl->id()
472       writer.writeAttribute("id", profControl->id());
473       //    name=		profControl->name()
474       const QString name = profControl->name();
475       if (!name.isEmpty() && name!=profControl->id()) writer.writeAttribute("name", name);
476       //    subcontrols=	profControl->renderSubcontrols()
477       writer.writeAttribute("subcontrols", profControl->renderSubcontrols());
478       //    show=		visibilityToString(profControl->getVisibility())
479       writer.writeAttribute("show", visibilityToString(profControl->getVisibility()));
480       //    mandatory=		"true"
481       if (profControl->isMandatory()) writer.writeAttribute("mandatory", "true");
482       //    split=		"true"
483       if (profControl->isSplit())  writer.writeAttribute("split", "true");
484       //  />
485       writer.writeEndElement();
486    }							// for all controls
487 
488    //  </soundcard>
489    writer.writeEndElement();
490 
491    writer.writeEndDocument();
492    if (writer.hasError())
493    {
494 	   qCWarning(KMIX_LOG) << "XML writing failed to" << fileNameFQ;
495 	   f.cancelWriting();
496 	   return (false);
497    }
498 
499    f.commit();
500    _dirty = false;
501    return (true);
502 }
503 
504 
505 
506 // -------------------------------------------------------------------------------------
setControls(ControlSet & newControlSet)507 void GUIProfile::setControls(ControlSet& newControlSet)
508 {
509     qDeleteAll(_controls);
510     _controls = newControlSet;
511 }
512 
513 // -------------------------------------------------------------------------------------
514 
515 
516 /**
517  * Returns how good the given Mixer matches this GUIProfile.
518  * A value between 0 (not matching at all) and MAXLONG (perfect match) is returned.
519  *
520  * Here is the current algorithm:
521  *
522  * If the driver doesn't match, 0 is returned. (OK)
523  * If the card-name ...  (OK)
524  *     is "*", this is worth 1 point
525  *     doesn't match, 0 is returned.
526  *     matches, this is worth 500 points.
527  *
528  * If the "card type" ...
529  *     is empty, this is worth 0 points.     !!! not implemented yet
530  *     doesn't match, 0 is returned.         !!! not implemented yet
531  *     matches , this is worth 500 points.  !!! not implemented yet
532  *
533  * If the "driver version" doesn't match, 0 is returned. !!! not implemented yet
534  * If the "driver version" matches, this is worth ...
535  *     4000 unlimited                             <=> "*:*"
536  *     6000 toLower-bound-limited                   <=> "toLower-bound:*"
537  *     6000 upper-bound-limited                   <=> "*:upper-bound"
538  *     8000 upper- and toLower-bound limited        <=> "toLower-bound:upper-bound"
539  * or 10000 points (upper-bound=toLower-bound=bound <=> "bound:bound"
540  *
541  * The Profile-Generation is added to the already achieved points. (done)
542  *   The maximum gain is 900 points.
543  *   Thus you can create up to 900 generations (0-899) without "overriding"
544  *   the points gained from the "driver version" or "card-type".
545  *
546  * For example:  card-name="*" (1), card-type matches (1000),
547  *               driver version "*:*" (4000), Profile-Generation 4 (4).
548  *         Sum:  1 + 1000 + 4000 + 4 = 5004
549  *
550  * @todo Implement "card type" match value
551  * @todo Implement "version" match value (must be in backends as well)
552  */
match(const Mixer * mixer) const553 unsigned long GUIProfile::match(const Mixer *mixer) const
554 {
555 	unsigned long matchValue = 0;
556 	if ( _soundcardDriver != mixer->getDriverName() ) {
557 		return 0;
558 	}
559 	if ( _soundcardName == "*" ) {
560 		matchValue += 1;
561 	}
562 	else if ( _soundcardName != mixer->getBaseName() ) {
563 		return 0; // card name does not match
564 	}
565 	else {
566 		matchValue += 500; // card name matches
567 	}
568 
569 	// !!! we don't check current for the driver version.
570 	//     So we assign simply 4000 points for now.
571 	matchValue += 4000;
572 	if ( _generation < 900 ) {
573 		matchValue += _generation;
574 	}
575 	else {
576 		matchValue += 900;
577 	}
578 	return matchValue;
579 }
580 
581 
ProfControl(const QString & id,const QString & subcontrols)582 ProfControl::ProfControl(const QString &id, const QString &subcontrols)
583 	: _id(id),
584 	  _visibility(GuiVisibility::Simple),
585 	  _mandatory(false),
586 	  _split(false)
587 {
588     setSubcontrols(subcontrols);
589 }
590 
ProfControl(const ProfControl & profControl)591 ProfControl::ProfControl(const ProfControl &profControl)
592 	: _mandatory(false),
593 	  _split(false)
594 {
595     _id = profControl._id;
596     _name = profControl._name;
597     _visibility = profControl._visibility;
598 
599     _useSubcontrolPlayback = profControl._useSubcontrolPlayback;
600     _useSubcontrolCapture = profControl._useSubcontrolCapture;
601     _useSubcontrolPlaybackSwitch = profControl._useSubcontrolPlaybackSwitch;
602     _useSubcontrolCaptureSwitch = profControl._useSubcontrolCaptureSwitch;
603     _useSubcontrolEnum = profControl._useSubcontrolEnum;
604     _subcontrols = profControl._subcontrols;
605 
606     _backgroundColor = profControl._backgroundColor;
607     _switchtype = profControl._switchtype;
608     _mandatory = profControl._mandatory;
609     _split = profControl._split;
610 }
611 
612 
satisfiesVisibility(GuiVisibility vis) const613 bool ProfControl::satisfiesVisibility(GuiVisibility vis) const
614 {
615 	GuiVisibility me = getVisibility();
616 	if (me==GuiVisibility::Never || vis==GuiVisibility::Never) return (false);
617 	if (me==GuiVisibility::Custom || vis==GuiVisibility::Custom) return (false);
618 	if (vis==GuiVisibility::Default) return (true);
619 	return (static_cast<int>(me)<=static_cast<int>(vis));
620 }
621 
622 
623 /**
624  * An overridden method that either sets
625  * GuiVisibility::Simple or GuiVisibility::Never.
626  */
setVisible(bool visible)627 void ProfControl::setVisible(bool visible)
628 {
629 	setVisibility(visible ? GuiVisibility::Simple : GuiVisibility::Never);
630 }
631 
setVisibility(GuiVisibility vis)632 void ProfControl::setVisibility(GuiVisibility vis)
633 {
634 	_visibility = vis;
635 }
636 
setVisibility(const QString & visString)637 void ProfControl::setVisibility(const QString &visString)
638 {
639 	setVisibility(visibilityFromString(visString));
640 }
641 
setSubcontrols(const QString & sctls)642 void ProfControl::setSubcontrols(const QString &sctls)
643 {
644     _subcontrols = sctls;
645 
646   _useSubcontrolPlayback = false;
647   _useSubcontrolCapture = false;
648   _useSubcontrolPlaybackSwitch = false;
649   _useSubcontrolCaptureSwitch = false;
650   _useSubcontrolEnum = false;
651 
652 #if QT_VERSION>=QT_VERSION_CHECK(5, 14, 0)
653   QStringList qsl = sctls.split( ',',  Qt::SkipEmptyParts, Qt::CaseInsensitive);
654 #else
655   QStringList qsl = sctls.split( ',',  QString::SkipEmptyParts, Qt::CaseInsensitive);
656 #endif
657 
658   QStringListIterator qslIt(qsl);
659   while (qslIt.hasNext()) {
660     QString sctl = qslIt.next();
661        //qCDebug(KMIX_LOG) << "setSubcontrols found: " << sctl.toLocal8Bit().constData();
662        if ( sctl == "pvolume" ) _useSubcontrolPlayback = true;
663        else if ( sctl == "cvolume" ) _useSubcontrolCapture = true;
664        else if ( sctl == "pswitch" ) _useSubcontrolPlaybackSwitch = true;
665        else if ( sctl == "cswitch" ) _useSubcontrolCaptureSwitch = true;
666        else if ( sctl == "enum" ) _useSubcontrolEnum = true;
667        else if ( sctl == "*" || sctl == ".*") {
668 	 _useSubcontrolCapture = true;
669 	 _useSubcontrolCaptureSwitch = true;
670 	 _useSubcontrolPlayback = true;
671 	 _useSubcontrolPlaybackSwitch = true;
672 	 _useSubcontrolEnum = true;
673        }
674        else qCWarning(KMIX_LOG) << "Ignoring unknown subcontrol type '" << sctl << "' in profile";
675   }
676 }
677 
renderSubcontrols() const678 QString ProfControl::renderSubcontrols() const
679 {
680     QString sctlString;
681     if ( _useSubcontrolPlayback && _useSubcontrolPlaybackSwitch && _useSubcontrolCapture && _useSubcontrolCaptureSwitch && _useSubcontrolEnum ) {
682         return QString("*");
683     }
684     else {
685         if ( _useSubcontrolPlayback ) {
686             sctlString += "pvolume,";
687         }
688         if ( _useSubcontrolCapture ) {
689             sctlString += "cvolume,";
690         }
691         if ( _useSubcontrolPlaybackSwitch ) {
692             sctlString += "pswitch,";
693         }
694         if ( _useSubcontrolCaptureSwitch ) {
695             sctlString += "cswitch,";
696         }
697         if ( _useSubcontrolEnum ) {
698             sctlString += "enum,";
699         }
700         if ( sctlString.length() > 0 ) {
701             sctlString.chop(1);
702         }
703         return sctlString;
704     }
705 }
706 
707 
708 // ### PARSER START ################################################
709 
GUIProfileParser(GUIProfile * ref_gp)710 GUIProfileParser::GUIProfileParser(GUIProfile *ref_gp)
711 {
712     _guiProfile = ref_gp;
713 }
714 
715 
addSoundcard(const QXmlStreamAttributes & attributes)716 void GUIProfileParser::addSoundcard(const QXmlStreamAttributes &attributes)
717 {
718     const QString driver     = attributes.value("driver").toString();
719     const QString version    = attributes.value("version").toString();
720     const QString name	     = attributes.value("name").toString();
721     const QString type	     = attributes.value("type").toString();
722     const QString generation = attributes.value("generation").toString();
723 
724     // Adding a card makes only sense if we have at least
725     // the driver and product name.
726     if (driver.isEmpty() || name.isEmpty() ) return;
727 
728     _guiProfile->_soundcardDriver = driver;
729     _guiProfile->_soundcardName = name;
730     _guiProfile->_soundcardType = type;
731 
732     if (version.isEmpty())
733     {
734         _guiProfile->_driverVersionMin = 0;
735         _guiProfile->_driverVersionMax = 0;
736     }
737     else
738     {
739         const QStringList versionMinMax = version.split(':', Qt::KeepEmptyParts);
740         _guiProfile->_driverVersionMin = versionMinMax.value(0).toULong();
741         _guiProfile->_driverVersionMax = versionMinMax.value(1).toULong();
742     }
743 
744     _guiProfile->_generation = generation.toUInt();
745 }
746 
747 
addProfileInfo(const QXmlStreamAttributes & attributes)748 void GUIProfileParser::addProfileInfo(const QXmlStreamAttributes& attributes)
749 {
750     const QString name = attributes.value("name").toString();
751     const QString id   = attributes.value("id").toString();
752 
753     _guiProfile->setId(id);
754     _guiProfile->setName(name);
755 }
756 
757 
addProduct(const QXmlStreamAttributes & attributes)758 void GUIProfileParser::addProduct(const QXmlStreamAttributes& attributes)
759 {
760     const QString vendor  = attributes.value("vendor").toString();
761     const QString name    = attributes.value("name").toString();
762     const QString release = attributes.value("release").toString();
763     const QString comment = attributes.value("comment").toString();
764 
765     // Adding a product makes only sense if we have at least
766     // the vendor and product name.
767     if (vendor.isEmpty() || name.isEmpty()) return;
768 
769     ProfProduct *prd = new ProfProduct();
770     prd->vendor = vendor;
771     prd->productName = name;
772     prd->productRelease = release;
773     prd->comment = comment;
774 
775     _guiProfile->addProduct(prd);
776 }
777 
778 
addControl(const QXmlStreamAttributes & attributes)779 void GUIProfileParser::addControl(const QXmlStreamAttributes &attributes)
780 {
781     const QString id          = attributes.value("id").toString();
782     const QString subcontrols = attributes.value("subcontrols").toString();
783     const QString name        = attributes.value("name").toString();
784     const QString show        = attributes.value("show").toString();
785     const QString background  = attributes.value("background").toString();
786     const QString switchtype  = attributes.value("switchtype").toString();
787     const QString mandatory   = attributes.value("mandatory").toString();
788     const QString split       = attributes.value("split").toString();
789 
790     // We need at least an "id".  We can set defaults for the rest, if undefined.
791     if (id.isEmpty()) return;
792 
793     // ignore whether 'name' is null, will be checked by all users.
794     bool isMandatory = (mandatory=="true");
795     // ignore whether 'background' is null, will be checked by all users.
796     // ignore whether 'switchtype' is null, will be checked by all users.
797 
798     // For compatibility reasons, we interpret an empty string as match-all (aka "*")
799     ProfControl *profControl = new ProfControl(id, (subcontrols.isEmpty() ? "*" : subcontrols));
800 
801     profControl->setName(name);
802     profControl->setVisibility(show.isEmpty() ? "all" : show);
803     profControl->setBackgroundColor(background);
804     profControl->setSwitchtype(switchtype);
805     profControl->setMandatory(isMandatory);
806     if (split=="true") profControl->setSplit(true);
807 
808     _guiProfile->addControl(profControl);
809 }
810