diff --git a/CMakeLists.txt b/CMakeLists.txt
index 70d4954448f1..2530efeff706 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2075,6 +2075,7 @@ add_executable(mixxx-test
src/test/configobject_test.cpp
src/test/controller_mapping_validation_test.cpp
src/test/controller_mapping_settings_test.cpp
+ src/test/controllers/controller_columnid_regression_test.cpp
src/test/controllerscriptenginelegacy_test.cpp
src/test/controlobjecttest.cpp
src/test/controlobjectaliastest.cpp
diff --git a/res/controllers/Traktor Kontrol S4 MK3.hid.xml b/res/controllers/Traktor Kontrol S4 MK3.hid.xml
index 02b9b5b93639..0c6f835c9723 100644
--- a/res/controllers/Traktor Kontrol S4 MK3.hid.xml
+++ b/res/controllers/Traktor Kontrol S4 MK3.hid.xml
@@ -9,6 +9,620 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Define the sensitivity factor when the jogwheel is used to move the in and out point of a loop using the Loop Mode.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/controllers/Traktor-Kontrol-S4-MK3.js b/res/controllers/Traktor-Kontrol-S4-MK3.js
index 857ff9ff35db..7f167085065b 100644
--- a/res/controllers/Traktor-Kontrol-S4-MK3.js
+++ b/res/controllers/Traktor-Kontrol-S4-MK3.js
@@ -40,95 +40,102 @@ const KeyboardColors = [
/*
* USER CONFIGURABLE SETTINGS
- * Adjust these to your liking
+ * Change settings in the preferences
*/
const DeckColors = [
- LedColors.red,
- LedColors.blue,
- LedColors.yellow,
- LedColors.purple,
+ LedColors[engine.getSetting("deckA")] || LedColors.red,
+ LedColors[engine.getSetting("deckB")] || LedColors.blue,
+ LedColors[engine.getSetting("deckC")] || LedColors.yellow,
+ LedColors[engine.getSetting("deckD")] || LedColors.purple,
];
const LibrarySortableColumns = [
- script.LIBRARY_COLUMNS.ARTIST,
- script.LIBRARY_COLUMNS.TITLE,
- script.LIBRARY_COLUMNS.BPM,
- script.LIBRARY_COLUMNS.KEY,
- script.LIBRARY_COLUMNS.DATETIME_ADDED,
-];
+ engine.getSetting("librarySortableColumns1Value"),
+ engine.getSetting("librarySortableColumns2Value"),
+ engine.getSetting("librarySortableColumns3Value"),
+ engine.getSetting("librarySortableColumns4Value"),
+ engine.getSetting("librarySortableColumns5Value"),
+ engine.getSetting("librarySortableColumns6Value"),
+].map(c => parseInt(c)).filter(c => c); // Filter '0' column, equivalent to '---' value in the UI or disabled
-const LoopWheelMoveFactor = 50;
-const LoopEncoderMoveFactor = 500;
-const LoopEncoderShiftmoveFactor = 2500;
+const LoopWheelMoveFactor = engine.getSetting("loopWheelMoveFactor") || 50;
+const LoopEncoderMoveFactor = engine.getSetting("loopEncoderMoveFactor") || 500;
+const LoopEncoderShiftMoveFactor = engine.getSetting("loopEncoderShiftMoveFactor") || 2500;
-const TempoFaderSoftTakeoverColorLow = LedColors.white;
-const TempoFaderSoftTakeoverColorHigh = LedColors.green;
+const TempoFaderSoftTakeoverColorLow = LedColors[engine.getSetting("tempoFaderSoftTakeoverColorLow")] || LedColors.white;
+const TempoFaderSoftTakeoverColorHigh = LedColors[engine.getSetting("tempoFaderSoftTakeoverColorHigh")] || LedColors.green;
// Define whether or not to keep LED that have only one color (reverse, flux, play, shift) dimmed if they are inactive.
// 'true' will keep them dimmed, 'false' will turn them off. Default: true
-const KeepLEDWithOneColorDimedWhenInactive = true;
+const InactiveLightsAlwaysBacklit = !!engine.getSetting("inactiveLightsAlwaysBacklit");
// Keep both deck select buttons backlit and do not fully turn off the inactive deck button.
-// 'true' will keep the unseclected deck dimmed, 'false' to fully turn it off. Default: true
-const KeepDeckSelectDimmed = true;
+// 'true' will keep the unselected deck dimmed, 'false' to fully turn it off. Default: true
+const DeckSelectAlwaysBacklit = !!engine.getSetting("deckSelectAlwaysBacklit");
// Define whether the keylock is mapped when doing "shift+master" (on press) or "shift+sync" (on release since long push copies the key)".
// 'true' will use "sync+master", 'false' will use "shift+sync". Default: false
-const UseKeylockOnMaster = false;
+const UseKeylockOnMaster = !!engine.getSetting("useKeylockOnMaster");
-// Define whether the grid button would blink when the playback is going over a detcted beat. Can help to adjust beat grid.
+// Define whether the grid button would blink when the playback is going over a detected beat. Can help to adjust beat grid.
// Default: false
-const GridButtonBlinkOverBeat = false;
+const GridButtonBlinkOverBeat = !!engine.getSetting("gridButtonBlinkOverBeat");
// Wheel led blinking if reaching the end of track warning (default 30 seconds, can be changed in the settings, under "Waveforms" > "End of track warning").
// Default: true
-const WheelLedBlinkOnTrackEnd = true;
+const WheelLedBlinkOnTrackEnd = !!engine.getSetting("wheelLedBlinkOnTrackEnd");
// When shifting either decks, the mixer will control microphones or auxiliary lines. If there is both a mic and an configure on the same channel, the mixer will control the auxiliary.
// Default: false
-const MixerControlsMixAuxOnShift = false;
+const MixerControlsMixAuxOnShift = !!engine.getSetting("mixerControlsMicAuxOnShift");
// Define how many wheel moves are sampled to compute the speed. The more you have, the more the speed is accurate, but the
// less responsive it gets in Mixxx. Default: 5
-const WheelSpeedSample = 3;
+const WheelSpeedSample = engine.getSetting("wheelSpeedSample") || 5;
// Make the sampler tab a beatlooproll tab instead
// Default: false
-const UseBeatloopRollInsteadOfSampler = false;
+const UseBeatloopRollInsteadOfSampler = !!engine.getSetting("useBeatloopRollInsteadOfSampler");
// Predefined beatlooproll sizes. Note that if you use AddLoopHalveAndDoubleOnBeatloopRollTab, the first and
// last size will be ignored
-const BeatLoopRolls = [1/16, 1/8, 1/4, 1/2, 1, 2, 4, 8];
+const BeatLoopRolls = [
+ engine.getSetting("beatLoopRollsSize1") || 1/8,
+ engine.getSetting("beatLoopRollsSize2") || 1/4,
+ engine.getSetting("beatLoopRollsSize3") || 1/2,
+ engine.getSetting("beatLoopRollsSize4") || 1,
+ engine.getSetting("beatLoopRollsSize5") || 2,
+ engine.getSetting("beatLoopRollsSize6") || 4,
+ engine.getSetting("beatLoopRollsSize7") || "half",
+ engine.getSetting("beatLoopRollsSize8") || "double"
+];
-// Make the two last button on the beatlooproll pad halve or double the loop size. This will take away the 1/16 and 8 loop size.
-// Default: true
-const AddLoopHalveAndDoubleOnBeatloopRollTab = true;
// Define the speed of the jogwheel. This will impact the speed of the LED playback indicator, the sratch, and the speed of
// the motor if enable. Recommended value are 33 + 1/3 or 45.
// Default: 33 + 1/3
-const BaseRevolutionsPerMinute = 33 + 1/3;
+const BaseRevolutionsPerMinute = engine.getSetting("baseRevolutionsPerMinute") || 33 + 1/3;
// Define whether or not to use motors.
// This is a BETA feature! Please use at your own risk. Setting this off means that below settings are inactive
// Default: false
-const UseMotors = false;
+const UseMotors = !!engine.getSetting("useMotors");
// Define how many wheel moves are sampled to compute the speed when using the motor. This is helpful to mitigate delay that
// occurs in communication as well as Mixxx limitation to 20ms latency.
// The more you have, the more the speed is accurate.
// less responsive it gets in Mixxx. Default: 20
-const TurnTableSpeedSample = 20;
+const TurnTableSpeedSample = engine.getSetting("turnTableSpeedSample") || 20;
// Define how much the wheel will resist. It is a similar setting that the Grid+Wheel in Tracktor
// Value must defined between 0 to 1. 0 is very tight, 1 is very loose.
// Default: 0.5
-const TightnessFactor = 0.5;
+const TightnessFactor = engine.getSetting("tightnessFactor") || 0.5;
// Define how much force can the motor use. This defines how much the wheel will "fight" you when you block it in TT mode
// This will also affect how quick the wheel starts spinning when enabling motor mode, or starting a deck with motor mode on
-const MaxWheelForce = 25000; // Traktor seems to cap the max value at 60000, which just sounds insane
+const MaxWheelForce = engine.getSetting("maxWheelForce") || 25000; // Traktor seems to cap the max value at 60000, which just sounds insane
@@ -699,7 +706,7 @@ class HotcueButton extends PushButton {
if (this.number === undefined || !Number.isInteger(this.number) || this.number < 1 || this.number > 32) {
throw Error("HotcueButton must have a number property of an integer between 1 and 32");
}
- this.outKey = `hotcue_${this.number}_enabled`;
+ this.outKey = `hotcue_${this.number}_status`;
this.colorKey = `hotcue_${this.number}_color`;
this.outConnect();
}
@@ -815,8 +822,16 @@ class BeatLoopRollButton extends TriggerButton {
if (options.number === undefined || !Number.isInteger(options.number) || options.number < 0 || options.number > 7) {
throw Error("BeatLoopRollButton must have a number property of an integer between 0 and 7");
}
- if (options.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) {
- options.key = "beatlooproll_"+BeatLoopRolls[AddLoopHalveAndDoubleOnBeatloopRollTab ? options.number + 1 : options.number]+"_activate";
+ if (BeatLoopRolls[options.number] === "half") {
+ options.key = "loop_halve";
+ } else if (BeatLoopRolls[options.number] === "double") {
+ options.key = "loop_double";
+ } else {
+ const size = parseFloat(BeatLoopRolls[options.number]);
+ if (isNaN(size)) {
+ throw Error(`BeatLoopRollButton ${options.number}'s size "${BeatLoopRolls[options.number]}" is invalid. Must be a float, or the literal 'half' or 'double'`);
+ }
+ options.key = `beatlooproll_${size}_activate`;
options.onShortPress = function() {
if (!this.deck.beatloopSize) {
this.deck.beatloopSize = engine.getValue(this.group, "beatloop_size");
@@ -830,10 +845,6 @@ class BeatLoopRollButton extends TriggerButton {
this.deck.beatloopSize = undefined;
}
};
- } else if (options.number === 6) {
- options.key = "loop_halve";
- } else {
- options.key = "loop_double";
}
super(options);
if (this.deck === undefined) {
@@ -843,7 +854,7 @@ class BeatLoopRollButton extends TriggerButton {
this.outConnect();
}
output(value) {
- if (this.number <= 5 || !AddLoopHalveAndDoubleOnBeatloopRollTab) {
+ if (this.key.startsWith("beatlooproll_")) {
this.send(LedColors.white + (value ? this.brightnessOn : this.brightnessOff));
} else {
this.send(this.color);
@@ -1522,7 +1533,7 @@ class S4Mk3Deck extends Deck {
super(decks, colors);
this.playButton = new PlayButton({
- output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput
+ output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput
});
this.cueButton = new CueButton({
@@ -1624,7 +1635,7 @@ class S4Mk3Deck extends Deck {
shift: function() {
this.setKey("loop_enabled");
},
- output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput,
+ output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput,
onShortRelease: function() {
if (!this.shifted) {
engine.setValue(this.group, this.key, false);
@@ -1708,7 +1719,7 @@ class S4Mk3Deck extends Deck {
}
}
},
- output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput,
+ output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput,
onShortRelease: function() {
if (!this.shifted) {
engine.setValue(this.group, this.key, false);
@@ -1820,7 +1831,7 @@ class S4Mk3Deck extends Deck {
this.deck.switchDeck(Deck.groupForNumber(decks[0]));
this.outReport.data[io.deckButtonOutputByteOffset] = colors[0] + this.brightnessOn;
// turn off the other deck selection button's LED
- this.outReport.data[io.deckButtonOutputByteOffset + 1] = KeepDeckSelectDimmed ? colors[1] + this.brightnessOff : 0;
+ this.outReport.data[io.deckButtonOutputByteOffset + 1] = DeckSelectAlwaysBacklit ? colors[1] + this.brightnessOff : 0;
this.outReport.send();
}
},
@@ -1831,7 +1842,7 @@ class S4Mk3Deck extends Deck {
if (value) {
this.deck.switchDeck(Deck.groupForNumber(decks[1]));
// turn off the other deck selection button's LED
- this.outReport.data[io.deckButtonOutputByteOffset] = KeepDeckSelectDimmed ? colors[0] + this.brightnessOff : 0;
+ this.outReport.data[io.deckButtonOutputByteOffset] = DeckSelectAlwaysBacklit ? colors[0] + this.brightnessOff : 0;
this.outReport.data[io.deckButtonOutputByteOffset + 1] = colors[1] + this.brightnessOn;
this.outReport.send();
}
@@ -1840,12 +1851,12 @@ class S4Mk3Deck extends Deck {
// set deck selection button LEDs
outReport.data[io.deckButtonOutputByteOffset] = colors[0] + Button.prototype.brightnessOn;
- outReport.data[io.deckButtonOutputByteOffset + 1] = KeepDeckSelectDimmed ? colors[1] + Button.prototype.brightnessOff : 0;
+ outReport.data[io.deckButtonOutputByteOffset + 1] = DeckSelectAlwaysBacklit ? colors[1] + Button.prototype.brightnessOff : 0;
outReport.send();
this.shiftButton = new PushButton({
deck: this,
- output: KeepLEDWithOneColorDimedWhenInactive ? undefined : Button.prototype.uncoloredOutput,
+ output: InactiveLightsAlwaysBacklit ? undefined : Button.prototype.uncoloredOutput,
unshift: function() {
this.output(false);
},
@@ -1924,7 +1935,7 @@ class S4Mk3Deck extends Deck {
deck: this,
onChange: function(right) {
if (this.deck.wheelMode === wheelModes.loopIn || this.deck.wheelMode === wheelModes.loopOut) {
- const moveFactor = this.shifted ? LoopEncoderShiftmoveFactor : LoopEncoderMoveFactor;
+ const moveFactor = this.shifted ? LoopEncoderShiftMoveFactor : LoopEncoderMoveFactor;
const valueIn = engine.getValue(this.group, "loop_start_position") + (right ? moveFactor : -moveFactor);
const valueOut = engine.getValue(this.group, "loop_end_position") + (right ? moveFactor : -moveFactor);
engine.setValue(this.group, "loop_start_position", valueIn);
diff --git a/src/controllers/legacycontrollersettings.cpp b/src/controllers/legacycontrollersettings.cpp
index 56f41fcd1f90..bf449a7a8495 100644
--- a/src/controllers/legacycontrollersettings.cpp
+++ b/src/controllers/legacycontrollersettings.cpp
@@ -99,7 +99,9 @@ QWidget* LegacyControllerBooleanSetting::buildWidget(
}
QWidget* LegacyControllerBooleanSetting::buildInputWidget(QWidget* pParent) {
- auto* pCheckBox = new QCheckBox(label(), pParent);
+ auto pWidget = make_parented(pParent);
+
+ auto* pCheckBox = new QCheckBox(pWidget);
pCheckBox->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
if (m_editedValue) {
pCheckBox->setCheckState(Qt::Checked);
@@ -118,7 +120,21 @@ QWidget* LegacyControllerBooleanSetting::buildInputWidget(QWidget* pParent) {
emit changed();
});
- return pCheckBox;
+ auto pLabelWidget = make_parented(pWidget);
+ pLabelWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
+ pLabelWidget->setText(label());
+
+ QBoxLayout* pLayout = new QHBoxLayout();
+
+ pLayout->addWidget(pCheckBox);
+ pLayout->addWidget(pLabelWidget);
+
+ pLayout->setStretch(0, 3);
+ pLayout->setStretch(1, 1);
+
+ pWidget->setLayout(pLayout);
+
+ return pWidget;
}
bool LegacyControllerBooleanSetting::match(const QDomElement& element) {
diff --git a/src/controllers/legacycontrollersettings.h b/src/controllers/legacycontrollersettings.h
index 559227007a61..b7d2f378d9c7 100644
--- a/src/controllers/legacycontrollersettings.h
+++ b/src/controllers/legacycontrollersettings.h
@@ -359,6 +359,10 @@ class LegacyControllerEnumSetting
return QJSValue(stringify());
}
+ const QList>& options() const {
+ return m_options;
+ }
+
QString stringify() const override {
return std::get<0>(m_options.value(static_cast(m_savedValue)));
}
@@ -416,6 +420,7 @@ class LegacyControllerEnumSetting
size_t m_editedValue;
friend class LegacyControllerMappingSettingsTest_enumSettingEditing_Test;
+ friend class ControllerS4MK3SettingTest_ensureLibrarySettingValueAndEnumEquals;
};
template<>
diff --git a/src/test/controllers/controller_columnid_regression_test.cpp b/src/test/controllers/controller_columnid_regression_test.cpp
new file mode 100644
index 000000000000..81f3379a4fdf
--- /dev/null
+++ b/src/test/controllers/controller_columnid_regression_test.cpp
@@ -0,0 +1,79 @@
+/*
+ This test case is used to ensure that hardcoded CO value in the the settings
+ definition matches with Mixxx value and will help detecting regression if they
+ are ever updated.
+
+ Currently, the S4 MK3 is referencing library column ID in its setting, so this
+ test ensure that the value always matches with the Mixxx spec. New controllers
+ can be added by duplicated the `ensureS4MK3` case and adapt as needed
+*/
+#include "controllers/legacycontrollermapping.h"
+#include "controllers/legacycontrollermappingfilehandler.h"
+#include "library/trackmodel.h"
+#include "test/mixxxtest.h"
+#include "util/time.h"
+
+class ControllerLibraryColumnIDRegressionTest : public MixxxTest {
+ protected:
+ void SetUp() override {
+ mixxx::Time::setTestMode(true);
+ mixxx::Time::setTestElapsedTime(mixxx::Duration::fromMillis(10));
+ }
+
+ void TearDown() override {
+ mixxx::Time::setTestMode(false);
+ }
+
+ static QHash COLUMN_MAPPING;
+};
+
+QHash
+ ControllerLibraryColumnIDRegressionTest::COLUMN_MAPPING = {
+ {"Artist", TrackModel::SortColumnId::Artist},
+ {"Title", TrackModel::SortColumnId::Title},
+ {"Album", TrackModel::SortColumnId::Album},
+ {"Album Artist", TrackModel::SortColumnId::AlbumArtist},
+ {"Year", TrackModel::SortColumnId::Year},
+ {"Genre", TrackModel::SortColumnId::Genre},
+ {"Composer", TrackModel::SortColumnId::Composer},
+ {"Grouping", TrackModel::SortColumnId::Grouping},
+ {"Track Number", TrackModel::SortColumnId::TrackNumber},
+ {"File Type", TrackModel::SortColumnId::FileType},
+ {"Native Location", TrackModel::SortColumnId::NativeLocation},
+ {"Comment", TrackModel::SortColumnId::Comment},
+ {"Duration", TrackModel::SortColumnId::Duration},
+ {"Bitrate", TrackModel::SortColumnId::BitRate},
+ {"BPM", TrackModel::SortColumnId::Bpm},
+ {"Replay Gain", TrackModel::SortColumnId::ReplayGain},
+ {"Datetime Added", TrackModel::SortColumnId::DateTimeAdded},
+ {"Times Played", TrackModel::SortColumnId::TimesPlayed},
+ {"Rating", TrackModel::SortColumnId::Rating},
+ {"Key", TrackModel::SortColumnId::Key},
+ // More mapping can be added here if needed.
+ // NOTE: If some of the missing value are referenced in a
+ // controller setting, test case will fail.
+};
+
+TEST_F(ControllerLibraryColumnIDRegressionTest, ensureS4MK3) {
+ std::shared_ptr pMapping =
+ LegacyControllerMappingFileHandler::loadMapping(
+ QFileInfo("res/controllers/Traktor Kontrol S4 MK3.hid.xml"), QDir());
+ EXPECT_TRUE(pMapping);
+ auto settings = pMapping->getSettings();
+ EXPECT_TRUE(!settings.isEmpty());
+
+ const int expectedSettingCount = 6; // Number of settings using library count.
+ int count = 0;
+ for (const auto& setting : settings) {
+ if (!setting->variableName().startsWith("librarySortableColumns")) {
+ continue;
+ }
+ auto pEnum = std::dynamic_pointer_cast(setting);
+ EXPECT_TRUE(pEnum);
+ for (const auto& opt : pEnum->options()) {
+ EXPECT_EQ(static_cast(COLUMN_MAPPING[std::get<0>(opt)]), std::get<1>(opt).toInt());
+ }
+ count++;
+ }
+ EXPECT_EQ(count, expectedSettingCount);
+}