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