Skip to content

Commit c501c43

Browse files
committed
Support mouse drag glissando on VirtualKeyboard
- Replace individual key mouse areas with a master pointer handler that tracks mouse position, automatically stops the previous note and triggers the new note as the cursor is dragged across keys, and synchronizes the visual pressed state.
1 parent afba752 commit c501c43

2 files changed

Lines changed: 103 additions & 33 deletions

File tree

‎CHANGELOG‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ Release date:
55

66
New features:
77

8+
* Support mouse drag glissando on VirtualKeyboard
9+
810
* Update JU Synth terminology and UI labels
911

1012
Bug fixes:

‎src/view/qml/Components/VirtualKeyboard.qml‎

Lines changed: 101 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Item {
3737
property int velocityKeyTrack: 0
3838
property int velocityKeyTrackOffset: 0
3939
property var activeNotes: []
40+
property int mousePressedNote: -1
4041

4142
function getVelocityColor(note: int, baseColor: color): color {
4243
if (velocityKeyTrack <= 0) {
@@ -68,6 +69,10 @@ Item {
6869
// Between A-B=2, B-C=1, C-D=2, D-E=2, E-F=1, F-G=2, G-A=2
6970
readonly property var whiteIntervals: [2, 1, 2, 2, 1, 2, 2]
7071

72+
// A–B has black (A#), B–C none, C–D (C#), D–E (D#), E–F none, F–G (F#), G–A (G#)
73+
// Represent as 1 if black key between current white and next
74+
readonly property var blackMap: [1, 0, 1, 1, 0, 1, 1]
75+
7176
// Compute semitone offset from the first white key (A0)
7277
function whiteOffset(i) {
7378
var s = 0;
@@ -76,6 +81,53 @@ Item {
7681
return s;
7782
}
7883

84+
function noteAt(x, y) {
85+
if (x < 0 || x >= width || y < 0 || y >= height || width <= 0 || height <= 0 || whiteKeyWidth <= 0) {
86+
return -1;
87+
}
88+
89+
// Check black keys in top 60% of keyboard height
90+
if (y <= height * 0.6) {
91+
const boundary = Math.round(x / whiteKeyWidth);
92+
const bIndex = boundary - 1;
93+
if (bIndex >= 0 && bIndex < whiteKeyCount - 1 && blackMap[bIndex % 7] === 1) {
94+
const bCenter = boundary * whiteKeyWidth;
95+
const bHalfWidth = (whiteKeyWidth * 0.6) * 0.5;
96+
if (Math.abs(x - bCenter) <= bHalfWidth) {
97+
return baseNote + whiteOffset(bIndex) + 1;
98+
}
99+
}
100+
}
101+
102+
// White key
103+
const wIndex = Math.floor(x / whiteKeyWidth);
104+
if (wIndex >= 0 && wIndex < whiteKeyCount) {
105+
return baseNote + whiteOffset(wIndex);
106+
}
107+
return -1;
108+
}
109+
110+
onEnabledChanged: {
111+
if (!enabled && mousePressedNote !== -1) {
112+
noteOffRequested(mousePressedNote);
113+
mousePressedNote = -1;
114+
}
115+
}
116+
117+
onVisibleChanged: {
118+
if (!visible && mousePressedNote !== -1) {
119+
noteOffRequested(mousePressedNote);
120+
mousePressedNote = -1;
121+
}
122+
}
123+
124+
Component.onDestruction: {
125+
if (mousePressedNote !== -1) {
126+
noteOffRequested(mousePressedNote);
127+
mousePressedNote = -1;
128+
}
129+
}
130+
79131
Rectangle {
80132
anchors.fill: parent
81133
// The bed the keys sit in. Without it the rounded fronts leave notches along the bottom
@@ -99,7 +151,7 @@ Item {
99151
y: pressed ? 1 : 0
100152
height: parent.height - (pressed ? 1 : 0)
101153
radius: rootItem.keyRadius
102-
property bool pressed: false
154+
property bool pressed: rootItem.mousePressedNote === note
103155
property int note: rootItem.baseNote + rootItem.whiteOffset(index)
104156
// The key-track tint stays the key's own colour; the gradient is derived from it
105157
// so a tinted key still shades from back to front.
@@ -149,19 +201,6 @@ Item {
149201
opacity: 0.3
150202
visible: rootItem.activeNotes.includes(parent.note)
151203
}
152-
153-
MouseArea {
154-
anchors.fill: parent
155-
cursorShape: Qt.PointingHandCursor
156-
onPressed: {
157-
wkey.pressed = true;
158-
rootItem.noteOnRequested(wkey.note);
159-
}
160-
onReleased: {
161-
wkey.pressed = false;
162-
rootItem.noteOffRequested(wkey.note);
163-
}
164-
}
165204
}
166205
}
167206
}
@@ -172,22 +211,18 @@ Item {
172211
anchors.fill: parent
173212
z: 2
174213

175-
// A–B has black (A#), B–C none, C–D (C#), D–E (D#), E–F none, F–G (F#), G–A (G#)
176-
// Represent as 1 if black key between current white and next
177-
readonly property var blackMap: [1, 0, 1, 1, 0, 1, 1]
178-
179214
Repeater {
180215
model: rootItem.whiteKeyCount - 1
181216

182217
delegate: Item {
183218
id: bkeySlot
184-
visible: blackLayer.blackMap[index % 7] === 1
219+
visible: rootItem.blackMap[index % 7] === 1
185220
width: rootItem.whiteKeyWidth * 0.6
186221
height: parent.height * 0.6
187222
x: (index + 1) * rootItem.whiteKeyWidth - width / 2
188223
y: 0
189-
property bool pressed: false
190224
property int note: rootItem.baseNote + rootItem.whiteOffset(index) + 1
225+
property bool pressed: rootItem.mousePressedNote === note
191226

192227
// Cast onto the white keys below. A rectangle rather than an effect: 36 of these
193228
// are on screen at once and a blur on each is not worth the frame.
@@ -249,19 +284,6 @@ Item {
249284
visible: rootItem.activeNotes.includes(bkeySlot.note)
250285
}
251286
}
252-
253-
MouseArea {
254-
anchors.fill: parent
255-
cursorShape: Qt.PointingHandCursor
256-
onPressed: {
257-
bkeySlot.pressed = true;
258-
rootItem.noteOnRequested(bkeySlot.note);
259-
}
260-
onReleased: {
261-
bkeySlot.pressed = false;
262-
rootItem.noteOffRequested(bkeySlot.note);
263-
}
264-
}
265287
}
266288
}
267289
}
@@ -308,5 +330,51 @@ Item {
308330
}
309331
}
310332
}
333+
334+
MouseArea {
335+
id: keyboardMouseArea
336+
anchors.fill: parent
337+
z: 10
338+
cursorShape: Qt.PointingHandCursor
339+
acceptedButtons: Qt.LeftButton
340+
preventStealing: true
341+
342+
function updateNote(x, y) {
343+
const note = rootItem.noteAt(x, y);
344+
if (note !== rootItem.mousePressedNote) {
345+
if (rootItem.mousePressedNote !== -1) {
346+
rootItem.noteOffRequested(rootItem.mousePressedNote);
347+
}
348+
rootItem.mousePressedNote = note;
349+
if (note !== -1) {
350+
rootItem.noteOnRequested(note);
351+
}
352+
}
353+
}
354+
355+
onPressed: mouse => {
356+
updateNote(mouse.x, mouse.y);
357+
}
358+
359+
onPositionChanged: mouse => {
360+
if (pressed) {
361+
updateNote(mouse.x, mouse.y);
362+
}
363+
}
364+
365+
onReleased: {
366+
if (rootItem.mousePressedNote !== -1) {
367+
rootItem.noteOffRequested(rootItem.mousePressedNote);
368+
rootItem.mousePressedNote = -1;
369+
}
370+
}
371+
372+
onCanceled: {
373+
if (rootItem.mousePressedNote !== -1) {
374+
rootItem.noteOffRequested(rootItem.mousePressedNote);
375+
rootItem.mousePressedNote = -1;
376+
}
377+
}
378+
}
311379
}
312380
}

0 commit comments

Comments
 (0)