Skip to content

Commit 5855cca

Browse files
committed
feat(LateNightQML): add deck BPM tap editor
1 parent c959092 commit 5855cca

3 files changed

Lines changed: 378 additions & 22 deletions

File tree

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
import QtQuick
2+
import Mixxx 1.0 as Mixxx
3+
import "../LateNightTheme"
4+
5+
Item {
6+
id: root
7+
8+
required property string group
9+
10+
property string mode: "listen"
11+
property real originalEditValue: 0
12+
property bool editingBpm: true
13+
property real tapModeEnteredAt: 0
14+
15+
readonly property bool listenHovered: listenArea.containsMouse
16+
readonly property int hoverLeaveTimeout: 2000
17+
readonly property int inactiveTimeout: 5000
18+
readonly property int tapModeClickGuardTimeout: 350
19+
readonly property real bpmStepSize: 1.0
20+
readonly property real rateStepSize: 0.01
21+
readonly property bool trackLoaded: trackLoadedProxy.value > 0
22+
23+
function switchMode(newMode) {
24+
hideTimer.stop();
25+
if (!root.trackLoaded) {
26+
newMode = "listen";
27+
}
28+
root.mode = newMode;
29+
if (newMode === "select") {
30+
hideTimer.stop();
31+
} else if (newMode === "edit") {
32+
startEditMode();
33+
} else if (newMode === "tap") {
34+
root.tapModeEnteredAt = Date.now();
35+
hideTimer.stop();
36+
} else if (newMode === "listen") {
37+
editInput.text = "";
38+
editInput.focus = false;
39+
}
40+
}
41+
42+
function handleTapClick(mouse) {
43+
if (Date.now() - root.tapModeEnteredAt < root.tapModeClickGuardTimeout) {
44+
return;
45+
}
46+
root.tapControl(mouse.button === Qt.RightButton ? bpmTapProxy : tempoTapProxy);
47+
}
48+
49+
function tapControl(control) {
50+
hideTimer.interval = root.inactiveTimeout;
51+
hideTimer.restart();
52+
control.value = 1;
53+
control.value = 0;
54+
}
55+
56+
function startEditMode() {
57+
root.editingBpm = fileBpmProxy.value !== 0;
58+
if (root.editingBpm) {
59+
root.originalEditValue = bpmProxy.value;
60+
editInput.text = root.originalEditValue.toFixed(2);
61+
} else {
62+
root.originalEditValue = rateRatioProxy.value * 100;
63+
editInput.text = root.originalEditValue.toFixed(2);
64+
}
65+
editInput.forceActiveFocus();
66+
editInput.selectAll();
67+
}
68+
69+
function applyStep(steps) {
70+
if (root.editingBpm) {
71+
const newBpm = Math.max(1, Math.min(fileBpmProxy.value * 4, bpmProxy.value + steps * root.bpmStepSize));
72+
bpmProxy.value = newBpm;
73+
editInput.text = newBpm.toFixed(2);
74+
root.originalEditValue = newBpm;
75+
} else {
76+
const newRateRatio = Math.max(0.01, Math.min(4.0, rateRatioProxy.value + steps * root.rateStepSize));
77+
rateRatioProxy.value = newRateRatio;
78+
editInput.text = (newRateRatio * 100).toFixed(2);
79+
root.originalEditValue = newRateRatio * 100;
80+
}
81+
editInput.selectAll();
82+
}
83+
84+
function applyEditValueAndQuit() {
85+
const parsedValue = Number(editInput.text);
86+
if (!isFinite(parsedValue)) {
87+
switchMode("listen");
88+
return;
89+
}
90+
if (parsedValue !== root.originalEditValue) {
91+
if (root.editingBpm) {
92+
bpmProxy.value = Math.max(1, Math.min(fileBpmProxy.value * 4, parsedValue));
93+
} else {
94+
rateRatioProxy.value = Math.max(0.01, Math.min(4.0, parsedValue / 100));
95+
}
96+
}
97+
switchMode("listen");
98+
}
99+
100+
Mixxx.ControlProxy {
101+
id: tempoTapProxy
102+
group: root.group
103+
key: "tempo_tap"
104+
}
105+
106+
Mixxx.ControlProxy {
107+
id: bpmTapProxy
108+
group: root.group
109+
key: "bpm_tap"
110+
}
111+
112+
Mixxx.ControlProxy {
113+
id: trackLoadedProxy
114+
group: root.group
115+
key: "track_loaded"
116+
}
117+
118+
Mixxx.ControlProxy {
119+
id: bpmProxy
120+
group: root.group
121+
key: "bpm"
122+
}
123+
124+
Mixxx.ControlProxy {
125+
id: fileBpmProxy
126+
group: root.group
127+
key: "file_bpm"
128+
}
129+
130+
Mixxx.ControlProxy {
131+
id: rateRatioProxy
132+
group: root.group
133+
key: "rate_ratio"
134+
}
135+
136+
onTrackLoadedChanged: {
137+
if (!root.trackLoaded) {
138+
root.switchMode("listen");
139+
}
140+
}
141+
142+
Timer {
143+
id: hideTimer
144+
interval: root.hoverLeaveTimeout
145+
repeat: false
146+
onTriggered: root.switchMode("listen")
147+
}
148+
149+
MouseArea {
150+
id: listenArea
151+
anchors.fill: parent
152+
hoverEnabled: true
153+
acceptedButtons: Qt.LeftButton
154+
visible: root.mode === "listen"
155+
onClicked: root.switchMode("select")
156+
onDoubleClicked: function(mouse) {
157+
root.switchMode(mouse.x < width / 2 ? "tap" : "edit");
158+
}
159+
}
160+
161+
Row {
162+
anchors.fill: parent
163+
visible: root.mode === "select"
164+
165+
Rectangle {
166+
width: parent.width / 2
167+
height: parent.height
168+
color: LateNightTheme.bpmTapEditorSelectBackgroundColor
169+
border.width: 1
170+
border.color: LateNightTheme.bpmTapEditorSelectBorderColor
171+
172+
Image {
173+
anchors.centerIn: parent
174+
source: LateNightTheme.assetDeckBpmSelectTapButton
175+
fillMode: Image.PreserveAspectFit
176+
}
177+
178+
MouseArea {
179+
anchors.fill: parent
180+
hoverEnabled: true
181+
onEntered: hideTimer.stop()
182+
onExited: {
183+
hideTimer.interval = root.hoverLeaveTimeout;
184+
hideTimer.restart();
185+
}
186+
onClicked: root.switchMode("tap")
187+
onDoubleClicked: root.switchMode("tap")
188+
}
189+
}
190+
191+
Rectangle {
192+
width: parent.width / 2
193+
height: parent.height
194+
color: LateNightTheme.bpmTapEditorSelectBackgroundColor
195+
border.width: 1
196+
border.color: LateNightTheme.bpmTapEditorSelectBorderColor
197+
198+
Image {
199+
anchors.centerIn: parent
200+
source: LateNightTheme.assetDeckBpmSelectEditButton
201+
fillMode: Image.PreserveAspectFit
202+
}
203+
204+
MouseArea {
205+
anchors.fill: parent
206+
hoverEnabled: true
207+
onEntered: hideTimer.stop()
208+
onExited: {
209+
hideTimer.interval = root.hoverLeaveTimeout;
210+
hideTimer.restart();
211+
}
212+
onClicked: root.switchMode("edit")
213+
onDoubleClicked: root.switchMode("edit")
214+
}
215+
}
216+
}
217+
218+
Rectangle {
219+
anchors.fill: parent
220+
visible: root.mode === "tap"
221+
color: LateNightTheme.bpmTapEditorBackgroundColor
222+
border.width: 1
223+
border.color: tapArea.pressed ? LateNightTheme.bpmTapEditorEditBorderColor : LateNightTheme.bpmTapEditorSelectBorderColor
224+
radius: 1
225+
226+
MouseArea {
227+
id: tapArea
228+
anchors.fill: parent
229+
hoverEnabled: true
230+
acceptedButtons: Qt.LeftButton | Qt.RightButton
231+
onEntered: hideTimer.stop()
232+
onExited: {
233+
hideTimer.interval = root.hoverLeaveTimeout;
234+
hideTimer.restart();
235+
}
236+
onClicked: function(mouse) { root.handleTapClick(mouse); }
237+
onDoubleClicked: function(mouse) { mouse.accepted = true; }
238+
}
239+
}
240+
241+
Rectangle {
242+
anchors.fill: parent
243+
visible: root.mode === "edit"
244+
color: LateNightTheme.bpmTapEditorBackgroundColor
245+
border.width: 1
246+
border.color: LateNightTheme.bpmTapEditorEditBorderColor
247+
radius: 2
248+
249+
TextInput {
250+
id: editInput
251+
x: 1
252+
y: 1
253+
width: parent.width - 2
254+
height: parent.height - 17
255+
color: LateNightTheme.textColor
256+
selectedTextColor: LateNightTheme.deckActiveButtonTextColor
257+
selectionColor: LateNightTheme.textColor
258+
font.family: "Open Sans"
259+
font.pixelSize: 14
260+
font.weight: Font.Medium
261+
horizontalAlignment: TextInput.AlignHCenter
262+
verticalAlignment: TextInput.AlignVCenter
263+
inputMethodHints: Qt.ImhFormattedNumbersOnly
264+
selectByMouse: true
265+
onActiveFocusChanged: {
266+
if (!activeFocus && root.mode === "edit") {
267+
root.switchMode("listen");
268+
}
269+
}
270+
271+
Keys.onReturnPressed: root.applyEditValueAndQuit()
272+
Keys.onEnterPressed: root.applyEditValueAndQuit()
273+
Keys.onEscapePressed: root.switchMode("listen")
274+
}
275+
276+
Rectangle {
277+
x: 0
278+
y: parent.height - 16
279+
width: parent.width / 2
280+
height: 16
281+
color: LateNightTheme.bpmTapEditorButtonColor
282+
283+
Image {
284+
anchors.centerIn: parent
285+
source: decreaseArea.pressed ? LateNightTheme.assetDeckBpmSpinboxMinusPressedButton : LateNightTheme.assetDeckBpmSpinboxMinusButton
286+
fillMode: Image.PreserveAspectFit
287+
}
288+
289+
MouseArea {
290+
id: decreaseArea
291+
anchors.fill: parent
292+
onClicked: root.applyStep(-1)
293+
}
294+
}
295+
296+
Rectangle {
297+
x: parent.width / 2
298+
y: parent.height - 16
299+
width: parent.width / 2
300+
height: 16
301+
color: LateNightTheme.bpmTapEditorButtonColor
302+
303+
Image {
304+
anchors.centerIn: parent
305+
source: increaseArea.pressed ? LateNightTheme.assetDeckBpmSpinboxPlusPressedButton : LateNightTheme.assetDeckBpmSpinboxPlusButton
306+
fillMode: Image.PreserveAspectFit
307+
}
308+
309+
MouseArea {
310+
id: increaseArea
311+
anchors.fill: parent
312+
onClicked: root.applyStep(1)
313+
}
314+
}
315+
}
316+
}

0 commit comments

Comments
 (0)