1 #include "controllers/dlgprefcontroller.h"
2 
3 #include <QDesktopServices>
4 #include <QDir>
5 #include <QFileDialog>
6 #include <QFileInfo>
7 #include <QInputDialog>
8 #include <QStandardPaths>
9 #include <QTableWidget>
10 #include <QTableWidgetItem>
11 #include <QtDebug>
12 
13 #include "controllers/controller.h"
14 #include "controllers/controllerlearningeventfilter.h"
15 #include "controllers/controllermanager.h"
16 #include "controllers/defs_controllers.h"
17 #include "controllers/midi/midicontrollerpreset.h"
18 #include "defs_urls.h"
19 #include "moc_dlgprefcontroller.cpp"
20 #include "preferences/usersettings.h"
21 #include "util/versionstore.h"
22 
23 namespace {
24 const QString kPresetExt(".midi.xml");
25 
presetNameToPath(const QString & directory,const QString & presetName)26 QString presetNameToPath(const QString& directory, const QString& presetName) {
27     // While / is allowed for the display name we can't use it for the file name.
28     QString fileName = QString(presetName).replace(QChar('/'), QChar('-'));
29     return directory + fileName + kPresetExt;
30 }
31 
32 } // namespace
33 
DlgPrefController(QWidget * parent,Controller * controller,ControllerManager * controllerManager,UserSettingsPointer pConfig)34 DlgPrefController::DlgPrefController(QWidget* parent,
35         Controller* controller,
36         ControllerManager* controllerManager,
37         UserSettingsPointer pConfig)
38         : DlgPreferencePage(parent),
39           m_pConfig(pConfig),
40           m_pUserDir(userPresetsPath(pConfig)),
41           m_pControllerManager(controllerManager),
42           m_pController(controller),
43           m_pDlgControllerLearning(nullptr),
44           m_pInputTableModel(nullptr),
45           m_pInputProxyModel(nullptr),
46           m_pOutputTableModel(nullptr),
47           m_pOutputProxyModel(nullptr),
48           m_bDirty(false) {
49     m_ui.setupUi(this);
50     // Create text color for the file and wiki links
51     createLinkColor();
52 
53     initTableView(m_ui.m_pInputMappingTableView);
54     initTableView(m_ui.m_pOutputMappingTableView);
55 
56     connect(m_pController, &Controller::presetLoaded, this, &DlgPrefController::slotShowPreset);
57     // TODO(rryan): Eh, this really isn't thread safe but it's the way it's been
58     // since 1.11.0. We shouldn't be calling Controller methods because it lives
59     // in a different thread. Booleans (like isOpen()) are fine but a complex
60     // object like a preset involves QHash's and other data structures that
61     // really don't like concurrent access.
62     ControllerPresetPointer pPreset = m_pController->getPreset();
63     slotShowPreset(pPreset);
64 
65     m_ui.labelDeviceName->setText(m_pController->getName());
66     QString category = m_pController->getCategory();
67     if (!category.isEmpty()) {
68         m_ui.labelDeviceCategory->setText(category);
69     } else {
70         m_ui.labelDeviceCategory->hide();
71     }
72 
73     // When the user picks a preset, load it.
74     connect(m_ui.comboBoxPreset,
75             QOverload<int>::of(&QComboBox::currentIndexChanged),
76             this,
77             &DlgPrefController::slotPresetSelected);
78 
79     // When the user toggles the Enabled checkbox, mark as dirty
80     connect(m_ui.chkEnabledDevice, &QCheckBox::clicked, this, [this] { setDirty(true); });
81 
82     // Connect our signals to controller manager.
83     connect(this,
84             &DlgPrefController::applyPreset,
85             m_pControllerManager,
86             &ControllerManager::slotApplyPreset);
87 
88     // Open script file links
89     connect(m_ui.labelLoadedPresetScriptFileLinks,
90             &QLabel::linkActivated,
91             [](const QString& path) {
92                 QDesktopServices::openUrl(QUrl::fromLocalFile(path));
93             });
94 
95     // Input mappings
96     connect(m_ui.btnAddInputMapping,
97             &QAbstractButton::clicked,
98             this,
99             &DlgPrefController::addInputMapping);
100     connect(m_ui.btnRemoveInputMappings,
101             &QAbstractButton::clicked,
102             this,
103             &DlgPrefController::removeInputMappings);
104     connect(m_ui.btnLearningWizard,
105             &QAbstractButton::clicked,
106             this,
107             &DlgPrefController::showLearningWizard);
108     connect(m_ui.btnClearAllInputMappings,
109             &QAbstractButton::clicked,
110             this,
111             &DlgPrefController::clearAllInputMappings);
112 
113     // Output mappings
114     connect(m_ui.btnAddOutputMapping,
115             &QAbstractButton::clicked,
116             this,
117             &DlgPrefController::addOutputMapping);
118     connect(m_ui.btnRemoveOutputMappings,
119             &QAbstractButton::clicked,
120             this,
121             &DlgPrefController::removeOutputMappings);
122     connect(m_ui.btnClearAllOutputMappings,
123             &QAbstractButton::clicked,
124             this,
125             &DlgPrefController::clearAllOutputMappings);
126 }
127 
~DlgPrefController()128 DlgPrefController::~DlgPrefController() {
129 }
130 
showLearningWizard()131 void DlgPrefController::showLearningWizard() {
132     if (isDirty()) {
133         QMessageBox::StandardButton result = QMessageBox::question(this,
134                 tr("Apply device settings?"),
135                 tr("Your settings must be applied before starting the learning "
136                    "wizard.\n"
137                    "Apply settings and continue?"),
138                 QMessageBox::Ok |
139                         QMessageBox::Cancel, // Buttons to be displayed
140                 QMessageBox::Ok);            // Default button
141         // Stop if the user has not pressed the Ok button,
142         // which could be the Cancel or the Close Button.
143         if (result != QMessageBox::Ok) {
144             return;
145         }
146     }
147     slotApply();
148 
149     if (!m_pPreset) {
150         m_pPreset = ControllerPresetPointer(new MidiControllerPreset());
151         emit applyPreset(m_pController, m_pPreset, true);
152     }
153 
154     // Note that DlgControllerLearning is set to delete itself on close using
155     // the Qt::WA_DeleteOnClose attribute (so this "new" doesn't leak memory)
156     m_pDlgControllerLearning = new DlgControllerLearning(this, m_pController);
157     m_pDlgControllerLearning->show();
158     ControllerLearningEventFilter* pControllerLearning =
159             m_pControllerManager->getControllerLearningEventFilter();
160     pControllerLearning->startListening();
161     connect(pControllerLearning,
162             &ControllerLearningEventFilter::controlClicked,
163             m_pDlgControllerLearning,
164             &DlgControllerLearning::controlClicked);
165     connect(m_pDlgControllerLearning,
166             &DlgControllerLearning::listenForClicks,
167             pControllerLearning,
168             &ControllerLearningEventFilter::startListening);
169     connect(m_pDlgControllerLearning,
170             &DlgControllerLearning::stopListeningForClicks,
171             pControllerLearning,
172             &ControllerLearningEventFilter::stopListening);
173     connect(m_pDlgControllerLearning,
174             &DlgControllerLearning::stopLearning,
175             this,
176             &DlgPrefController::show);
177     connect(m_pDlgControllerLearning,
178             &DlgControllerLearning::inputMappingsLearned,
179             this,
180             &DlgPrefController::midiInputMappingsLearned);
181 
182     emit mappingStarted();
183     connect(m_pDlgControllerLearning,
184             &DlgControllerLearning::stopLearning,
185             this,
186             &DlgPrefController::slotStopLearning);
187 }
188 
slotStopLearning()189 void DlgPrefController::slotStopLearning() {
190     VERIFY_OR_DEBUG_ASSERT(m_pPreset) {
191         emit mappingEnded();
192         return;
193     }
194 
195     applyPresetChanges();
196     if (m_pPreset->filePath().isEmpty()) {
197         // This mapping was created when the learning wizard was started
198         if (m_pPreset->isDirty()) {
199             QString presetName = askForPresetName();
200             QString presetPath = presetNameToPath(m_pUserDir, presetName);
201             m_pPreset->setName(presetName);
202             if (m_pPreset->savePreset(presetPath)) {
203                 qDebug() << "Mapping saved as" << presetPath;
204                 m_pPreset->setFilePath(presetPath);
205                 m_pPreset->setDirty(false);
206                 emit applyPreset(m_pController, m_pPreset, true);
207                 enumeratePresets(presetPath);
208             } else {
209                 qDebug() << "Failed to save mapping as" << presetPath;
210                 // Discard the new mapping and disable the controller
211                 m_pPreset.reset();
212                 emit applyPreset(m_pController, m_pPreset, false);
213             }
214         } else {
215             // No changes made to the new mapping, discard it and disable the
216             // controller
217             m_pPreset.reset();
218             emit applyPreset(m_pController, m_pPreset, false);
219         }
220     }
221 
222     emit mappingEnded();
223 }
224 
midiInputMappingsLearned(const MidiInputMappings & mappings)225 void DlgPrefController::midiInputMappingsLearned(
226         const MidiInputMappings& mappings) {
227     // This is just a shortcut since doing a round-trip from Learning ->
228     // Controller -> slotPresetLoaded -> setPreset is too heavyweight.
229     if (m_pInputTableModel != nullptr) {
230         m_pInputTableModel->addMappings(mappings);
231     }
232 }
233 
presetShortName(const ControllerPresetPointer pPreset) const234 QString DlgPrefController::presetShortName(
235         const ControllerPresetPointer pPreset) const {
236     QString presetName = tr("None");
237     if (pPreset) {
238         QString name = pPreset->name();
239         QString author = pPreset->author();
240         if (name.length() > 0 && author.length() > 0) {
241             presetName = tr("%1 by %2").arg(pPreset->name(), pPreset->author());
242         } else if (name.length() > 0) {
243             presetName = name;
244         } else if (pPreset->filePath().length() > 0) {
245             QFileInfo file(pPreset->filePath());
246             presetName = file.baseName();
247         }
248     }
249     return presetName;
250 }
251 
presetName(const ControllerPresetPointer pPreset) const252 QString DlgPrefController::presetName(
253         const ControllerPresetPointer pPreset) const {
254     if (pPreset) {
255         QString name = pPreset->name();
256         if (name.length() > 0) {
257             return name;
258         }
259     }
260     return tr("No Name");
261 }
262 
presetDescription(const ControllerPresetPointer pPreset) const263 QString DlgPrefController::presetDescription(
264         const ControllerPresetPointer pPreset) const {
265     if (pPreset) {
266         QString description = pPreset->description();
267         if (description.length() > 0) {
268             return description;
269         }
270     }
271     return tr("No Description");
272 }
273 
presetAuthor(const ControllerPresetPointer pPreset) const274 QString DlgPrefController::presetAuthor(
275         const ControllerPresetPointer pPreset) const {
276     if (pPreset) {
277         QString author = pPreset->author();
278         if (author.length() > 0) {
279             return author;
280         }
281     }
282     return tr("No Author");
283 }
284 
presetSupportLinks(const ControllerPresetPointer pPreset) const285 QString DlgPrefController::presetSupportLinks(
286         const ControllerPresetPointer pPreset) const {
287     if (!pPreset) {
288         return QString();
289     }
290 
291     QStringList linkList;
292 
293     QString forumLink = pPreset->forumlink();
294     if (!forumLink.isEmpty()) {
295         linkList << coloredLinkString(
296                 m_pLinkColor,
297                 "Mixxx Forums",
298                 forumLink);
299     }
300 
301     QString wikiLink = pPreset->wikilink();
302     if (!wikiLink.isEmpty()) {
303         linkList << coloredLinkString(
304                 m_pLinkColor,
305                 "Mixxx Wiki",
306                 wikiLink);
307     }
308 
309     QString manualLink = pPreset->manualLink();
310     if (!manualLink.isEmpty()) {
311         linkList << coloredLinkString(
312                 m_pLinkColor,
313                 "Mixxx Manual",
314                 manualLink);
315     }
316 
317     // There is always at least one support link.
318     // TODO(rryan): This is a horrible general support link for MIDI!
319     linkList << coloredLinkString(
320             m_pLinkColor,
321             tr("Troubleshooting"),
322             MIXXX_WIKI_MIDI_SCRIPTING_URL);
323 
324     return QString(linkList.join("&nbsp;&nbsp;"));
325 }
326 
presetFileLinks(const ControllerPresetPointer pPreset) const327 QString DlgPrefController::presetFileLinks(
328         const ControllerPresetPointer pPreset) const {
329     if (!pPreset) {
330         return QString();
331     }
332 
333     const QString builtinFileSuffix = QStringLiteral(" (") + tr("built-in") + QStringLiteral(")");
334     QString systemPresetPath = resourcePresetsPath(m_pConfig);
335     QStringList linkList;
336     QString xmlFileLink = coloredLinkString(
337             m_pLinkColor,
338             QFileInfo(pPreset->filePath()).fileName(),
339             pPreset->filePath());
340     if (pPreset->filePath().startsWith(systemPresetPath)) {
341         xmlFileLink += builtinFileSuffix;
342     }
343     linkList << xmlFileLink;
344 
345     for (const auto& script : pPreset->getScriptFiles()) {
346         QString scriptFileLink = coloredLinkString(
347                 m_pLinkColor,
348                 script.name,
349                 script.file.absoluteFilePath());
350         if (!script.file.exists()) {
351             scriptFileLink +=
352                     QStringLiteral(" (") + tr("missing") + QStringLiteral(")");
353         } else if (script.file.absoluteFilePath().startsWith(
354                            systemPresetPath)) {
355             scriptFileLink += builtinFileSuffix;
356         }
357 
358         linkList << scriptFileLink;
359     }
360     return linkList.join("<br/>");
361 }
362 
enumeratePresets(const QString & selectedPresetPath)363 void DlgPrefController::enumeratePresets(const QString& selectedPresetPath) {
364     m_ui.comboBoxPreset->blockSignals(true);
365     m_ui.comboBoxPreset->clear();
366 
367     // qDebug() << "Enumerating presets for controller" << m_pController->getName();
368 
369     // Check the text color of the palette for whether to use dark or light icons
370     QDir iconsPath;
371     if (!Color::isDimColor(palette().text().color())) {
372         iconsPath.setPath(":/images/preferences/light/");
373     } else {
374         iconsPath.setPath(":/images/preferences/dark/");
375     }
376 
377     // Insert a dummy item at the top to try to make it less confusing.
378     // (We don't want the first found file showing up as the default item when a
379     // user has their controller plugged in)
380     QIcon noPresetIcon(iconsPath.filePath("ic_none.svg"));
381     m_ui.comboBoxPreset->addItem(noPresetIcon, tr("No Preset"));
382 
383     PresetInfo match;
384     // Enumerate user presets
385     QIcon userPresetIcon(iconsPath.filePath("ic_custom.svg"));
386 
387     // Reload user presets to detect added, changed or removed mappings
388     m_pControllerManager->getMainThreadUserPresetEnumerator()->loadSupportedPresets();
389 
390     PresetInfo userPresetsMatch = enumeratePresetsFromEnumerator(
391             m_pControllerManager->getMainThreadUserPresetEnumerator(),
392             userPresetIcon);
393     if (userPresetsMatch.isValid()) {
394         match = userPresetsMatch;
395     }
396 
397     // Insert a separator between user presets (+ dummy item) and system presets
398     m_ui.comboBoxPreset->insertSeparator(m_ui.comboBoxPreset->count());
399 
400     // Enumerate system presets
401     QIcon systemPresetIcon(iconsPath.filePath("ic_mixxx_symbolic.svg"));
402     PresetInfo systemPresetsMatch = enumeratePresetsFromEnumerator(
403             m_pControllerManager->getMainThreadSystemPresetEnumerator(),
404             systemPresetIcon);
405     if (systemPresetsMatch.isValid()) {
406         match = systemPresetsMatch;
407     }
408 
409     // Preselect configured or matching preset
410     int index = -1;
411     if (!selectedPresetPath.isEmpty()) {
412         index = m_ui.comboBoxPreset->findData(selectedPresetPath);
413     } else if (match.isValid()) {
414         index = m_ui.comboBoxPreset->findText(match.getName());
415     }
416     if (index == -1) {
417         m_ui.chkEnabledDevice->setEnabled(false);
418     } else {
419         m_ui.comboBoxPreset->setCurrentIndex(index);
420         m_ui.chkEnabledDevice->setEnabled(true);
421     }
422     m_ui.comboBoxPreset->blockSignals(false);
423     slotPresetSelected(m_ui.comboBoxPreset->currentIndex());
424 }
425 
enumeratePresetsFromEnumerator(QSharedPointer<PresetInfoEnumerator> pPresetEnumerator,const QIcon & icon)426 PresetInfo DlgPrefController::enumeratePresetsFromEnumerator(
427         QSharedPointer<PresetInfoEnumerator> pPresetEnumerator, const QIcon& icon) {
428     PresetInfo match;
429 
430     // Check if enumerator is ready. Should be rare that it isn't. We will
431     // re-enumerate on the next open of the preferences.
432     if (!pPresetEnumerator.isNull()) {
433         // Get a list of presets in alphabetical order
434         QList<PresetInfo> systemPresets =
435                 pPresetEnumerator->getPresetsByExtension(
436                         m_pController->presetExtension());
437 
438         for (const PresetInfo& preset : systemPresets) {
439             m_ui.comboBoxPreset->addItem(
440                     icon, preset.getName(), preset.getPath());
441             if (m_pController->matchPreset(preset)) {
442                 match = preset;
443             }
444         }
445     }
446 
447     return match;
448 }
449 
slotUpdate()450 void DlgPrefController::slotUpdate() {
451     // Check if the controller is open.
452     bool deviceOpen = m_pController->isOpen();
453     // Check/uncheck the "Enabled" box
454     m_ui.chkEnabledDevice->setChecked(deviceOpen);
455 
456     enumeratePresets(m_pControllerManager->getConfiguredPresetFileForDevice(
457             m_pController->getName()));
458 
459     // If the controller is not mappable, disable the input and output mapping
460     // sections and the learning wizard button.
461     bool isMappable = m_pController->isMappable();
462     m_ui.btnLearningWizard->setEnabled(isMappable);
463     m_ui.inputMappingsTab->setEnabled(isMappable);
464     m_ui.outputMappingsTab->setEnabled(isMappable);
465 }
466 
slotResetToDefaults()467 void DlgPrefController::slotResetToDefaults() {
468     m_ui.chkEnabledDevice->setChecked(false);
469     enumeratePresets(QString());
470     slotPresetSelected(m_ui.comboBoxPreset->currentIndex());
471 }
472 
applyPresetChanges()473 void DlgPrefController::applyPresetChanges() {
474     if (m_pInputTableModel) {
475         m_pInputTableModel->apply();
476     }
477 
478     if (m_pOutputTableModel) {
479         m_pOutputTableModel->apply();
480     }
481 }
482 
slotApply()483 void DlgPrefController::slotApply() {
484     applyPresetChanges();
485 
486     // If no changes were made, do nothing
487     if (!(isDirty() || (m_pPreset && m_pPreset->isDirty()))) {
488         return;
489     }
490 
491     bool bEnabled = false;
492     if (m_pPreset) {
493         bEnabled = m_ui.chkEnabledDevice->isChecked();
494 
495         if (m_pPreset->isDirty()) {
496             savePreset();
497         }
498     }
499     m_ui.chkEnabledDevice->setChecked(bEnabled);
500 
501     // The shouldn't be dirty at this point because we already tried to save
502     // it. If that failed, don't apply the preset.
503     if (m_pPreset && m_pPreset->isDirty()) {
504         return;
505     }
506 
507     QString presetPath = presetPathFromIndex(m_ui.comboBoxPreset->currentIndex());
508     m_pPreset = ControllerPresetFileHandler::loadPreset(
509             presetPath, QDir(resourcePresetsPath(m_pConfig)));
510 
511     // Load the resulting preset (which has been mutated by the input/output
512     // table models). The controller clones the preset so we aren't touching
513     // the same preset.
514     emit applyPreset(m_pController, m_pPreset, bEnabled);
515 
516     // Mark the dialog as not dirty
517     setDirty(false);
518 }
519 
helpUrl() const520 QUrl DlgPrefController::helpUrl() const {
521     return QUrl(MIXXX_MANUAL_CONTROLLERS_URL);
522 }
523 
presetPathFromIndex(int index) const524 QString DlgPrefController::presetPathFromIndex(int index) const {
525     if (index == 0) {
526         // "No Preset" item
527         return QString();
528     }
529 
530     return m_ui.comboBoxPreset->itemData(index).toString();
531 }
532 
slotPresetSelected(int chosenIndex)533 void DlgPrefController::slotPresetSelected(int chosenIndex) {
534     QString presetPath = presetPathFromIndex(chosenIndex);
535     if (presetPath.isEmpty()) {
536         // User picked "No Preset" item
537         m_ui.chkEnabledDevice->setEnabled(false);
538 
539         if (m_ui.chkEnabledDevice->isChecked()) {
540             m_ui.chkEnabledDevice->setChecked(false);
541             setDirty(true);
542         }
543     } else {
544         // User picked a preset
545         m_ui.chkEnabledDevice->setEnabled(true);
546 
547         if (!m_ui.chkEnabledDevice->isChecked()) {
548             m_ui.chkEnabledDevice->setChecked(true);
549             setDirty(true);
550         }
551     }
552 
553     // Check if the preset is different from the configured preset
554     if (m_pControllerManager->getConfiguredPresetFileForDevice(
555                 m_pController->getName()) != presetPath) {
556         setDirty(true);
557     }
558 
559     applyPresetChanges();
560     if (m_pPreset && m_pPreset->isDirty()) {
561         if (QMessageBox::question(this,
562                     tr("Mapping has been edited"),
563                     tr("Do you want to save the changes?")) ==
564                 QMessageBox::Yes) {
565             savePreset();
566         }
567     }
568 
569     ControllerPresetPointer pPreset = ControllerPresetFileHandler::loadPreset(
570             presetPath, QDir(resourcePresetsPath(m_pConfig)));
571 
572     if (pPreset) {
573         DEBUG_ASSERT(!pPreset->isDirty());
574     }
575 
576     slotShowPreset(pPreset);
577 }
578 
savePreset()579 void DlgPrefController::savePreset() {
580     VERIFY_OR_DEBUG_ASSERT(m_pPreset) {
581         return;
582     }
583 
584     if (!m_pPreset->isDirty()) {
585         qDebug() << "Mapping is not dirty, no need to save it.";
586         return;
587     }
588 
589     QString oldFilePath = m_pPreset->filePath();
590     QString newFilePath;
591     QFileInfo fileInfo(oldFilePath);
592     QString presetName = m_pPreset->name();
593 
594     bool isUserPreset = fileInfo.absoluteDir().absolutePath().append("/") == m_pUserDir;
595     bool saveAsNew = true;
596     if (m_pOverwritePresets.contains(oldFilePath) &&
597             m_pOverwritePresets.value(oldFilePath) == true) {
598         saveAsNew = false;
599     }
600 
601     // If this is a user preset, ask whether to overwrite or save with new name.
602     // Optionally, tick checkbox to always overwrite this preset in the current session.
603     if (isUserPreset && saveAsNew) {
604         QString overwriteTitle = tr("Mapping already exists.");
605         QString overwriteLabel = tr(
606                 "<b>%1</b> already exists in user mapping folder.<br>"
607                 "Overwrite or save with a new name?");
608         QString overwriteCheckLabel = tr("Always overwrite during this session");
609 
610         QMessageBox overwriteMsgBox;
611         overwriteMsgBox.setIcon(QMessageBox::Question);
612         overwriteMsgBox.setWindowTitle(overwriteTitle);
613         overwriteMsgBox.setText(overwriteLabel.arg(presetName));
614         QCheckBox overwriteCheckBox;
615         overwriteCheckBox.setText(overwriteCheckLabel);
616         overwriteCheckBox.blockSignals(true);
617         overwriteCheckBox.setCheckState(Qt::Unchecked);
618         overwriteMsgBox.addButton(&overwriteCheckBox, QMessageBox::ActionRole);
619         QPushButton* pSaveAsNew = overwriteMsgBox.addButton(
620                 tr("Save As"), QMessageBox::AcceptRole);
621         QPushButton* pOverwrite = overwriteMsgBox.addButton(
622                 tr("Overwrite"), QMessageBox::AcceptRole);
623         overwriteMsgBox.setDefaultButton(pSaveAsNew);
624         overwriteMsgBox.exec();
625 
626         if (overwriteMsgBox.clickedButton() == pOverwrite) {
627             saveAsNew = false;
628             if (overwriteCheckBox.checkState() == Qt::Checked) {
629                 m_pOverwritePresets.insert(m_pPreset->filePath(), true);
630             }
631         } else if (overwriteMsgBox.close()) {
632             return;
633         }
634     }
635 
636     // Ask for a preset name when
637     // * initially saving a modified Mixxx preset to the user folder
638     // * saving a user preset with a new name.
639     // The name will be used as display name and file name.
640     if (!saveAsNew) {
641         newFilePath = oldFilePath;
642     } else {
643         presetName = askForPresetName(presetName);
644         newFilePath = presetNameToPath(m_pUserDir, presetName);
645         m_pPreset->setName(presetName);
646         qDebug() << "Mapping renamed to" << m_pPreset->name();
647     }
648 
649     if (!m_pPreset->savePreset(newFilePath)) {
650         qDebug() << "Failed to save mapping as" << newFilePath;
651         return;
652     }
653     qDebug() << "Mapping saved as" << newFilePath;
654 
655     m_pPreset->setFilePath(newFilePath);
656     m_pPreset->setDirty(false);
657 
658     enumeratePresets(m_pPreset->filePath());
659 }
660 
askForPresetName(const QString & prefilledName) const661 QString DlgPrefController::askForPresetName(const QString& prefilledName) const {
662     QString savePresetTitle = tr("Save user mapping");
663     QString savePresetLabel = tr("Enter the name for saving the mapping to the user folder.");
664     QString savingFailedTitle = tr("Saving mapping failed");
665     QString invalidNameLabel =
666             tr("A mapping cannot have a blank name and may not contain "
667                "special characters.");
668     QString fileExistsLabel = tr("A mapping file with that name already exists.");
669     // Only allow the name to contain letters, numbers, whitespaces and _-+()/
670     const QRegExp rxRemove = QRegExp("[^[(a-zA-Z0-9\\_\\-\\+\\(\\)\\/|\\s]");
671 
672     // Choose a new file (base) name
673     bool validPresetName = false;
674     QString presetName = prefilledName;
675     while (!validPresetName) {
676         bool ok = false;
677         presetName = QInputDialog::getText(nullptr,
678                 savePresetTitle,
679                 savePresetLabel,
680                 QLineEdit::Normal,
681                 presetName,
682                 &ok)
683                              .remove(rxRemove)
684                              .trimmed();
685         if (!ok) {
686             continue;
687         }
688         if (presetName.isEmpty()) {
689             QMessageBox::warning(nullptr,
690                     savingFailedTitle,
691                     invalidNameLabel);
692             continue;
693         }
694         QString presetPath = presetNameToPath(m_pUserDir, presetName);
695         if (QFile::exists(presetPath)) {
696             QMessageBox::warning(nullptr,
697                     savingFailedTitle,
698                     fileExistsLabel);
699             continue;
700         }
701         validPresetName = true;
702     }
703     return presetName;
704 }
705 
initTableView(QTableView * pTable)706 void DlgPrefController::initTableView(QTableView* pTable) {
707     // Enable selection by rows and extended selection (ctrl/shift click)
708     pTable->setSelectionBehavior(QAbstractItemView::SelectRows);
709     pTable->setSelectionMode(QAbstractItemView::ExtendedSelection);
710 
711     pTable->setWordWrap(false);
712     pTable->setShowGrid(false);
713     pTable->setCornerButtonEnabled(false);
714     pTable->setSortingEnabled(true);
715 
716     //Work around a Qt bug that lets you make your columns so wide you
717     //can't reach the divider to make them small again.
718     pTable->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
719 
720     pTable->verticalHeader()->hide();
721     pTable->verticalHeader()->setDefaultSectionSize(20);
722     pTable->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
723     pTable->setAlternatingRowColors(true);
724 }
725 
slotShowPreset(ControllerPresetPointer preset)726 void DlgPrefController::slotShowPreset(ControllerPresetPointer preset) {
727     m_ui.labelLoadedPreset->setText(presetName(preset));
728     m_ui.labelLoadedPresetDescription->setText(presetDescription(preset));
729     m_ui.labelLoadedPresetAuthor->setText(presetAuthor(preset));
730     m_ui.labelLoadedPresetSupportLinks->setText(presetSupportLinks(preset));
731     m_ui.labelLoadedPresetScriptFileLinks->setText(presetFileLinks(preset));
732 
733     // We mutate this preset so keep a reference to it while we are using it.
734     // TODO(rryan): Clone it? Technically a waste since nothing else uses this
735     // copy but if someone did they might not expect it to change.
736     m_pPreset = preset;
737 
738     ControllerInputMappingTableModel* pInputModel =
739             new ControllerInputMappingTableModel(this);
740     pInputModel->setPreset(preset);
741 
742     QSortFilterProxyModel* pInputProxyModel = new QSortFilterProxyModel(this);
743     pInputProxyModel->setSortRole(Qt::UserRole);
744     pInputProxyModel->setSourceModel(pInputModel);
745     m_ui.m_pInputMappingTableView->setModel(pInputProxyModel);
746 
747     for (int i = 0; i < pInputModel->columnCount(); ++i) {
748         QAbstractItemDelegate* pDelegate = pInputModel->delegateForColumn(
749             i, m_ui.m_pInputMappingTableView);
750         if (pDelegate != nullptr) {
751             qDebug() << "Setting input delegate for column" << i << pDelegate;
752             m_ui.m_pInputMappingTableView->setItemDelegateForColumn(i, pDelegate);
753         }
754     }
755 
756     // Now that we have set the new model our old model can be deleted.
757     delete m_pInputProxyModel;
758     m_pInputProxyModel = pInputProxyModel;
759     delete m_pInputTableModel;
760     m_pInputTableModel = pInputModel;
761 
762     ControllerOutputMappingTableModel* pOutputModel =
763             new ControllerOutputMappingTableModel(this);
764     pOutputModel->setPreset(preset);
765 
766     QSortFilterProxyModel* pOutputProxyModel = new QSortFilterProxyModel(this);
767     pOutputProxyModel->setSortRole(Qt::UserRole);
768     pOutputProxyModel->setSourceModel(pOutputModel);
769     m_ui.m_pOutputMappingTableView->setModel(pOutputProxyModel);
770 
771     for (int i = 0; i < pOutputModel->columnCount(); ++i) {
772         QAbstractItemDelegate* pDelegate = pOutputModel->delegateForColumn(
773             i, m_ui.m_pOutputMappingTableView);
774         if (pDelegate != nullptr) {
775             qDebug() << "Setting output delegate for column" << i << pDelegate;
776             m_ui.m_pOutputMappingTableView->setItemDelegateForColumn(i, pDelegate);
777         }
778     }
779 
780     // Now that we have set the new model our old model can be deleted.
781     delete m_pOutputProxyModel;
782     m_pOutputProxyModel = pOutputProxyModel;
783     delete m_pOutputTableModel;
784     m_pOutputTableModel = pOutputModel;
785 }
786 
addInputMapping()787 void DlgPrefController::addInputMapping() {
788     if (m_pInputTableModel) {
789         m_pInputTableModel->addEmptyMapping();
790         // Ensure the added row is visible.
791         QModelIndex left = m_pInputProxyModel->mapFromSource(
792             m_pInputTableModel->index(m_pInputTableModel->rowCount() - 1, 0));
793         QModelIndex right = m_pInputProxyModel->mapFromSource(
794             m_pInputTableModel->index(m_pInputTableModel->rowCount() - 1,
795                                        m_pInputTableModel->columnCount() - 1));
796         m_ui.m_pInputMappingTableView->selectionModel()->select(
797             QItemSelection(left, right), QItemSelectionModel::Clear | QItemSelectionModel::Select);
798         m_ui.m_pInputMappingTableView->scrollTo(left);
799     }
800 }
801 
removeInputMappings()802 void DlgPrefController::removeInputMappings() {
803     if (m_pInputProxyModel) {
804         QItemSelection selection = m_pInputProxyModel->mapSelectionToSource(
805             m_ui.m_pInputMappingTableView->selectionModel()->selection());
806         QModelIndexList selectedIndices = selection.indexes();
807         if (selectedIndices.size() > 0 && m_pInputTableModel) {
808             m_pInputTableModel->removeMappings(selectedIndices);
809         }
810     }
811 }
812 
clearAllInputMappings()813 void DlgPrefController::clearAllInputMappings() {
814     if (QMessageBox::warning(
815             this, tr("Clear Input Mappings"),
816             tr("Are you sure you want to clear all input mappings?"),
817             QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel) != QMessageBox::Ok) {
818         return;
819     }
820     if (m_pInputTableModel) {
821         m_pInputTableModel->clear();
822     }
823 }
824 
addOutputMapping()825 void DlgPrefController::addOutputMapping() {
826     if (m_pOutputTableModel) {
827         m_pOutputTableModel->addEmptyMapping();
828         // Ensure the added row is visible.
829         QModelIndex left = m_pOutputProxyModel->mapFromSource(
830             m_pOutputTableModel->index(m_pOutputTableModel->rowCount() - 1, 0));
831         QModelIndex right = m_pOutputProxyModel->mapFromSource(
832             m_pOutputTableModel->index(m_pOutputTableModel->rowCount() - 1,
833                                        m_pOutputTableModel->columnCount() - 1));
834         m_ui.m_pOutputMappingTableView->selectionModel()->select(
835             QItemSelection(left, right), QItemSelectionModel::Clear | QItemSelectionModel::Select);
836         m_ui.m_pOutputMappingTableView->scrollTo(left);
837     }
838 }
839 
removeOutputMappings()840 void DlgPrefController::removeOutputMappings() {
841     if (m_pOutputProxyModel) {
842         QItemSelection selection = m_pOutputProxyModel->mapSelectionToSource(
843             m_ui.m_pOutputMappingTableView->selectionModel()->selection());
844         QModelIndexList selectedIndices = selection.indexes();
845         if (selectedIndices.size() > 0 && m_pOutputTableModel) {
846             m_pOutputTableModel->removeMappings(selectedIndices);
847         }
848     }
849 }
850 
clearAllOutputMappings()851 void DlgPrefController::clearAllOutputMappings() {
852     if (QMessageBox::warning(
853             this, tr("Clear Output Mappings"),
854             tr("Are you sure you want to clear all output mappings?"),
855             QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel) != QMessageBox::Ok) {
856         return;
857     }
858     if (m_pOutputTableModel) {
859         m_pOutputTableModel->clear();
860     }
861 }
862