Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3973,6 +3973,7 @@ if(QML)
src/qml/qmlconfigproxybase.cpp
src/qml/qmlcontrolproxy.cpp
src/qml/qmlcuesmodel.cpp
src/qml/qmldurationformatter.cpp
src/qml/qmldlgpreferencesproxy.cpp
src/qml/qmleffectmanifestparametersmodel.cpp
src/qml/qmleffectslotproxy.cpp
Expand Down
39 changes: 5 additions & 34 deletions res/qml/Deck/TrackTime.qml
Original file line number Diff line number Diff line change
Expand Up @@ -26,41 +26,12 @@ Skin.EmbeddedText {
property double remaining: durationControl.value * (1 - playPositionControl.value)

function toTime(value) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rational for moving this from JS to C++? Making this C++ makes customisation harder, as it requires our user to recompile Mixxx entirely.
Editing C++ also requires non trivial experience (e.g memory management, exception handling), while QML/JS is much more beginner friendly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was explicitly requested in Joerg’s earlier review. cc @JoergAtGithub

let result = "";
switch (root.mode) {
case TrackTime.Mode.Seconds:
case TrackTime.Mode.SecondsLong:
{
let seconds = parseInt(value).toString();
let subs = value % 1;
return `${seconds.padStart(root.mode === TrackTime.Mode.SecondsLong ? 3 : 0, '0')}.${subs.toFixed(2).slice(-2)}`;
}
case TrackTime.Mode.KiloSeconds:
{
let kilos = parseInt(value / 1000);
let seconds = parseInt(value % 1000).toString();
let subs = value % 1;
return `${kilos}.${seconds.padStart(3, '0')} ${subs.toFixed(2).slice(-2)}`;
}
case TrackTime.Mode.HectoSeconds:
return `???`;
default:
console.warn(`Unsupported track time mode: ${root.mode}. Defaulting to traditional`);
case TrackTime.Mode.Traditional:
case TrackTime.Mode.TraditionalCoarse:
{
let component = [];
if (remaining + elapsed > 3600) {
component.push(parseInt(value / 3600).toString().padStart(2, '0'));
}
component.push(parseInt(value / 60).toString().padStart(2, '0'));
component.push(parseInt(value % 60).toString().padStart(2, '0'));
if (root.mode !== TrackTime.Mode.TraditionalCoarse) {
component[component.length - 1] += `.${(value % 1).toFixed(2).slice(-2)}`;
}
return component.join(':');
}
if (!Number.isFinite(value)) {
return "";
}

const sign = value < 0 ? "-" : "";
return sign + Mixxx.DurationFormatter.format(Math.abs(value), root.mode);
}

text: {
Expand Down
332 changes: 332 additions & 0 deletions res/skins/LateNightQML/Deck/LateNightBpmTapEditor.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
pragma ComponentBehavior: Bound

import QtQuick
Comment thread
JoergAtGithub marked this conversation as resolved.
import Mixxx 1.0 as Mixxx
import "../LateNightTheme"

Item {
id: root

required property string group

property string mode: "listen"
property real originalEditValue: 0
property bool editingBpm: true

readonly property bool listenHovered: listenArea.containsMouse
readonly property int hoverLeaveTimeout: 2000
readonly property int inactiveTimeout: 5000
readonly property int tapModeClickGuardTimeout: 350
readonly property real bpmStepSize: 1.0
readonly property real rateStepSize: 0.01
readonly property bool trackLoaded: trackLoadedProxy.value > 0

function switchMode(newMode) {
hideTimer.stop();
if (!root.trackLoaded) {
newMode = "listen";
}
root.mode = newMode;
if (newMode === "select") {
hideTimer.stop();
} else if (newMode === "edit") {
startEditMode();
} else if (newMode === "tap") {
tapClickGuardTimer.restart();
hideTimer.stop();
} else if (newMode === "listen") {
editInput.text = "";
editInput.focus = false;
}
}

function handleTapClick(mouse) {
if (tapClickGuardTimer.running) {
return;
}
root.tapControl(mouse.button === Qt.RightButton ? bpmTapProxy : tempoTapProxy);
}

function tapControl(control) {
hideTimer.interval = root.inactiveTimeout;
hideTimer.restart();
control.value = 1;
control.value = 0;
}

function startEditMode() {
root.editingBpm = fileBpmProxy.value !== 0;
if (root.editingBpm) {
root.originalEditValue = bpmProxy.value;
editInput.text = root.originalEditValue.toFixed(2);
} else {
root.originalEditValue = rateRatioProxy.value * 100;
editInput.text = root.originalEditValue.toFixed(2);
}
editInput.forceActiveFocus();
editInput.selectAll();
}

function applyStep(steps) {
if (root.editingBpm) {
const newBpm = Math.max(1, Math.min(fileBpmProxy.value * 4, bpmProxy.value + steps * root.bpmStepSize));
bpmProxy.value = newBpm;
editInput.text = newBpm.toFixed(2);
root.originalEditValue = newBpm;
} else {
const newRateRatio = Math.max(0.01, Math.min(4.0, rateRatioProxy.value + steps * root.rateStepSize));
rateRatioProxy.value = newRateRatio;
editInput.text = (newRateRatio * 100).toFixed(2);
root.originalEditValue = newRateRatio * 100;
}
editInput.selectAll();
}

function applyEditValueAndQuit() {
const input = editInput.text.trim();
if (input.length === 0) {
switchMode("listen");
return;
}

const parsedValue = Number(input);
if (!Number.isFinite(parsedValue)) {
switchMode("listen");
return;
}
if (parsedValue !== root.originalEditValue) {
if (root.editingBpm) {
bpmProxy.value = Math.max(1, Math.min(fileBpmProxy.value * 4, parsedValue));
} else {
rateRatioProxy.value = Math.max(0.01, Math.min(4.0, parsedValue / 100));
}
}
switchMode("listen");
}

Mixxx.ControlProxy {
id: tempoTapProxy
group: root.group
key: "tempo_tap"
}

Mixxx.ControlProxy {
id: bpmTapProxy
group: root.group
key: "bpm_tap"
}

Mixxx.ControlProxy {
id: trackLoadedProxy
group: root.group
key: "track_loaded"
}

Mixxx.ControlProxy {
id: bpmProxy
group: root.group
key: "bpm"
}

Mixxx.ControlProxy {
id: fileBpmProxy
group: root.group
key: "file_bpm"
}

Mixxx.ControlProxy {
id: rateRatioProxy
group: root.group
key: "rate_ratio"
}

onTrackLoadedChanged: {
if (!root.trackLoaded) {
root.switchMode("listen");
}
}

Timer {
id: hideTimer
interval: root.hoverLeaveTimeout
repeat: false
onTriggered: {
if (root.mode !== "edit") {
root.switchMode("listen");
}
}
}

Timer {
id: tapClickGuardTimer
interval: root.tapModeClickGuardTimeout
repeat: false
}

MouseArea {
id: listenArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
visible: root.mode === "listen"
onClicked: root.switchMode("select")
onDoubleClicked: function(mouse) {
root.switchMode(mouse.x < width / 2 ? "tap" : "edit");
}
}

Row {
anchors.fill: parent
visible: root.mode === "select"

Rectangle {
width: parent.width / 2
height: parent.height
color: LateNightTheme.bpmTapEditorSelectBackgroundColor
border.width: 1
border.color: LateNightTheme.bpmTapEditorSelectBorderColor

Image {
anchors.centerIn: parent
source: LateNightTheme.assetDeckBpmSelectTapButton
fillMode: Image.PreserveAspectFit
}

MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: hideTimer.stop()
onExited: {
if (root.mode === "select") {
hideTimer.interval = root.hoverLeaveTimeout;
hideTimer.restart();
}
}
onClicked: root.switchMode("tap")
onDoubleClicked: root.switchMode("tap")
}
}

Rectangle {
width: parent.width / 2
height: parent.height
color: LateNightTheme.bpmTapEditorSelectBackgroundColor
border.width: 1
border.color: LateNightTheme.bpmTapEditorSelectBorderColor

Image {
anchors.centerIn: parent
source: LateNightTheme.assetDeckBpmSelectEditButton
fillMode: Image.PreserveAspectFit
}

MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: hideTimer.stop()
onExited: {
if (root.mode === "select") {
hideTimer.interval = root.hoverLeaveTimeout;
hideTimer.restart();
}
}
onClicked: root.switchMode("edit")
onDoubleClicked: root.switchMode("edit")
}
}
}

Rectangle {
anchors.fill: parent
visible: root.mode === "tap"
color: LateNightTheme.bpmTapEditorBackgroundColor
border.width: 1
border.color: tapArea.pressed ? LateNightTheme.bpmTapEditorEditBorderColor : LateNightTheme.bpmTapEditorSelectBorderColor
radius: 1

MouseArea {
id: tapArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onEntered: hideTimer.stop()
onExited: {
hideTimer.interval = root.hoverLeaveTimeout;
hideTimer.restart();
}
onClicked: function(mouse) { root.handleTapClick(mouse); }
onDoubleClicked: function(mouse) { mouse.accepted = true; }
}
}

Rectangle {
anchors.fill: parent
visible: root.mode === "edit"
color: LateNightTheme.bpmTapEditorBackgroundColor
border.width: 1
border.color: LateNightTheme.bpmTapEditorEditBorderColor
radius: 2

TextInput {
id: editInput
x: 1
y: 1
width: parent.width - 2
height: parent.height - 17
color: LateNightTheme.textColor
selectedTextColor: LateNightTheme.deckActiveButtonTextColor
selectionColor: LateNightTheme.textColor
font.family: "Open Sans"
font.pixelSize: 14
font.weight: Font.Medium
horizontalAlignment: TextInput.AlignHCenter
verticalAlignment: TextInput.AlignVCenter
inputMethodHints: Qt.ImhFormattedNumbersOnly
selectByMouse: true

Keys.onReturnPressed: root.applyEditValueAndQuit()
Keys.onEnterPressed: root.applyEditValueAndQuit()
Keys.onEscapePressed: root.switchMode("listen")
}

Rectangle {
x: 0
y: parent.height - 16
width: parent.width / 2
height: 16
color: LateNightTheme.bpmTapEditorButtonColor

Image {
anchors.centerIn: parent
source: decreaseArea.pressed ? LateNightTheme.assetDeckBpmSpinboxMinusPressedButton : LateNightTheme.assetDeckBpmSpinboxMinusButton
fillMode: Image.PreserveAspectFit
}

MouseArea {
id: decreaseArea
anchors.fill: parent
onClicked: root.applyStep(-1)
}
}

Rectangle {
x: parent.width / 2
y: parent.height - 16
width: parent.width / 2
height: 16
color: LateNightTheme.bpmTapEditorButtonColor

Image {
anchors.centerIn: parent
source: increaseArea.pressed ? LateNightTheme.assetDeckBpmSpinboxPlusPressedButton : LateNightTheme.assetDeckBpmSpinboxPlusButton
fillMode: Image.PreserveAspectFit
}

MouseArea {
id: increaseArea
anchors.fill: parent
onClicked: root.applyStep(1)
}
}
}
}
Loading
Loading