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