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(" "));
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