diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c67775d2566..70d4954448f1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -731,10 +731,14 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL src/controllers/controllerlearningeventfilter.cpp src/controllers/controllermanager.cpp src/controllers/controllermappinginfo.cpp + src/controllers/legacycontrollersettings.cpp + src/controllers/legacycontrollersettingslayout.cpp src/controllers/controllermappinginfoenumerator.cpp src/controllers/controllermappingtablemodel.cpp src/controllers/controlleroutputmappingtablemodel.cpp src/controllers/controlpickermenu.cpp + src/controllers/legacycontrollermappingfilehandler.cpp + src/controllers/legacycontrollermapping.cpp src/controllers/delegates/controldelegate.cpp src/controllers/delegates/midibytedelegate.cpp src/controllers/delegates/midichanneldelegate.cpp @@ -2070,6 +2074,7 @@ add_executable(mixxx-test src/test/colorpalette_test.cpp src/test/configobject_test.cpp src/test/controller_mapping_validation_test.cpp + src/test/controller_mapping_settings_test.cpp src/test/controllerscriptenginelegacy_test.cpp src/test/controlobjecttest.cpp src/test/controlobjectaliastest.cpp diff --git a/res/controllers/engine-api.d.ts b/res/controllers/engine-api.d.ts index 4a2b935cd298..e40f67db0d2a 100644 --- a/res/controllers/engine-api.d.ts +++ b/res/controllers/engine-api.d.ts @@ -24,6 +24,16 @@ declare interface ScriptConnection { /** ControllerScriptInterfaceLegacy */ declare namespace engine { + type SettingValue = string | number | boolean; + /** + * Gets the value of a controller setting + * The value is either set in the preferences dialog, + * or got restored from file. + * @param name Name of the setting (as specified in the XML file of the mapping) + * @returns Value of the setting, or undefined in failure case + */ + function getSetting(name: string): SettingValue | undefined; + /** * Gets the control value * diff --git a/src/controllers/controller.cpp b/src/controllers/controller.cpp index c9cb45c7e5eb..e37933858d18 100644 --- a/src/controllers/controller.cpp +++ b/src/controllers/controller.cpp @@ -76,6 +76,8 @@ bool Controller::applyMapping() { } m_pScriptEngineLegacy->setScriptFiles(scriptFiles); + + m_pScriptEngineLegacy->setSettings(pMapping->getSettings()); return m_pScriptEngineLegacy->initialize(); } diff --git a/src/controllers/controllermanager.cpp b/src/controllers/controllermanager.cpp index 52501776ffb7..7dbac102ef32 100644 --- a/src/controllers/controllermanager.cpp +++ b/src/controllers/controllermanager.cpp @@ -282,6 +282,7 @@ void ControllerManager::slotSetUpDevices() { if (!pMapping) { continue; } + pMapping->loadSettings(m_pConfig, pController->getName()); // This runs on the main thread but LegacyControllerMapping is not thread safe, so clone it. pController->setMapping(pMapping->clone()); diff --git a/src/controllers/dlgprefcontroller.cpp b/src/controllers/dlgprefcontroller.cpp index 2c6f09dee856..3b672d8005c9 100644 --- a/src/controllers/dlgprefcontroller.cpp +++ b/src/controllers/dlgprefcontroller.cpp @@ -21,6 +21,7 @@ #include "moc_dlgprefcontroller.cpp" #include "preferences/usersettings.h" #include "util/desktophelper.h" +#include "util/parented_ptr.h" #include "util/string.h" namespace { @@ -410,6 +411,7 @@ QString DlgPrefController::mappingFileLinks( void DlgPrefController::enumerateMappings(const QString& selectedMappingPath) { m_ui.comboBoxMapping->blockSignals(true); + QString currentMappingFilePath = mappingFilePathFromIndex(m_ui.comboBoxMapping->currentIndex()); m_ui.comboBoxMapping->clear(); // qDebug() << "Enumerating mappings for controller" << m_pController->getName(); @@ -461,14 +463,18 @@ void DlgPrefController::enumerateMappings(const QString& selectedMappingPath) { } else if (match.isValid()) { index = m_ui.comboBoxMapping->findText(match.getName()); } + QString newMappingFilePath = mappingFilePathFromIndex(index); if (index == -1) { m_ui.chkEnabledDevice->setEnabled(false); + m_ui.groupBoxSettings->setVisible(false); } else { m_ui.comboBoxMapping->setCurrentIndex(index); m_ui.chkEnabledDevice->setEnabled(true); } m_ui.comboBoxMapping->blockSignals(false); - slotMappingSelected(m_ui.comboBoxMapping->currentIndex()); + if (newMappingFilePath != currentMappingFilePath) { + slotMappingSelected(index); + } } MappingInfo DlgPrefController::enumerateMappingsFromEnumerator( @@ -498,6 +504,8 @@ MappingInfo DlgPrefController::enumerateMappingsFromEnumerator( void DlgPrefController::slotUpdate() { enumerateMappings(m_pControllerManager->getConfiguredMappingFileForDevice( m_pController->getName())); + // Force updating the controller settings + slotMappingSelected(m_ui.comboBoxMapping->currentIndex()); // enumeratePresets calls slotPresetSelected which will check the m_ui.chkEnabledDevice // checkbox if there is a valid mapping saved in the mixxx.cfg file. However, the @@ -514,9 +522,10 @@ void DlgPrefController::slotUpdate() { } void DlgPrefController::slotResetToDefaults() { - m_ui.chkEnabledDevice->setChecked(false); + if (m_pMapping) { + m_pMapping->resetSettings(); + } enumerateMappings(QString()); - slotMappingSelected(m_ui.comboBoxMapping->currentIndex()); } void DlgPrefController::applyMappingChanges() { @@ -557,9 +566,13 @@ void DlgPrefController::slotApply() { return; } - QString mappingPath = mappingPathFromIndex(m_ui.comboBoxMapping->currentIndex()); - m_pMapping = LegacyControllerMappingFileHandler::loadMapping( - QFileInfo(mappingPath), QDir(resourceMappingsPath(m_pConfig))); + // If there is currently a mapping loaded, we save the new settings for it. + // Note that `m_pMapping`, `mappingFileInfo` and the setting on the screen + // will always match as the settings displayed are updated depending of the + // currently selected mapping in `slotMappingSelected` + if (m_pMapping) { + m_pMapping->saveSettings(m_pConfig, m_pController->getName()); + } // Load the resulting mapping (which has been mutated by the input/output // table models). The controller clones the mapping so we aren't touching @@ -593,7 +606,7 @@ void DlgPrefController::enableWizardAndIOTabs(bool enable) { m_ui.outputMappingsTab->setEnabled(enable); } -QString DlgPrefController::mappingPathFromIndex(int index) const { +QString DlgPrefController::mappingFilePathFromIndex(int index) const { if (index == 0) { // "No Mapping" item return QString(); @@ -603,8 +616,8 @@ QString DlgPrefController::mappingPathFromIndex(int index) const { } void DlgPrefController::slotMappingSelected(int chosenIndex) { - QString mappingPath = mappingPathFromIndex(chosenIndex); - if (mappingPath.isEmpty()) { // User picked "No Mapping" item + QString mappingFilePath = mappingFilePathFromIndex(chosenIndex); + if (mappingFilePath.isEmpty()) { // User picked "No Mapping" item m_ui.chkEnabledDevice->setEnabled(false); if (m_ui.chkEnabledDevice->isChecked()) { @@ -614,6 +627,8 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) { } enableWizardAndIOTabs(false); } + + m_ui.groupBoxSettings->setVisible(false); } else { // User picked a mapping m_ui.chkEnabledDevice->setEnabled(true); @@ -628,7 +643,7 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) { // Check if the mapping is different from the configured mapping if (m_GuiInitialized && m_pControllerManager->getConfiguredMappingFileForDevice( - m_pController->getName()) != mappingPath) { + m_pController->getName()) != mappingFilePath) { setDirty(true); } @@ -643,9 +658,10 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) { } } + auto mappingFileInfo = QFileInfo(mappingFilePath); std::shared_ptr pMapping = LegacyControllerMappingFileHandler::loadMapping( - QFileInfo(mappingPath), QDir(resourceMappingsPath(m_pConfig))); + mappingFileInfo, QDir(resourceMappingsPath(m_pConfig))); if (pMapping) { DEBUG_ASSERT(!pMapping->isDirty()); @@ -654,7 +670,7 @@ void DlgPrefController::slotMappingSelected(int chosenIndex) { if (previousMappingSaved) { // We might have saved the previous preset with a new name, so update // the preset combobox. - enumerateMappings(mappingPath); + enumerateMappings(mappingFilePath); } else { slotShowMapping(pMapping); } @@ -823,10 +839,41 @@ void DlgPrefController::slotShowMapping(std::shared_ptr m_ui.labelLoadedMappingSupportLinks->setText(mappingSupportLinks(pMapping)); m_ui.labelLoadedMappingScriptFileLinks->setText(mappingFileLinks(pMapping)); - // We mutate this mapping so keep a reference to it while we are using it. - // TODO(rryan): Clone it? Technically a waste since nothing else uses this - // copy but if someone did they might not expect it to change. - m_pMapping = pMapping; + if (pMapping) { + pMapping->loadSettings(m_pConfig, m_pController->getName()); + auto settings = pMapping->getSettings(); + auto* pLayout = pMapping->getSettingsLayout(); + + QLayoutItem* pItem; + while ((pItem = m_ui.groupBoxSettings->layout()->takeAt(0)) != nullptr) { + delete pItem->widget(); + delete pItem; + } + + if (pLayout != nullptr && !settings.isEmpty()) { + m_ui.groupBoxSettings->layout()->addWidget(pLayout->build(m_ui.groupBoxSettings)); + + for (const auto& setting : qAsConst(settings)) { + connect(setting.get(), + &AbstractLegacyControllerSetting::changed, + this, + [this] { setDirty(true); }); + } + } + + m_ui.groupBoxSettings->setVisible(!settings.isEmpty()); + } + + // If there is still settings that may be saved and no new mapping selected + // (e.g restored default), we keep the the dirty mapping live so it can be + // saved in apply slot. If there is a new mapping, then setting changes are + // discarded + if (pMapping || (m_pMapping && !m_pMapping->hasDirtySettings())) { + // We mutate this mapping so keep a reference to it while we are using it. + // TODO(rryan): Clone it? Technically a waste since nothing else uses this + // copy but if someone did they might not expect it to change. + m_pMapping = pMapping; + } // Inputs tab ControllerInputMappingTableModel* pInputModel = diff --git a/src/controllers/dlgprefcontroller.h b/src/controllers/dlgprefcontroller.h index eccd070f22b7..9a3fbd7e6ed4 100644 --- a/src/controllers/dlgprefcontroller.h +++ b/src/controllers/dlgprefcontroller.h @@ -81,7 +81,7 @@ class DlgPrefController : public DlgPreferencePage { QString mappingDescription(const std::shared_ptr pMapping) const; QString mappingSupportLinks(const std::shared_ptr pMapping) const; QString mappingFileLinks(const std::shared_ptr pMapping) const; - QString mappingPathFromIndex(int index) const; + QString mappingFilePathFromIndex(int index) const; QString askForMappingName(const QString& prefilledName = QString()) const; void applyMappingChanges(); bool saveMapping(); diff --git a/src/controllers/dlgprefcontrollerdlg.ui b/src/controllers/dlgprefcontrollerdlg.ui index 1d391eb06aad..9b296011d893 100644 --- a/src/controllers/dlgprefcontrollerdlg.ui +++ b/src/controllers/dlgprefcontrollerdlg.ui @@ -29,110 +29,8 @@ Controller Setup - - - 0 - 0 - - - - - - - true - - - - 0 - 0 - - - - - 14 - 75 - true - - - - Controller Name - - - - - - - true - - - - 0 - 0 - - - - - - - (device category goes here) - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Enabled - - - - - - - Click to start the Controller Learning wizard. - - - - - - Learning Wizard (MIDI Only) - - - false - - - false - - - - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Load Mapping: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - comboBoxMapping - - - - + + @@ -191,6 +89,53 @@ + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Load Mapping: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + comboBoxMapping + + + + + + + true + + + + 0 + 0 + + + + + + + (device category goes here) + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + @@ -207,6 +152,71 @@ + + + + Click to start the Controller Learning wizard. + + + + + + Learning Wizard (MIDI Only) + + + false + + + false + + + + + + + true + + + + 0 + 0 + + + + + 14 + 75 + true + + + + Controller Name + + + + + + + Enabled + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 40 + + + + @@ -421,18 +431,22 @@ - - - - Qt::Vertical + + + + + 0 + 0 + - - - 20 - 40 - + + Mapping settings - + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + @@ -525,7 +539,6 @@ - Output Mappings diff --git a/src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp b/src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp index 1b715ea8cd60..d33506d567e3 100644 --- a/src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp +++ b/src/controllers/hid/legacyhidcontrollermappingfilehandler.cpp @@ -24,6 +24,7 @@ LegacyHidControllerMappingFileHandler::load(const QDomElement& root, auto pMapping = std::make_shared(); pMapping->setFilePath(filePath); parseMappingInfo(root, pMapping); + parseMappingSettings(root, pMapping.get()); addScriptFilesToMapping(controller, pMapping, systemMappingsPath); return pMapping; } diff --git a/src/controllers/legacycontrollermapping.cpp b/src/controllers/legacycontrollermapping.cpp new file mode 100644 index 000000000000..9f6a400e9dde --- /dev/null +++ b/src/controllers/legacycontrollermapping.cpp @@ -0,0 +1,93 @@ +#include "controllers/legacycontrollermapping.h" + +#include + +namespace { +const QString kControllerSettingsPreferenceGroupKey = QStringLiteral("[ControllerSettings_%1_%2]"); +const QString kControllerSettingsSettingPathSubst = QStringLiteral("%SETTING_PATH"); +const QString kControllerSettingsResourcePathSubst = QStringLiteral("%RESOURCE_PATH"); +} // anonymous namespace + +void LegacyControllerMapping::loadSettings(UserSettingsPointer pConfig, + const QString& controllerName) const { + auto mappingFile = QFileInfo(m_filePath); + DEBUG_ASSERT(mappingFile.exists()); + QString controllerPath = + mappingFile.absoluteFilePath() + .replace(pConfig->getSettingsPath(), + kControllerSettingsSettingPathSubst) + .replace(pConfig->getResourcePath(), + kControllerSettingsResourcePathSubst); + + QString controllerKey = QString(kControllerSettingsPreferenceGroupKey) + .arg(controllerName, controllerPath); + + auto availableSettings = getSettings(); + QList definedSettings = pConfig->getKeysWithGroup(controllerKey); + + QList availableSettingKeys; + for (const auto& pSetting : qAsConst(availableSettings)) { + availableSettingKeys.append(pSetting->variableName()); + } + + bool ok; + for (const auto& key : definedSettings) { + if (!availableSettingKeys.contains(key.item)) { + qDebug() << "The setting" << key.item + << "does not seem to exist in the mapping" << mappingFile.absoluteFilePath() + << ". It may be invalid or may have been removed."; + pConfig->remove(key); + continue; + } + const auto& pSetting = availableSettings.at(availableSettingKeys.indexOf(key.item)); + QString value = pConfig->getValueString(key); + if (!pSetting->valid()) { + qWarning() << "The setting" << pSetting->variableName() + << "for the mapping" << mappingFile.absoluteFilePath() + << "appears to be invalid. Its saved value won't be restored."; + } + pSetting->parse(value, &ok); + if (!ok || !pSetting->valid()) { + qWarning() << "The setting" << pSetting->variableName() + << "for the mapping" << mappingFile.absoluteFilePath() + << "could not be restore. Removing and resetting the setting default value."; + pConfig->remove(key); + pSetting->reset(); + } + } +} + +void LegacyControllerMapping::resetSettings() { + for (auto setting : getSettings()) { + setting->reset(); + } +} + +void LegacyControllerMapping::saveSettings(UserSettingsPointer pConfig, + const QString& controllerName) const { + auto mappingFile = QFileInfo(m_filePath); + DEBUG_ASSERT(mappingFile.exists()); + QString controllerPath = + mappingFile.absoluteFilePath() + .replace(pConfig->getSettingsPath(), kControllerSettingsSettingPathSubst) + .replace(pConfig->getResourcePath(), kControllerSettingsResourcePathSubst); + QString controllerKey = QString(kControllerSettingsPreferenceGroupKey) + .arg(controllerName, controllerPath); + for (auto setting : getSettings()) { + if (!setting->isDirty()) { + continue; + } + setting->save(); + if (!setting->valid()) { + qWarning() << "Setting" << setting->variableName() + << "for controller" << controllerName + << "is invalid. Its value will not be saved."; + continue; + } + if (setting->isDefault()) { + pConfig->remove(ConfigKey(controllerKey, setting->variableName())); + } else { + pConfig->set(ConfigKey(controllerKey, setting->variableName()), setting->stringify()); + } + } +} diff --git a/src/controllers/legacycontrollermapping.h b/src/controllers/legacycontrollermapping.h index b0a18ffd8e96..df2a534adbc9 100644 --- a/src/controllers/legacycontrollermapping.h +++ b/src/controllers/legacycontrollermapping.h @@ -1,14 +1,14 @@ #pragma once -#include +#include + #include -#include -#include -#include -#include -#include +#include +#include "controllers/legacycontrollersettings.h" +#include "controllers/legacycontrollersettingslayout.h" #include "defs_urls.h" +#include "preferences/usersettings.h" /// This class represents a controller mapping, containing the data elements that /// make it up. @@ -18,6 +18,25 @@ class LegacyControllerMapping { : m_bDirty(false), m_deviceDirection(DeviceDirection::Bidirectionnal) { } + LegacyControllerMapping(const LegacyControllerMapping& other) + : m_productMatches(other.m_productMatches), + m_bDirty(other.m_bDirty), + m_deviceId(other.m_deviceId), + m_filePath(other.m_filePath), + m_name(other.m_name), + m_author(other.m_author), + m_description(other.m_description), + m_forumlink(other.m_forumlink), + m_manualPage(other.m_manualPage), + m_wikilink(other.m_wikilink), + m_schemaVersion(other.m_schemaVersion), + m_mixxxVersion(other.m_mixxxVersion), + m_settings(other.m_settings), + m_settingsLayout(other.m_settingsLayout.get() != nullptr + ? other.m_settingsLayout->clone() + : nullptr), + m_scripts(other.m_scripts) { + } virtual ~LegacyControllerMapping() = default; virtual std::shared_ptr clone() const = 0; @@ -64,58 +83,106 @@ class LegacyControllerMapping { setDirty(true); } + /// Adds a setting option to the list of setting option for this mapping. + /// The option added must be a valid option. + /// @param option The option to add + /// @return whether or not the setting was added successfully. + bool addSetting(std::shared_ptr option) { + VERIFY_OR_DEBUG_ASSERT(option->valid()) { + return false; + } + for (const auto& setting : qAsConst(m_settings)) { + if (*setting == *option) { + qWarning() << "Mapping setting duplication detected for " + "setting with name" + << option->variableName() + << ". Keeping the first occurrence."; + return false; + } + } + m_settings.append(option); + return true; + } + + /// @brief Set a setting layout as they should be perceived when edited in + /// the preference dialog. + /// @param layout The layout root element + void setSettingLayout(std::unique_ptr&& layout) { + VERIFY_OR_DEBUG_ASSERT(layout.get()) { + return; + } + m_settingsLayout = std::move(layout); + } + const QList& getScriptFiles() const { return m_scripts; } - inline void setDeviceDirection(DeviceDirections aDeviceDirection) { + const QList>& getSettings() const { + return m_settings; + } + + bool hasDirtySettings() const { + for (const auto& setting : m_settings) { + if (setting->isDirty()) { + return true; + } + } + return false; + } + + LegacyControllerSettingsLayoutElement* getSettingsLayout() { + return m_settingsLayout.get(); + } + + void setDeviceDirection(DeviceDirections aDeviceDirection) { m_deviceDirection = aDeviceDirection; } - inline DeviceDirections getDeviceDirection() const { + DeviceDirections getDeviceDirection() const { return m_deviceDirection; } - inline void setDirty(bool bDirty) { + void setDirty(bool bDirty) { m_bDirty = bDirty; } - inline bool isDirty() const { + bool isDirty() const { return m_bDirty; } - inline void setDeviceId(const QString& id) { + void setDeviceId(const QString& id) { m_deviceId = id; setDirty(true); } - inline QString deviceId() const { + QString deviceId() const { return m_deviceId; } - inline void setFilePath(const QString& filePath) { + void setFilePath(const QString& filePath) { m_filePath = filePath; setDirty(true); } - inline QString filePath() const { + QString filePath() const { return m_filePath; } - inline QDir dirPath() const { + QDir dirPath() const { return QFileInfo(filePath()).absoluteDir(); } - inline void setName(const QString& name) { + void setName(const QString& name) { m_name = name; setDirty(true); } - inline QString name() const { + QString name() const { return m_name; } - inline void setAuthor(const QString& author) { + void setAuthor(const QString& author) { m_author = author; setDirty(true); } @@ -196,6 +263,12 @@ class LegacyControllerMapping { virtual bool isMappable() const = 0; + void loadSettings(UserSettingsPointer pConfig, + const QString& controllerName) const; + void saveSettings(UserSettingsPointer pConfig, + const QString& controllerName) const; + void resetSettings(); + // Optional list of controller device match details QList> m_productMatches; @@ -213,6 +286,8 @@ class LegacyControllerMapping { QString m_schemaVersion; QString m_mixxxVersion; + QList> m_settings; + std::unique_ptr m_settingsLayout; QList m_scripts; DeviceDirections m_deviceDirection; }; diff --git a/src/controllers/legacycontrollermappingfilehandler.cpp b/src/controllers/legacycontrollermappingfilehandler.cpp index c3e9868d6657..b8c6ab94321b 100644 --- a/src/controllers/legacycontrollermappingfilehandler.cpp +++ b/src/controllers/legacycontrollermappingfilehandler.cpp @@ -45,20 +45,20 @@ std::shared_ptr LegacyControllerMappingFileHandler::loa return nullptr; } - LegacyControllerMappingFileHandler* pHandler = nullptr; + std::unique_ptr pHandler; if (mappingFile.fileName().endsWith( MIDI_MAPPING_EXTENSION, Qt::CaseInsensitive)) { - pHandler = new LegacyMidiControllerMappingFileHandler(); + pHandler = std::make_unique(); } else if (mappingFile.fileName().endsWith( HID_MAPPING_EXTENSION, Qt::CaseInsensitive) || mappingFile.fileName().endsWith( BULK_MAPPING_EXTENSION, Qt::CaseInsensitive)) { #ifdef __HID__ - pHandler = new LegacyHidControllerMappingFileHandler(); + pHandler = std::make_unique(); #endif } - if (pHandler == nullptr) { + if (!pHandler) { qDebug() << "Mapping" << mappingFile.absoluteFilePath() << "has an unrecognized extension."; return nullptr; @@ -66,6 +66,7 @@ std::shared_ptr LegacyControllerMappingFileHandler::loa std::shared_ptr pMapping = pHandler->load( mappingFile.absoluteFilePath(), systemMappingsPath); + if (pMapping) { pMapping->setDirty(false); } @@ -108,6 +109,85 @@ void LegacyControllerMappingFileHandler::parseMappingInfo( mapping->setWikiLink(wiki.isNull() ? "" : wiki.text()); } +void LegacyControllerMappingFileHandler::parseMappingSettings( + const QDomElement& root, LegacyControllerMapping* mapping) const { + if (root.isNull() || !mapping) { + return; + } + + QDomElement settings = root.firstChildElement("settings"); + if (settings.isNull()) { + return; + } + + std::unique_ptr settingLayout = + std::make_unique( + LegacyControllerSettingsLayoutContainer::Disposition:: + VERTICAL); + parseMappingSettingsElement(settings, mapping, settingLayout.get()); + mapping->setSettingLayout(std::move(settingLayout)); +} + +void LegacyControllerMappingFileHandler::parseMappingSettingsElement( + const QDomElement& current, + LegacyControllerMapping* pMapping, + LegacyControllerSettingsLayoutContainer* pLayout) + const { + for (QDomElement element = current.firstChildElement(); + !element.isNull(); + element = element.nextSiblingElement()) { + const QString& tagName = element.tagName().toLower(); + if (tagName == "option") { + std::shared_ptr pSetting( + LegacyControllerSettingBuilder::build(element)); + if (pSetting.get() == nullptr) { + qDebug() << "Ignoring unsupported controller setting in file" + << pMapping->filePath() << "at line" + << element.lineNumber() << "."; + continue; + } + if (!pSetting->valid()) { + qDebug() << "The parsed setting in file" << pMapping->filePath() + << "at line" << element.lineNumber() + << "appears to be invalid. It will be ignored."; + continue; + } + if (pMapping->addSetting(pSetting)) { + pLayout->addItem(pSetting); + } else { + qDebug() << "The parsed setting in file" << pMapping->filePath() + << "at line" << element.lineNumber() + << "couldn't be added. Its layout information will also be ignored."; + continue; + } + } else if (tagName == "row") { + LegacyControllerSettingsLayoutContainer::Disposition orientation = + element.attribute("orientation").trimmed().toLower() == + "vertical" + ? LegacyControllerSettingsLayoutContainer::VERTICAL + : LegacyControllerSettingsLayoutContainer::HORIZONTAL; + std::unique_ptr row = + std::make_unique( + LegacyControllerSettingsLayoutContainer::HORIZONTAL, + orientation); + parseMappingSettingsElement(element, pMapping, row.get()); + pLayout->addItem(std::move(row)); + } else if (tagName == "group") { + std::unique_ptr group = + std::make_unique( + element.attribute("label")); + parseMappingSettingsElement(element, pMapping, group.get()); + pLayout->addItem(std::move(group)); + } else { + qDebug() << "Ignoring unsupported tag" << tagName + << "in file" << pMapping->filePath() + << "on line" << element.lineNumber() + << "for controller layout settings. Check the documentation supported tags."; + continue; + } + } +} + QDomElement LegacyControllerMappingFileHandler::getControllerNode( const QDomElement& root) { if (root.isNull()) { @@ -204,7 +284,7 @@ QDomDocument LegacyControllerMappingFileHandler::buildRootWithScripts( QString blank = "\n" "\n" - "\n"; + "\n"; doc.setContent(blank); QDomElement rootNode = doc.documentElement(); diff --git a/src/controllers/legacycontrollermappingfilehandler.h b/src/controllers/legacycontrollermappingfilehandler.h index c7bc6377b6d2..73e5eea584c3 100644 --- a/src/controllers/legacycontrollermappingfilehandler.h +++ b/src/controllers/legacycontrollermappingfilehandler.h @@ -7,6 +7,7 @@ class QFileInfo; class QDir; class LegacyControllerMapping; +class LegacyControllerSettingsLayoutContainer; /// The LegacyControllerMappingFileHandler is used for serializing/deserializing the /// LegacyControllerMapping objects to/from XML files and is also responsible @@ -41,6 +42,12 @@ class LegacyControllerMappingFileHandler { void parseMappingInfo(const QDomElement& root, std::shared_ptr mapping) const; + /// @brief Parse the setting definition block from the root node if any. + /// @param root The root node (MixxxControllerPreset) + /// @param mapping The mapping object to populate with the gathered data + void parseMappingSettings(const QDomElement& root, + LegacyControllerMapping* mapping) const; + /// Adds script files from XML to the LegacyControllerMapping. /// /// This function parses the supplied QDomElement structure, finds the @@ -61,8 +68,21 @@ class LegacyControllerMappingFileHandler { bool writeDocument(const QDomDocument& root, const QString& fileName) const; private: + /// @brief Recursively parse setting definition and layout information + /// within a setting node + /// @param current The setting node (MixxxControllerPreset.settings) or any + /// children nodes + /// @param mapping The mapping object to populate with the gathered data + /// @param layout The currently active layout, on which new setting item + /// (leaf) should be attached + void parseMappingSettingsElement(const QDomElement& current, + LegacyControllerMapping* pMapping, + LegacyControllerSettingsLayoutContainer* pLayout) const; + // Sub-classes implement this. virtual std::shared_ptr load(const QDomElement& root, const QString& filePath, const QDir& systemMappingPath) = 0; + + friend class LegacyControllerMappingSettingsTest_parseSettingBlock_Test; }; diff --git a/src/controllers/legacycontrollersettings.cpp b/src/controllers/legacycontrollersettings.cpp new file mode 100644 index 000000000000..56f41fcd1f90 --- /dev/null +++ b/src/controllers/legacycontrollersettings.cpp @@ -0,0 +1,234 @@ +#include "controllers/legacycontrollersettings.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "moc_legacycontrollersettings.cpp" + +LegacyControllerSettingBuilder* LegacyControllerSettingBuilder::instance() { + static LegacyControllerSettingBuilder* s_self = nullptr; + + if (s_self == nullptr) { + s_self = new LegacyControllerSettingBuilder(); + } + + return s_self; +} + +LegacyControllerSettingBuilder::LegacyControllerSettingBuilder() { + // Each possible setting types must be added there. This will allow the + // builder to know each type of supported setting + registerType(); + registerType(); + registerType(); + registerType(); +} + +AbstractLegacyControllerSetting::AbstractLegacyControllerSetting(const QDomElement& element) { + m_variableName = element.attribute("variable").trimmed(); + m_label = element.attribute("label", m_variableName).trimmed(); + + QDomElement description = element.firstChildElement("description"); + if (!description.isNull()) { + m_description = description.text().trimmed(); + } +} + +QWidget* AbstractLegacyControllerSetting::buildWidget(QWidget* pParent, + LegacyControllerSettingsLayoutContainer::Disposition orientation) { + auto pRoot = make_parented(pParent); + QBoxLayout* pLayout = new QBoxLayout(QBoxLayout::LeftToRight); + + pLayout->setContentsMargins(0, 0, 0, 0); + + if (orientation == LegacyControllerSettingsLayoutContainer::VERTICAL) { + auto* pSettingsContainer = dynamic_cast(pParent); + if (pSettingsContainer) { + connect(pSettingsContainer, + &WLegacyControllerSettingsContainer::orientationChanged, + this, + [pLayout, pParent]( + LegacyControllerSettingsLayoutContainer::Disposition + disposition) { + pLayout->setDirection(disposition == + LegacyControllerSettingsLayoutContainer:: + HORIZONTAL + ? QBoxLayout::TopToBottom + : QBoxLayout::LeftToRight); + pParent->layout()->invalidate(); + }); + } + } + + auto pLabelWidget = make_parented(pRoot); + pLabelWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + pLabelWidget->setText(label()); + + if (!description().isEmpty()) { + pRoot->setToolTip(QString("

%1

").arg(description())); + } + + pLayout->addWidget(pLabelWidget); + pLayout->addWidget(buildInputWidget(pRoot)); + + pLayout->setStretch(0, 3); + pLayout->setStretch(1, 1); + + pRoot->setLayout(pLayout); + + return pRoot; +} + +LegacyControllerBooleanSetting::LegacyControllerBooleanSetting( + const QDomElement& element) + : AbstractLegacyControllerSetting(element) { + m_defaultValue = parseValue(element.attribute("default")); + m_savedValue = m_defaultValue; + m_editedValue = m_defaultValue; +} + +QWidget* LegacyControllerBooleanSetting::buildWidget( + QWidget* pParent, LegacyControllerSettingsLayoutContainer::Disposition) { + return buildInputWidget(pParent); +} + +QWidget* LegacyControllerBooleanSetting::buildInputWidget(QWidget* pParent) { + auto* pCheckBox = new QCheckBox(label(), pParent); + pCheckBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); + if (m_editedValue) { + pCheckBox->setCheckState(Qt::Checked); + } + + if (!description().isEmpty()) { + pCheckBox->setToolTip(QString("

%1

").arg(description())); + } + + connect(this, &AbstractLegacyControllerSetting::valueReset, pCheckBox, [this, &pCheckBox]() { + pCheckBox->setCheckState(m_editedValue ? Qt::Checked : Qt::Unchecked); + }); + + connect(pCheckBox, &QCheckBox::stateChanged, this, [this](int state) { + m_editedValue = state == Qt::Checked; + emit changed(); + }); + + return pCheckBox; +} + +bool LegacyControllerBooleanSetting::match(const QDomElement& element) { + return element.hasAttribute("type") && + QString::compare(element.attribute("type"), + "boolean", + Qt::CaseInsensitive) == 0; +} + +template ValueSerializer, + Deserializer ValueDeserializer, + class InputWidget> +QWidget* LegacyControllerNumberSetting::buildInputWidget(QWidget* pParent) { + auto* pSpinBox = new InputWidget(pParent); + pSpinBox->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); + + pSpinBox->setRange(m_minValue, m_maxValue); + pSpinBox->setSingleStep(m_stepValue); + pSpinBox->setValue(m_editedValue); + + connect(this, &AbstractLegacyControllerSetting::valueReset, pSpinBox, [this, pSpinBox]() { + pSpinBox->setValue(m_editedValue); + }); + + connect(pSpinBox, + QOverload::of(&InputWidget::valueChanged), + this, + [this](SettingType value) { + m_editedValue = value; + emit changed(); + }); + + return pSpinBox; +} + +QWidget* LegacyControllerRealSetting::buildInputWidget(QWidget* pParent) { + QDoubleSpinBox* spinBox = dynamic_cast( + LegacyControllerNumberSetting::buildInputWidget(pParent)); + VERIFY_OR_DEBUG_ASSERT(spinBox != nullptr) { + qWarning() << "Unable to set precision on the input widget " + "input. It does not appear to be a valid QDoubleSpinBox"; + return spinBox; + } + spinBox->setDecimals(m_precisionValue); + + return spinBox; +} + +LegacyControllerEnumSetting::LegacyControllerEnumSetting( + const QDomElement& element) + : AbstractLegacyControllerSetting(element), m_options(), m_defaultValue(0) { + size_t pos = 0; + for (QDomElement value = element.firstChildElement("value"); + !value.isNull(); + value = value.nextSiblingElement("value")) { + QString val = value.text(); + m_options.append(std::tuple(val, value.attribute("label", val))); + if (value.hasAttribute("default")) { + m_defaultValue = pos; + } + pos++; + } + reset(); + save(); +} + +void LegacyControllerEnumSetting::parse(const QString& in, bool* ok) { + if (ok != nullptr) { + *ok = false; + } + reset(); + save(); + + size_t pos = 0; + for (const auto& value : qAsConst(m_options)) { + if (std::get<0>(value) == in) { + if (ok != nullptr) { + *ok = true; + } + m_savedValue = pos; + m_editedValue = m_savedValue; + return; + } + pos++; + } +} + +QWidget* LegacyControllerEnumSetting::buildInputWidget(QWidget* pParent) { + auto* pComboBox = new QComboBox(pParent); + + for (const auto& value : qAsConst(m_options)) { + pComboBox->addItem(std::get<1>(value)); + } + pComboBox->setCurrentIndex(static_cast(m_editedValue)); + + connect(this, &AbstractLegacyControllerSetting::valueReset, pComboBox, [this, &pComboBox]() { + pComboBox->setCurrentIndex(static_cast(m_editedValue)); + }); + + connect(pComboBox, + QOverload::of(&QComboBox::currentIndexChanged), + this, + [this](int selected) { + m_editedValue = selected; + emit changed(); + }); + + return pComboBox; +} diff --git a/src/controllers/legacycontrollersettings.h b/src/controllers/legacycontrollersettings.h new file mode 100644 index 000000000000..559227007a61 --- /dev/null +++ b/src/controllers/legacycontrollersettings.h @@ -0,0 +1,452 @@ +#pragma once + +#include + +#include "controllers/legacycontrollersettingsfactory.h" +#include "controllers/legacycontrollersettingslayout.h" +#include "util/parented_ptr.h" + +class QSpinBox; +class QDoubleSpinBox; + +/// @brief The abstract controller setting. Any type of setting will have to +/// implement this base class +class AbstractLegacyControllerSetting : public QObject { + Q_OBJECT + public: + ~AbstractLegacyControllerSetting() override = default; + + /// @brief Build a widget that can be used to interact with this setting. It + /// shouldn't mutate the state of the setting. + /// @param parent The parent widget for which this widget is being created. + /// The parent widget will own the newly created widget + /// @return a new widget + virtual QWidget* buildWidget(QWidget* parent, + LegacyControllerSettingsLayoutContainer::Disposition orientation = + LegacyControllerSettingsLayoutContainer::HORIZONTAL); + + /// @brief Build a JSValue with the current setting value. The JSValue + /// variant will use the appropriate type + /// @return A QJSValue with the current value + virtual QJSValue value() const = 0; + + /// @brief Serialize the current value in a string format + /// @return A String with current setting value + virtual QString stringify() const = 0; + + /// @brief Parse a string that contains the value in a compatible format. + /// @param in The string containing the data + /// @param ok A pointer to a boolean in which the result of the + /// deserialisation will be stored (true means the operation was successful) + virtual void parse(const QString&, bool*) = 0; + + /// @brief Indicate if the setting is currently not using a user-specified value + /// @return Whether or not the setting is currently set to its default value + virtual bool isDefault() const = 0; + + /// @brief Indicate if the setting is currently being mutated and if the + /// edited value is different than its its currently known value. This would + /// indicate that the user may need to save the changes or acknowledge + /// otherwise. + /// @return Whether or not the setting value is dirty + virtual bool isDirty() const = 0; + + /// @brief Commit the the edited value to become the currently known value. + /// Note that if `isDirty() == false`, this have no effect + virtual void save() = 0; + + /// @brief Reset the current value, as well as the editing value to use the + /// default, as specified in the spec + virtual void reset() = 0; + + /// @brief Whether of not this setting definition is valid. Validity scope + /// includes things like default value within range' for example. + /// @return true if valid + virtual bool valid() const { + return !m_variableName.isEmpty(); + } + + /// @brief The variable name as perceived within the mapping definition. + /// @return a string + QString variableName() const { + return m_variableName; + } + + /// @brief The user-friendly label to be display in the UI + /// @return a string + const QString& label() const { + return m_label; + } + + /// @brief A description of what this setting does + /// @return a string + const QString& description() const { + return m_description; + } + + bool operator==(const AbstractLegacyControllerSetting& other) const noexcept { + return variableName() == other.variableName(); + } + + protected: + AbstractLegacyControllerSetting(const QString& variableName, + const QString& label, + const QString& description) + : m_variableName(variableName), + m_label(label), + m_description(description) { + } + AbstractLegacyControllerSetting(const QDomElement& element); + + virtual QWidget* buildInputWidget(QWidget* parent) = 0; + + signals: + /// This signal will be emitted when the user has interacted with the + /// setting and changed its value + void changed(); + /// This signal will be emitted when the user has requested a value reset + void valueReset(); + + private: + QString m_variableName; + QString m_label; + QString m_description; +}; + +class LegacyControllerBooleanSetting + : public LegacyControllerSettingFactory, + public AbstractLegacyControllerSetting { + public: + LegacyControllerBooleanSetting(const QDomElement& element); + + virtual ~LegacyControllerBooleanSetting() = default; + + QWidget* buildWidget(QWidget* parent, + LegacyControllerSettingsLayoutContainer::Disposition orientation = + LegacyControllerSettingsLayoutContainer::HORIZONTAL) + override; + + QJSValue value() const override { + return QJSValue(m_savedValue); + } + + QString stringify() const override { + return m_savedValue ? "true" : "false"; + } + void parse(const QString& in, bool* ok = nullptr) override { + if (ok != nullptr) + *ok = true; + m_savedValue = parseValue(in); + m_editedValue = m_savedValue; + } + bool isDefault() const override { + return m_savedValue == m_defaultValue; + } + bool isDirty() const override { + return m_savedValue != m_editedValue; + } + + virtual void save() override { + m_savedValue = m_editedValue; + } + + virtual void reset() override { + m_editedValue = m_defaultValue; + emit valueReset(); + } + + static AbstractLegacyControllerSetting* createFrom(const QDomElement& element) { + return new LegacyControllerBooleanSetting(element); + } + static bool match(const QDomElement& element); + + protected: + LegacyControllerBooleanSetting(const QDomElement& element, + bool currentValue, + bool defaultValue) + : AbstractLegacyControllerSetting(element), + m_savedValue(currentValue), + m_defaultValue(defaultValue) { + } + + bool parseValue(const QString& in) { + return QString::compare(in, "true", Qt::CaseInsensitive) == 0 || in == "1"; + } + + virtual QWidget* buildInputWidget(QWidget* parent) override; + + private: + bool m_savedValue; + bool m_defaultValue; + bool m_editedValue; + + friend class LegacyControllerMappingSettingsTest_booleanSettingEditing_Test; +}; + +template +using Serializer = QString (*)(const SettingType&); + +template +using Deserializer = SettingType (*)(const QString&, bool*); + +template ValueSerializer, + Deserializer ValueDeserializer, + class InputWidget> +class LegacyControllerNumberSetting + : public LegacyControllerSettingFactory< + LegacyControllerNumberSetting>, + public AbstractLegacyControllerSetting { + public: + LegacyControllerNumberSetting(const QDomElement& element) + : AbstractLegacyControllerSetting(element) { + bool isOk = false; + m_minValue = ValueDeserializer(element.attribute("min"), &isOk); + if (!isOk) { + m_minValue = std::numeric_limits::min(); + } + m_maxValue = ValueDeserializer(element.attribute("max"), &isOk); + if (!isOk) { + m_maxValue = std::numeric_limits::max(); + } + m_stepValue = ValueDeserializer(element.attribute("step"), &isOk); + if (!isOk) { + m_stepValue = 1; + } + m_defaultValue = ValueDeserializer(element.attribute("default"), &isOk); + if (!isOk) { + m_defaultValue = 0; + } + reset(); + save(); + } + + virtual ~LegacyControllerNumberSetting() = default; + + QJSValue value() const override { + return QJSValue(m_savedValue); + } + + QString stringify() const override { + return ValueSerializer(m_savedValue); + } + void parse(const QString& in, bool* ok) override { + m_savedValue = ValueDeserializer(in, ok); + m_editedValue = m_savedValue; + } + + bool isDefault() const override { + return m_savedValue == m_defaultValue; + } + bool isDirty() const override { + return m_savedValue != m_editedValue; + } + + virtual void save() override { + m_savedValue = m_editedValue; + } + + virtual void reset() override { + m_editedValue = m_defaultValue; + emit valueReset(); + } + + /// @brief Whether of not this setting definition and its current state are + /// valid. Validity scope includes default/current/dirty value within range + /// and a strictly positive step, strictly less than max.. + /// @return true if valid + bool valid() const override { + return AbstractLegacyControllerSetting::valid() && + m_defaultValue >= m_minValue && m_savedValue >= m_minValue && + m_editedValue >= m_minValue && m_defaultValue <= m_maxValue && + m_savedValue <= m_maxValue && m_editedValue <= m_maxValue && + m_stepValue > 0 && m_stepValue < m_maxValue; + } + + static AbstractLegacyControllerSetting* createFrom(const QDomElement& element) { + return new LegacyControllerNumberSetting(element); + } + static bool match(const QDomElement& element); + + protected: + LegacyControllerNumberSetting(const QDomElement& element, + SettingType currentValue, + SettingType defaultValue, + SettingType minValue, + SettingType maxValue, + SettingType stepValue) + : AbstractLegacyControllerSetting(element), + m_savedValue(currentValue), + m_defaultValue(defaultValue), + m_minValue(minValue), + m_maxValue(maxValue), + m_stepValue(stepValue) { + } + + virtual QWidget* buildInputWidget(QWidget* parent) override; + + private: + SettingType m_savedValue; + SettingType m_defaultValue; + SettingType m_minValue; + SettingType m_maxValue; + SettingType m_stepValue; + + SettingType m_editedValue; + + friend class LegacyControllerMappingSettingsTest_integerSettingEditing_Test; + friend class LegacyControllerMappingSettingsTest_doubleSettingEditing_Test; +}; + +template +inline bool matchSetting(const QDomElement& element); + +inline int extractSettingIntegerValue(const QString& str, bool* ok = nullptr) { + return str.toInt(ok); +} +inline double extractSettingDoubleValue(const QString& str, bool* ok = nullptr) { + return str.toDouble(ok); +} + +inline QString packSettingIntegerValue(const int& in) { + return QString::number(in); +} +inline QString packSettingDoubleValue(const double& in) { + return QString::number(in); +} + +using LegacyControllerIntegerSetting = LegacyControllerNumberSetting; + +class LegacyControllerRealSetting : public LegacyControllerNumberSetting { + public: + LegacyControllerRealSetting(const QDomElement& element) + : LegacyControllerNumberSetting(element) { + bool isOk = false; + m_precisionValue = element.attribute("precision").toInt(&isOk); + if (!isOk) { + m_precisionValue = 2; + } + } + + static AbstractLegacyControllerSetting* createFrom(const QDomElement& element) { + return new LegacyControllerRealSetting(element); + } + + QWidget* buildInputWidget(QWidget* parent) override; + + private: + int m_precisionValue; +}; + +class LegacyControllerEnumSetting + : public LegacyControllerSettingFactory, + public AbstractLegacyControllerSetting { + public: + LegacyControllerEnumSetting(const QDomElement& element); + + virtual ~LegacyControllerEnumSetting() = default; + + QJSValue value() const override { + return QJSValue(stringify()); + } + + QString stringify() const override { + return std::get<0>(m_options.value(static_cast(m_savedValue))); + } + void parse(const QString& in, bool* ok) override; + bool isDefault() const override { + return m_savedValue == m_defaultValue; + } + bool isDirty() const override { + return m_savedValue != m_editedValue; + } + + virtual void save() override { + m_savedValue = m_editedValue; + } + + virtual void reset() override { + m_editedValue = m_defaultValue; + emit valueReset(); + } + + /// @brief Whether or not this setting definition and its current state are + /// valid. Validity scope includes a known default/current/dirty option. + /// @return true if valid + bool valid() const override { + return AbstractLegacyControllerSetting::valid() && + static_cast(m_defaultValue) < m_options.size() && + static_cast(m_savedValue) < m_options.size() && + static_cast(m_editedValue) < m_options.size(); + } + + static AbstractLegacyControllerSetting* createFrom(const QDomElement& element) { + return new LegacyControllerEnumSetting(element); + } + static inline bool match(const QDomElement& element); + + protected: + LegacyControllerEnumSetting(const QDomElement& element, + const QList>& options, + size_t currentValue, + size_t defaultValue) + : AbstractLegacyControllerSetting(element), + m_options(options), + m_savedValue(currentValue), + m_defaultValue(defaultValue) { + } + + virtual QWidget* buildInputWidget(QWidget* parent) override; + + private: + // We use a QList instead of QHash here because we want to keep the natural order + QList> m_options; + size_t m_savedValue; + size_t m_defaultValue; + + size_t m_editedValue; + + friend class LegacyControllerMappingSettingsTest_enumSettingEditing_Test; +}; + +template<> +inline bool matchSetting(const QDomElement& element) { + return element.hasAttribute("type") && + QString::compare(element.attribute("type"), + "integer", + Qt::CaseInsensitive) == 0; +} +template<> +inline bool matchSetting(const QDomElement& element) { + return element.hasAttribute("type") && + QString::compare(element.attribute("type"), + "real", + Qt::CaseInsensitive) == 0; +} + +template ValueSerializer, + Deserializer ValueDeserializer, + class InputWidget> +inline bool LegacyControllerNumberSetting::match(const QDomElement& element) { + return matchSetting(element); +} + +inline bool LegacyControllerEnumSetting::match(const QDomElement& element) { + return element.hasAttribute("type") && + QString::compare(element.attribute("type"), + "enum", + Qt::CaseInsensitive) == 0; +} diff --git a/src/controllers/legacycontrollersettingsfactory.h b/src/controllers/legacycontrollersettingsfactory.h new file mode 100644 index 000000000000..ae5488f59c81 --- /dev/null +++ b/src/controllers/legacycontrollersettingsfactory.h @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include "controllers/legacycontrollersettingsfactory.h" + +class AbstractLegacyControllerSetting; + +/// @brief This class defines an interface that a controller setting type must +/// implement so it can be used properly by the builder +/// @tparam T The class implementing this interface +template +class LegacyControllerSettingFactory { + inline static LegacyControllerSettingFactory* createFrom(const QDomElement& element) { + return new T(element); + } + inline static bool match(const QDomElement& element) { + return T::match(element); + } +}; + +/// @brief This class is used to dynamically instantiate a controller setting based on its type +class LegacyControllerSettingBuilder { + public: + static LegacyControllerSettingBuilder* instance(); + + /// @brief Register a new type of setting. This method is used by the + /// REGISTER macro, it shouldn't be used directly + /// @param match the match function of the new setting + /// @param creator the creator function of the new setting + /// @return Always true + template + bool registerType() { + m_supportedSettings.append(SupportedSetting{ + &T::match, + &T::createFrom}); + return true; + } + + /// @brief instantiate a new setting from a an XML definition if any valid + /// setting was found. The caller is the owner of the instance + /// @param element The XML element to parse to build the new setting + /// @return an instance if a a supported setting has been found, null + /// otherwise + static AbstractLegacyControllerSetting* build(const QDomElement& element) { + for (const auto& settingType : qAsConst(instance()->m_supportedSettings)) { + if (settingType.matcher(element)) { + return settingType.builder(element); + } + } + + return nullptr; + } + + private: + struct SupportedSetting { + bool (*matcher)(const QDomElement&); + AbstractLegacyControllerSetting* (*builder)(const QDomElement&); + }; + + LegacyControllerSettingBuilder(); + + QList + m_supportedSettings; +}; diff --git a/src/controllers/legacycontrollersettingslayout.cpp b/src/controllers/legacycontrollersettingslayout.cpp new file mode 100644 index 000000000000..483e765ba1f9 --- /dev/null +++ b/src/controllers/legacycontrollersettingslayout.cpp @@ -0,0 +1,95 @@ + +#include "controllers/legacycontrollersettingslayout.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "controllers/legacycontrollersettings.h" +#include "moc_legacycontrollersettingslayout.cpp" +#include "util/parented_ptr.h" + +namespace { +constexpr int kMinScreenSizeForControllerSettingRow = 960; +} // anonymous namespace + +void LegacyControllerSettingsLayoutContainer::addItem( + std::shared_ptr setting) { + m_elements.push_back(std::make_unique( + setting, m_widgetOrientation)); +} + +QBoxLayout* LegacyControllerSettingsLayoutContainer::buildLayout(QWidget* pParent) const { + auto pLayout = make_parented(QBoxLayout::TopToBottom); + + pParent->setLayout(pLayout); + + return pLayout; +} + +QWidget* LegacyControllerSettingsLayoutContainer::build(QWidget* pParent) { + auto pContainer = make_parented(m_disposition, pParent); + QBoxLayout* pLayout = buildLayout(pContainer); + + pLayout->setContentsMargins(0, 0, 0, 0); + + auto& lastElement = m_elements.back(); + for (auto& element : m_elements) { + auto* pWidget = element->build(pContainer); + pWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + pLayout->addWidget(pWidget); + if (element != lastElement) { + pLayout->addItem(new QSpacerItem( + 10, 10, QSizePolicy::Expanding, QSizePolicy::Fixed)); + } + } + + return pContainer; +} + +QWidget* LegacyControllerSettingsGroup::build(QWidget* pParent) { + auto pContainer = make_parented(m_label, pParent); + QBoxLayout* pLayout = buildLayout(pContainer); + + for (auto& element : m_elements) { + pLayout->addWidget(element->build(pContainer)); + } + + return pContainer; +} + +QWidget* LegacyControllerSettingsLayoutItem::build(QWidget* parent) { + VERIFY_OR_DEBUG_ASSERT(m_setting.get() != nullptr) { + return nullptr; + } + return m_setting->buildWidget(parent, m_preferredOrientation); +} + +void WLegacyControllerSettingsContainer::resizeEvent(QResizeEvent* event) { + if (m_preferredOrientation == LegacyControllerSettingsLayoutContainer::VERTICAL) { + return; + } + + auto* pLayout = dynamic_cast(layout()); + if (pLayout == nullptr) { + return; + } + + if (event->size().width() < kMinScreenSizeForControllerSettingRow && + pLayout->direction() == QBoxLayout::LeftToRight) { + pLayout->setDirection(QBoxLayout::TopToBottom); + pLayout->setSpacing(6); + emit orientationChanged(LegacyControllerSettingsLayoutContainer::VERTICAL); + } else if (event->size().width() >= + kMinScreenSizeForControllerSettingRow && + pLayout->direction() == QBoxLayout::TopToBottom) { + pLayout->setDirection(QBoxLayout::LeftToRight); + pLayout->setSpacing(16); + emit orientationChanged(LegacyControllerSettingsLayoutContainer::HORIZONTAL); + } +} diff --git a/src/controllers/legacycontrollersettingslayout.h b/src/controllers/legacycontrollersettingslayout.h new file mode 100644 index 000000000000..f9b07fb40dc0 --- /dev/null +++ b/src/controllers/legacycontrollersettingslayout.h @@ -0,0 +1,133 @@ +#pragma once + +#include "defs_urls.h" +#include "preferences/usersettings.h" + +class AbstractLegacyControllerSetting; +class QBoxLayout; + +/// @brief Layout information used for controller setting when rendered in the Preference Dialog +class LegacyControllerSettingsLayoutElement { + public: + LegacyControllerSettingsLayoutElement() { + } + virtual ~LegacyControllerSettingsLayoutElement() = default; + + virtual std::unique_ptr clone() const = 0; + + virtual QWidget* build(QWidget* parent) = 0; +}; + +/// @brief This layout element can hold others element. It is also the one used +/// to represent a `row` in the settings +class LegacyControllerSettingsLayoutContainer : public LegacyControllerSettingsLayoutElement { + public: + /// @brief This is a simplified representation of disposition orientation. This used to + /// define how a container orients its children. This is also used by layout + /// items to decide how the label should be rendered alongside the input + /// widget + enum Disposition { + HORIZONTAL = 0, + VERTICAL, + }; + + LegacyControllerSettingsLayoutContainer( + Disposition disposition = HORIZONTAL, + Disposition widgetOrientation = HORIZONTAL) + : LegacyControllerSettingsLayoutElement(), + m_disposition(disposition), + m_widgetOrientation(widgetOrientation) { + } + LegacyControllerSettingsLayoutContainer(const LegacyControllerSettingsLayoutContainer& other) { + m_elements.reserve(other.m_elements.size()); + for (const auto& e : other.m_elements) + m_elements.push_back(e->clone()); + } + virtual ~LegacyControllerSettingsLayoutContainer() = default; + + virtual std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + /// @brief This helper method allows to add a LegacyControllerSetting + /// directly, without to have it to wrap within an item object. This is + /// helpful as the item that will be create to wrap will be initialised with + /// the right parameters + /// @param setting The controller setting to add to the layout container + void addItem(std::shared_ptr setting); + void addItem(std::unique_ptr&& container) { + m_elements.push_back(std::move(container)); + } + + virtual QWidget* build(QWidget* parent) override; + + protected: + QBoxLayout* buildLayout(QWidget* parent) const; + + Disposition m_disposition; + Disposition m_widgetOrientation; + std::vector> m_elements; +}; + +class LegacyControllerSettingsGroup : public LegacyControllerSettingsLayoutContainer { + public: + LegacyControllerSettingsGroup(const QString& label, + LegacyControllerSettingsLayoutContainer::Disposition disposition = + VERTICAL) + : LegacyControllerSettingsLayoutContainer(disposition), + m_label(label) { + } + virtual ~LegacyControllerSettingsGroup() = default; + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + QWidget* build(QWidget* parent) override; + + private: + QString m_label; +}; + +class LegacyControllerSettingsLayoutItem : public LegacyControllerSettingsLayoutElement { + public: + LegacyControllerSettingsLayoutItem( + std::shared_ptr setting, + LegacyControllerSettingsLayoutContainer::Disposition orientation = + LegacyControllerSettingsGroup::HORIZONTAL) + : LegacyControllerSettingsLayoutElement(), + m_setting(setting), + m_preferredOrientation(orientation) { + } + virtual ~LegacyControllerSettingsLayoutItem() = default; + + std::unique_ptr clone() const override { + return std::make_unique(*this); + } + + QWidget* build(QWidget* parent) override; + + private: + std::shared_ptr m_setting; + LegacyControllerSettingsLayoutContainer::Disposition m_preferredOrientation; +}; + +class WLegacyControllerSettingsContainer : public QWidget { + Q_OBJECT + public: + WLegacyControllerSettingsContainer( + LegacyControllerSettingsLayoutContainer::Disposition + preferredOrientation, + QWidget* parent) + : QWidget(parent), m_preferredOrientation(preferredOrientation) { + } + + protected: + void resizeEvent(QResizeEvent* event); + + signals: + void orientationChanged(LegacyControllerSettingsLayoutContainer::Disposition); + + private: + LegacyControllerSettingsLayoutContainer::Disposition m_preferredOrientation; +}; diff --git a/src/controllers/midi/legacymidicontrollermappingfilehandler.cpp b/src/controllers/midi/legacymidicontrollermappingfilehandler.cpp index e0fcea473cf3..4735a6a32553 100644 --- a/src/controllers/midi/legacymidicontrollermappingfilehandler.cpp +++ b/src/controllers/midi/legacymidicontrollermappingfilehandler.cpp @@ -11,6 +11,7 @@ std::shared_ptr LegacyMidiControllerMappingFileHandler::load(const QDomElement& root, const QString& filePath, const QDir& systemMappingsPath) { + // TODO (XXX): support for controller settings if (root.isNull()) { return nullptr; } diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp index a5886204ec61..faad865b5c59 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp @@ -99,6 +99,18 @@ void ControllerScriptEngineLegacy::setScriptFiles( m_scriptFiles = scripts; } +void ControllerScriptEngineLegacy::setSettings( + const QList>& settings) { + m_settings.clear(); + for (const auto& pSetting : qAsConst(settings)) { + QString name = pSetting->variableName(); + VERIFY_OR_DEBUG_ASSERT(!name.isEmpty()) { + continue; + } + m_settings[name] = pSetting->value(); + } +} + bool ControllerScriptEngineLegacy::initialize() { if (!ControllerScriptEngineBase::initialize()) { return false; @@ -123,6 +135,7 @@ bool ControllerScriptEngineLegacy::initialize() { QJSValue engineGlobalObject = m_pJSEngine->globalObject(); ControllerScriptInterfaceLegacy* legacyScriptInterface = new ControllerScriptInterfaceLegacy(this, m_logger); + engineGlobalObject.setProperty( "engine", m_pJSEngine->newQObject(legacyScriptInterface)); diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h index 70dea6a392e0..9bf379dc0cf4 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.h @@ -29,7 +29,21 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { public slots: void setScriptFiles(const QList& scripts); + /// @brief Set the list of customizable settings and their currently set + /// value, ready to be used. This method will generate a JSValue from their + /// current state, meaning that any later mutation won't be used, and this + /// method should be called again + /// @param settings The list of settings in a valid state (initialized and + /// restored) + void setSettings( + const QList>& settings); + private: + struct Setting { + QString name; + QJSValue value; + }; + bool evaluateScriptFile(const QFileInfo& scriptFile); void shutdown() override; @@ -44,6 +58,7 @@ class ControllerScriptEngineLegacy : public ControllerScriptEngineBase { QList m_incomingDataFunctions; QHash m_scriptWrappedFunctionCache; QList m_scriptFiles; + QHash m_settings; QFileSystemWatcher m_fileWatcher; diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp index 33a60e3cae05..3ee8d09a280b 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp @@ -108,6 +108,29 @@ ControlObjectScript* ControllerScriptInterfaceLegacy::getControlObjectScript( return coScript; } +QJSValue ControllerScriptInterfaceLegacy::getSetting(const QString& name) { + VERIFY_OR_DEBUG_ASSERT(m_pScriptEngineLegacy) { + return QJSValue::UndefinedValue; + } + if (name.isEmpty()) { + m_pScriptEngineLegacy->logOrThrowError( + QStringLiteral("getSetting called with empty name " + "string, returning undefined") + .arg(name)); + return QJSValue::UndefinedValue; + } + + const auto it = m_pScriptEngineLegacy->m_settings.constFind(name); + if (it != m_pScriptEngineLegacy->m_settings.constEnd()) { + return it.value(); + } else { + m_pScriptEngineLegacy->logOrThrowError( + QStringLiteral("Unknown controllerSetting (%1) returning undefined") + .arg(name)); + return QJSValue::UndefinedValue; + } +} + double ControllerScriptInterfaceLegacy::getValue(const QString& group, const QString& name) { ControlObjectScript* coScript = getControlObjectScript(group, name); if (coScript == nullptr) { diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h index be8589f4d1a6..4d3e280a6ffc 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h @@ -22,6 +22,7 @@ class ControllerScriptInterfaceLegacy : public QObject { virtual ~ControllerScriptInterfaceLegacy(); + Q_INVOKABLE QJSValue getSetting(const QString& name); Q_INVOKABLE double getValue(const QString& group, const QString& name); Q_INVOKABLE void setValue(const QString& group, const QString& name, double newValue); Q_INVOKABLE double getParameter(const QString& group, const QString& name); diff --git a/src/test/controller_mapping_settings_test.cpp b/src/test/controller_mapping_settings_test.cpp new file mode 100644 index 000000000000..3719fcefaf4e --- /dev/null +++ b/src/test/controller_mapping_settings_test.cpp @@ -0,0 +1,513 @@ + +#include + +#include + +#include "controllers/legacycontrollermapping.h" +#include "controllers/legacycontrollermappingfilehandler.h" +#include "controllers/legacycontrollersettings.h" +#include "test/mixxxtest.h" + +class LegacyControllerMappingSettingsTest : public MixxxTest { +}; + +const char* const kValidBoolean = + ""; + +const char* const kValidInteger = + ""; + +// This setting has purposfully no custom "label" and description +const char* const kValidDouble = + ""; +const char* const kValidEnumOption = "%2"; + +TEST_F(LegacyControllerMappingSettingsTest, booleanSettingParsing) { + QDomDocument doc; + doc.setContent(QString(kValidBoolean).arg("false").toLatin1()); + + EXPECT_TRUE(LegacyControllerBooleanSetting::match(doc.documentElement())); + LegacyControllerBooleanSetting* setting = (LegacyControllerBooleanSetting*) + LegacyControllerBooleanSetting::createFrom(doc.documentElement()); + EXPECT_TRUE(setting->valid()) << "Unable to create a boolean setting"; + + EXPECT_EQ(setting->variableName(), "myToggle1"); + EXPECT_EQ(setting->label(), "Test label"); + EXPECT_EQ(setting->description(), "Test description"); + + EXPECT_FALSE(setting->isDirty()); + EXPECT_TRUE(setting->isDefault()); + EXPECT_EQ(setting->stringify(), "false"); + EXPECT_TRUE(setting->valid()); + + delete setting; + + doc.setContent(QString(kValidBoolean).arg("true").toLatin1()); + + setting = (LegacyControllerBooleanSetting*) + LegacyControllerBooleanSetting::createFrom(doc.documentElement()); + EXPECT_TRUE(setting->valid()) << "Unable to create a boolean setting"; + + EXPECT_EQ(setting->stringify(), "true"); + + delete setting; +} + +TEST_F(LegacyControllerMappingSettingsTest, booleanSettingEditing) { + QDomDocument doc; + doc.setContent( + QByteArray("