smoosic 1.0.10 → 1.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/smoosic.js +23 -23
- package/package.json +1 -1
- package/release/library/piano/20seconds.json +1 -0
- package/release/library/trumpet/2min.json +1 -0
- package/release/library/trumpet/Fireworks - trumpets.json +1 -1
- package/release/smoosic.js +23 -23
- package/src/application/application.ts +14 -2
- package/src/application/common.ts +38 -3
- package/src/application/eventHandler.ts +27 -28
- package/src/application/keyCommands.ts +42 -10
- package/src/render/sui/piano.ts +1 -1
- package/src/render/sui/scoreView.ts +15 -11
- package/src/render/sui/scoreViewOperations.ts +9 -7
- package/src/render/sui/tracker.ts +57 -18
- package/src/render/vex/vxMeasure.ts +1 -0
- package/src/smo/data/common.ts +1 -0
- package/src/smo/data/measure.ts +14 -0
- package/src/smo/data/noteModifiers.ts +9 -7
- package/src/smo/data/staffModifiers.ts +10 -4
- package/src/smo/data/systemStaff.ts +19 -1
- package/src/smo/data/tuplet.ts +17 -0
- package/src/smo/mxml/xmlHelpers.ts +8 -1
- package/src/smo/xform/beamers.ts +51 -66
- package/src/smo/xform/copypaste.ts +8 -0
- package/src/smo/xform/selections.ts +13 -1
- package/src/smo/xform/tickDuration.ts +58 -27
- package/src/ui/dialogs/components/rocker.ts +4 -1
- package/src/ui/keyBindings/default/editorKeys.ts +0 -7
- package/tsconfig.json +1 -1
|
@@ -32,7 +32,7 @@ import { PromiseHelpers } from '../common/promiseHelpers';
|
|
|
32
32
|
import { SuiDom } from './dom';
|
|
33
33
|
import { SuiKeyCommands } from './keyCommands';
|
|
34
34
|
import { SuiEventHandler } from './eventHandler';
|
|
35
|
-
import { KeyBinding, ModalEventHandlerProxy } from './common';
|
|
35
|
+
import { KeyBinding, ModalEventHandlerProxy, isTrackerKeyAction, isEditorKeyAction } from './common';
|
|
36
36
|
import { SmoMeasure } from '../smo/data/measure';
|
|
37
37
|
import { getDomContainer } from '../common/htmlHelpers';
|
|
38
38
|
import { SuiHelp } from '../ui/help';
|
|
@@ -142,13 +142,25 @@ export class SuiApplication {
|
|
|
142
142
|
*/
|
|
143
143
|
static get keyBindingDefaults(): KeyBinding[] {
|
|
144
144
|
var editorKeys = SuiEventHandler.editorKeyBindingDefaults;
|
|
145
|
+
let unknownKeyAction: boolean = false;
|
|
145
146
|
editorKeys.forEach((key) => {
|
|
146
|
-
key.module = 'keyCommands'
|
|
147
|
+
key.module = 'keyCommands';
|
|
148
|
+
if (!isEditorKeyAction(key.action)) {
|
|
149
|
+
console.error(`unknown key action ${key.action} in configuration`);
|
|
150
|
+
unknownKeyAction = true;
|
|
151
|
+
}
|
|
147
152
|
});
|
|
148
153
|
var trackerKeys = SuiEventHandler.trackerKeyBindingDefaults;
|
|
149
154
|
trackerKeys.forEach((key) => {
|
|
150
155
|
key.module = 'tracker'
|
|
156
|
+
if (!isTrackerKeyAction(key.action)) {
|
|
157
|
+
console.error(`unknown key action ${key.action} in configuration`);
|
|
158
|
+
unknownKeyAction = true;
|
|
159
|
+
}
|
|
151
160
|
});
|
|
161
|
+
if (unknownKeyAction) {
|
|
162
|
+
throw(`unknown key action in configuration`);
|
|
163
|
+
}
|
|
152
164
|
return trackerKeys.concat(editorKeys);
|
|
153
165
|
}
|
|
154
166
|
/**
|
|
@@ -2,8 +2,43 @@ import { SuiScoreViewOperations } from "../render/sui/scoreViewOperations";
|
|
|
2
2
|
import { SuiTracker } from "../render/sui/tracker";
|
|
3
3
|
import { CompleteNotifier } from "../ui/common";
|
|
4
4
|
import { ModalComponent } from "../ui/common";
|
|
5
|
+
import { KeyEvent } from "../smo/data/common";
|
|
5
6
|
import { BrowserEventSource, EventHandler } from "../ui/eventSource";
|
|
6
7
|
|
|
8
|
+
export type trackerKeyAction = "moveHome" | "moveEnd" | "moveSelectionRight" | "moveSelectionLeft" |
|
|
9
|
+
"moveSelectionUp" | "moveSelectionDown" | "moveSelectionRightMeasure" | "moveSelectionLeftMeasure" |
|
|
10
|
+
"advanceModifierSelection" | "growSelectionRight" | "growSelectionLeft" |
|
|
11
|
+
"growSelectionRightMeasure" | "growSelectionRightMeasure" |
|
|
12
|
+
"moveSelectionPitchUp" | "moveSelectionPitchDown";
|
|
13
|
+
export const trackerKeyActions = ["moveHome" , "moveEnd" , "moveSelectionRight" , "moveSelectionLeft" ,
|
|
14
|
+
"moveSelectionUp" , "moveSelectionDown" , "moveSelectionRightMeasure" , "moveSelectionLeftMeasure" ,
|
|
15
|
+
"advanceModifierSelection" , "growSelectionRight" , "growSelectionLeft" , "growSelectionRightMeasure",
|
|
16
|
+
"moveSelectionPitchUp" , "moveSelectionPitchDown"];
|
|
17
|
+
export type editorKeyAction = "transposeUp" | "transposeDown" | "upOctave" | "downOctave" |
|
|
18
|
+
"toggleCourtesyAccidental" | "toggleEnharmonic" |
|
|
19
|
+
"doubleDuration" | "halveDuration" | "dotDuration" | "undotDuration" | "setPitch" |
|
|
20
|
+
"slashGraceNotes" | "addGraceNote" | "removeGraceNote" |
|
|
21
|
+
"playScore" | "stopPlayer" | "pausePlayer" | "togglePlayer" |
|
|
22
|
+
"undo" | "copy" | "paste" |
|
|
23
|
+
"makeTuplet" | "interval" | "unmakeTuplet" | "addMeasure" | "deleteNote" | "makeRest"|
|
|
24
|
+
"toggleBeamGroup" | "beamSelections" | "toggleBeamDirection" |
|
|
25
|
+
"addRemoveAccent" | "addRemoveTenuto" | "addRemoveStaccato" |
|
|
26
|
+
"addRemovePizzicato" | "addRemoveMarcato";
|
|
27
|
+
export const editorKeyActions = ["transposeUp" , "transposeDown" , "upOctave" , "downOctave" ,
|
|
28
|
+
"toggleCourtesyAccidental" , "toggleEnharmonic",
|
|
29
|
+
"doubleDuration" , "halveDuration" , "dotDuration" , "undotDuration" , "setPitch" ,
|
|
30
|
+
"slashGraceNotes" , "addGraceNote" , "removeGraceNote" ,
|
|
31
|
+
"playScore" , "stopPlayer" , "pausePlayer", "togglePlayer",
|
|
32
|
+
"undo", "copy", "paste",
|
|
33
|
+
"makeTuplet" , "interval" , "unmakeTuplet" , "addMeasure" , "deleteNote" , "makeRest",
|
|
34
|
+
"toggleBeamGroup" , "beamSelections" , "toggleBeamDirection",
|
|
35
|
+
"addRemoveAccent" , "addRemoveTenuto" , "addRemoveStaccato",
|
|
36
|
+
"addRemovePizzicato", "addRemoveMarcato"
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export function isEditorKeyAction(action: string) { return editorKeyActions.indexOf(action) >= 0 };
|
|
40
|
+
export function isTrackerKeyAction(action: string) { return trackerKeyActions.indexOf(action) >= 0 };
|
|
41
|
+
|
|
7
42
|
/**
|
|
8
43
|
* A binding of a key to some action performed by a module
|
|
9
44
|
* @category SuiApplication
|
|
@@ -43,7 +78,7 @@ export interface KeyCommandParams {
|
|
|
43
78
|
export abstract class ModalEventHandler {
|
|
44
79
|
abstract mouseMove(ev: any): void;
|
|
45
80
|
abstract mouseClick(ev: any): void;
|
|
46
|
-
abstract evKey(evdata: any): void
|
|
81
|
+
abstract evKey(evdata: any): Promise<void>;
|
|
47
82
|
abstract keyUp(evdata: any): void;
|
|
48
83
|
}
|
|
49
84
|
export type handler = (ev: any) => void;
|
|
@@ -69,9 +104,9 @@ export class ModalEventHandlerProxy {
|
|
|
69
104
|
this._handler = value;
|
|
70
105
|
this.unbound = false;
|
|
71
106
|
}
|
|
72
|
-
evKey(ev: any) {
|
|
107
|
+
async evKey(ev: any) {
|
|
73
108
|
if (this._handler) {
|
|
74
|
-
this._handler.evKey(ev);
|
|
109
|
+
await this._handler.evKey(ev);
|
|
75
110
|
}
|
|
76
111
|
}
|
|
77
112
|
keyUp(ev: any) {
|
|
@@ -165,10 +165,10 @@ export class SuiEventHandler implements ModalEventHandler {
|
|
|
165
165
|
return;
|
|
166
166
|
// this.unbindKeyboardForModal(dialog);
|
|
167
167
|
} else {
|
|
168
|
-
this.view.tracker.advanceModifierSelection(
|
|
168
|
+
this.view.tracker.advanceModifierSelection(ev);
|
|
169
169
|
}
|
|
170
170
|
} else {
|
|
171
|
-
this.view.tracker.selectSuggestion(
|
|
171
|
+
this.view.tracker.selectSuggestion(ev);
|
|
172
172
|
}
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
@@ -254,7 +254,7 @@ export class SuiEventHandler implements ModalEventHandler {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
evKey(evdata: any) {
|
|
257
|
+
async evKey(evdata: any) {
|
|
258
258
|
if ($('body').hasClass('translation-mode')) {
|
|
259
259
|
return;
|
|
260
260
|
}
|
|
@@ -270,34 +270,33 @@ export class SuiEventHandler implements ModalEventHandler {
|
|
|
270
270
|
Qwerty.handleKeyEvent(evdata);
|
|
271
271
|
}
|
|
272
272
|
const dataCopy = SuiTracker.serializeEvent(evdata);
|
|
273
|
-
this.view.renderer.updatePromise()
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
273
|
+
await this.view.renderer.updatePromise();
|
|
274
|
+
if (dataCopy.key == '?') {
|
|
275
|
+
SuiHelp.displayHelp();
|
|
276
|
+
}
|
|
277
|
+
if (dataCopy.key == 'Enter') {
|
|
278
|
+
this.trackerModifierSelect(dataCopy);
|
|
279
|
+
}
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
var binding: KeyBinding | undefined = this.keyBind.find((ev: KeyBinding) =>
|
|
282
|
+
ev.event === 'keydown' && ev.key === dataCopy.key &&
|
|
283
|
+
ev.ctrlKey === dataCopy.ctrlKey &&
|
|
284
|
+
ev.altKey === dataCopy.altKey && dataCopy.shiftKey === ev.shiftKey);
|
|
285
285
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
this.exhandler.exceptionHandler(e);
|
|
286
|
+
if (binding) {
|
|
287
|
+
try {
|
|
288
|
+
if (binding.module === 'tracker') {
|
|
289
|
+
(this.tracker as any)[binding.action](dataCopy);
|
|
290
|
+
} else {
|
|
291
|
+
(this.keyCommands as any)[binding.action](dataCopy);
|
|
292
|
+
}
|
|
293
|
+
} catch (e) {
|
|
294
|
+
if (typeof (e) === 'string') {
|
|
295
|
+
console.error(e);
|
|
298
296
|
}
|
|
297
|
+
this.exhandler.exceptionHandler(e);
|
|
299
298
|
}
|
|
300
|
-
}
|
|
299
|
+
}
|
|
301
300
|
}
|
|
302
301
|
|
|
303
302
|
mouseMove(ev: any) {
|
|
@@ -310,7 +309,7 @@ export class SuiEventHandler implements ModalEventHandler {
|
|
|
310
309
|
mouseClick(ev: any) {
|
|
311
310
|
const dataCopy = SuiTracker.serializeEvent(ev);
|
|
312
311
|
this.view.renderer.updatePromise().then(() => {
|
|
313
|
-
this.view.tracker.selectSuggestion(
|
|
312
|
+
this.view.tracker.selectSuggestion(dataCopy);
|
|
314
313
|
var modifier = this.view.tracker.getSelectedModifier();
|
|
315
314
|
if (modifier) {
|
|
316
315
|
this.createModifierDialog(modifier);
|
|
@@ -9,14 +9,41 @@ import { BrowserEventSource } from '../ui/eventSource';
|
|
|
9
9
|
import { SuiTracker } from '../render/sui/tracker';
|
|
10
10
|
import { KeyCommandParams } from './common';
|
|
11
11
|
import { CompleteNotifier } from '../ui/common';
|
|
12
|
-
import { PitchLetter, IsPitchLetter, KeyEvent } from '../smo/data/common';
|
|
13
|
-
|
|
12
|
+
import { PitchLetter, IsPitchLetter, KeyEvent, keyHandler, defaultKeyEvent } from '../smo/data/common';
|
|
13
|
+
|
|
14
|
+
export interface EditorKeyHandler {
|
|
15
|
+
transposeUp: keyHandler,
|
|
16
|
+
transposeDown: keyHandler,
|
|
17
|
+
upOctave: keyHandler,
|
|
18
|
+
toggleCourtesyAccidental: keyHandler,
|
|
19
|
+
toggleEnharmonic: keyHandler,
|
|
20
|
+
doubleDuration: keyHandler,
|
|
21
|
+
halveDuration: keyHandler,
|
|
22
|
+
dotDuration: keyHandler,
|
|
23
|
+
undotDuration: keyHandler,
|
|
24
|
+
setPitch: keyHandler,
|
|
25
|
+
slashGraceNotes: keyHandler,
|
|
26
|
+
addGraceNote: keyHandler,
|
|
27
|
+
removeGraceNote: keyHandler,
|
|
28
|
+
playScore: keyHandler,
|
|
29
|
+
stopPlayer: keyHandler,
|
|
30
|
+
makeTuplet: keyHandler,
|
|
31
|
+
interval: keyHandler,
|
|
32
|
+
unmakeTuplet: keyHandler,
|
|
33
|
+
addMeasure: keyHandler,
|
|
34
|
+
deleteNote: keyHandler,
|
|
35
|
+
toggleBeamGroup: keyHandler,
|
|
36
|
+
beamSelections: keyHandler,
|
|
37
|
+
addRemoveAccent: keyHandler,
|
|
38
|
+
addRemoveTenuto: keyHandler,
|
|
39
|
+
addRemoveStaccato: keyHandler
|
|
40
|
+
}
|
|
14
41
|
/**
|
|
15
|
-
* KeyCommands object handles key events and converts them into commands, updating the score and
|
|
42
|
+
* KeyCommands object handles key events and converts them into commands: keyHandler, updating the score and
|
|
16
43
|
* display
|
|
17
44
|
* @category SuiApplication
|
|
18
45
|
* */
|
|
19
|
-
export class SuiKeyCommands {
|
|
46
|
+
export class SuiKeyCommands implements EditorKeyHandler {
|
|
20
47
|
view: SuiScoreViewOperations;
|
|
21
48
|
slashMode: boolean = false;
|
|
22
49
|
completeNotifier: CompleteNotifier;
|
|
@@ -97,8 +124,9 @@ export class SuiKeyCommands {
|
|
|
97
124
|
await this.view.setInterval(direction * interval);
|
|
98
125
|
}
|
|
99
126
|
|
|
100
|
-
async
|
|
127
|
+
async interval(ev?: KeyEvent) {
|
|
101
128
|
// code='Digit3'
|
|
129
|
+
const keyEvent = ev ?? defaultKeyEvent();
|
|
102
130
|
var interval = parseInt(keyEvent.keyCode.toString(), 10) - 49; // 48 === '0', 0 indexed
|
|
103
131
|
if (isNaN(interval) || interval < 1 || interval > 7) {
|
|
104
132
|
return;
|
|
@@ -129,7 +157,8 @@ export class SuiKeyCommands {
|
|
|
129
157
|
await this.view.setPitch(letter);
|
|
130
158
|
}
|
|
131
159
|
|
|
132
|
-
async setPitch(
|
|
160
|
+
async setPitch(ev?: KeyEvent) {
|
|
161
|
+
const keyEvent = ev ?? defaultKeyEvent();
|
|
133
162
|
const letter = keyEvent.key.toLowerCase();
|
|
134
163
|
if (IsPitchLetter(letter)) {
|
|
135
164
|
await this.setPitchCommand(letter);
|
|
@@ -152,8 +181,8 @@ export class SuiKeyCommands {
|
|
|
152
181
|
await this.view.batchDurationOperation('halveDuration');
|
|
153
182
|
}
|
|
154
183
|
|
|
155
|
-
async addMeasure(keyEvent
|
|
156
|
-
await this.view.addMeasure(
|
|
184
|
+
async addMeasure(keyEvent?: KeyEvent) {
|
|
185
|
+
await this.view.addMeasure(false);
|
|
157
186
|
}
|
|
158
187
|
async deleteNote() {
|
|
159
188
|
await this.view.deleteNote();
|
|
@@ -167,9 +196,12 @@ export class SuiKeyCommands {
|
|
|
167
196
|
}
|
|
168
197
|
|
|
169
198
|
async makeTupletCommand(numNotes: number) {
|
|
170
|
-
await this.view.makeTuplet({numNotes: numNotes, notesOccupied: 2, bracketed: true, ratioed: false});
|
|
199
|
+
await this.view.makeTuplet({ numNotes: numNotes, notesOccupied: 2, bracketed: true, ratioed: false });
|
|
171
200
|
}
|
|
172
|
-
async makeTuplet(keyEvent
|
|
201
|
+
async makeTuplet(keyEvent?: KeyEvent) {
|
|
202
|
+
if (!keyEvent) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
173
205
|
const numNotes = parseInt(keyEvent.key, 10);
|
|
174
206
|
await this.makeTupletCommand(numNotes);
|
|
175
207
|
}
|
package/src/render/sui/piano.ts
CHANGED
|
@@ -123,7 +123,7 @@ export class SuiPiano {
|
|
|
123
123
|
this.view.tracker.moveSelectionLeft();
|
|
124
124
|
});
|
|
125
125
|
$('button.jsRight').off('click').on('click', () => {
|
|
126
|
-
this.view.tracker.moveSelectionRight(
|
|
126
|
+
this.view.tracker.moveSelectionRight();
|
|
127
127
|
});
|
|
128
128
|
$('button.jsGrowDuration').off('click').on('click', () => {
|
|
129
129
|
this.view.batchDurationOperation('doubleDuration');
|
|
@@ -241,24 +241,28 @@ export abstract class SuiScoreView {
|
|
|
241
241
|
// The length of the paste buffer, in ticks
|
|
242
242
|
const ticksToPaste = this.storePaste.getCopyBufferTickCount();
|
|
243
243
|
const selections: SmoSelection[] = SmoSelection.getMeasureList(this.tracker.selections);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
if (!selections.length) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
const destination = selections[0];
|
|
248
|
+
selections.splice(0);
|
|
249
|
+
selections.push(destination);
|
|
250
|
+
const voice = destination.selector.voice;
|
|
251
|
+
const tm = destination.measure.tickmapForVoice(voice);
|
|
247
252
|
// length of first selected measure, in ticks
|
|
248
|
-
const measureTicks =
|
|
253
|
+
const measureTicks = destination.measure.getTicksFromVoice(voice);
|
|
249
254
|
// remaining ticks after first selection. This is our starting point.
|
|
250
255
|
let startTick = measureTicks - tm.durationMap[this.tracker.selections[0].selector.tick];
|
|
251
|
-
|
|
252
|
-
for (let i = 1; i < selections.length; ++i) {
|
|
253
|
-
const sel = selections[i];
|
|
254
|
-
startTick += sel.measure.getTicksFromVoice(0);
|
|
255
|
-
}
|
|
256
|
+
let currentMeasure = destination.selector.measure + 1;
|
|
256
257
|
// if we are short, and there are measures left, add them to the selection list
|
|
257
|
-
|
|
258
|
-
const newSel = SmoSelection.measureSelection(this.score,
|
|
258
|
+
while (startTick < ticksToPaste && destination.staff.measures.length > currentMeasure) {
|
|
259
|
+
const newSel = SmoSelection.measureSelection(this.score,
|
|
260
|
+
destination.selector.staff, currentMeasure);
|
|
259
261
|
if (newSel) {
|
|
260
262
|
selections.push(newSel);
|
|
263
|
+
startTick += newSel.measure.getTicksFromThisOrAnyVoice(voice);
|
|
261
264
|
}
|
|
265
|
+
currentMeasure += 1;
|
|
262
266
|
}
|
|
263
267
|
return selections;
|
|
264
268
|
}
|
|
@@ -1182,7 +1182,9 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
1182
1182
|
const altSel = this._getEquivalentSelection(selected);
|
|
1183
1183
|
SmoOperation.setPitch(altSel!, [pitch]);
|
|
1184
1184
|
if (this.score.preferences.autoAdvance) {
|
|
1185
|
-
|
|
1185
|
+
// Don't play the next note and the added pitch at the same time.
|
|
1186
|
+
this.tracker.deferNextAutoPlay();
|
|
1187
|
+
this.tracker.moveSelectionRight();
|
|
1186
1188
|
}
|
|
1187
1189
|
});
|
|
1188
1190
|
if (selections.length === 1 && this.score.preferences.autoPlay) {
|
|
@@ -1936,7 +1938,7 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
1936
1938
|
* @returns
|
|
1937
1939
|
*/
|
|
1938
1940
|
async moveHome(ev: KeyEvent): Promise<any> {
|
|
1939
|
-
this.tracker.moveHome(
|
|
1941
|
+
this.tracker.moveHome(ev);
|
|
1940
1942
|
await this.renderer.updatePromise();
|
|
1941
1943
|
}
|
|
1942
1944
|
/**
|
|
@@ -1946,7 +1948,7 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
1946
1948
|
* @returns
|
|
1947
1949
|
*/
|
|
1948
1950
|
async moveEnd(ev: KeyEvent): Promise<any> {
|
|
1949
|
-
this.tracker.moveEnd(
|
|
1951
|
+
this.tracker.moveEnd(ev);
|
|
1950
1952
|
await this.renderer.updatePromise();
|
|
1951
1953
|
}
|
|
1952
1954
|
/**
|
|
@@ -1973,7 +1975,7 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
1973
1975
|
* @returns
|
|
1974
1976
|
*/
|
|
1975
1977
|
async advanceModifierSelection(keyEv: KeyEvent): Promise<any> {
|
|
1976
|
-
this.tracker.advanceModifierSelection(
|
|
1978
|
+
this.tracker.advanceModifierSelection(keyEv);
|
|
1977
1979
|
await this.renderer.updatePromise();
|
|
1978
1980
|
}
|
|
1979
1981
|
/**
|
|
@@ -1989,8 +1991,8 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
1989
1991
|
* @param ev
|
|
1990
1992
|
* @returns
|
|
1991
1993
|
*/
|
|
1992
|
-
async moveSelectionRight(
|
|
1993
|
-
this.tracker.moveSelectionRight(
|
|
1994
|
+
async moveSelectionRight(): Promise<any> {
|
|
1995
|
+
this.tracker.moveSelectionRight();
|
|
1994
1996
|
await this.renderer.updatePromise();
|
|
1995
1997
|
}
|
|
1996
1998
|
/**
|
|
@@ -2054,7 +2056,7 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
2054
2056
|
* @returns
|
|
2055
2057
|
*/
|
|
2056
2058
|
async selectSuggestion(evData: KeyEvent): Promise<any> {
|
|
2057
|
-
this.tracker.selectSuggestion(
|
|
2059
|
+
this.tracker.selectSuggestion(evData);
|
|
2058
2060
|
await this.renderer.updatePromise();
|
|
2059
2061
|
}
|
|
2060
2062
|
/**
|
|
@@ -6,7 +6,7 @@ import { SmoSelection, SmoSelector, ModifierTab } from '../../smo/xform/selectio
|
|
|
6
6
|
import { smoSerialize } from '../../common/serializationHelpers';
|
|
7
7
|
import { SuiOscillator } from '../audio/oscillator';
|
|
8
8
|
import { SmoScore } from '../../smo/data/score';
|
|
9
|
-
import { SvgBox, KeyEvent } from '../../smo/data/common';
|
|
9
|
+
import { SvgBox, KeyEvent, defaultKeyEvent, keyHandler } from '../../smo/data/common';
|
|
10
10
|
import { SuiScroller } from './scroller';
|
|
11
11
|
import { PasteBuffer } from '../../smo/xform/copypaste';
|
|
12
12
|
import { SmoNote } from '../../smo/data/note';
|
|
@@ -14,15 +14,31 @@ import { SmoMeasure } from '../../smo/data/measure';
|
|
|
14
14
|
import { layoutDebug } from './layoutDebug';
|
|
15
15
|
declare var $: any;
|
|
16
16
|
|
|
17
|
+
export interface TrackerKeyHandler {
|
|
18
|
+
moveHome : keyHandler,
|
|
19
|
+
moveEnd : keyHandler,
|
|
20
|
+
moveSelectionRight : keyHandler,
|
|
21
|
+
moveSelectionLeft : keyHandler,
|
|
22
|
+
moveSelectionUp : keyHandler,
|
|
23
|
+
moveSelectionDown : keyHandler,
|
|
24
|
+
moveSelectionRightMeasure : keyHandler,
|
|
25
|
+
moveSelectionLeftMeasure : keyHandler,
|
|
26
|
+
advanceModifierSelection : keyHandler,
|
|
27
|
+
growSelectionRight : keyHandler,
|
|
28
|
+
growSelectionLeft : keyHandler,
|
|
29
|
+
moveSelectionPitchUp : keyHandler,
|
|
30
|
+
moveSelectionPitchDown: keyHandler
|
|
31
|
+
}
|
|
17
32
|
/**
|
|
18
33
|
* SuiTracker
|
|
19
34
|
* A tracker maps the UI elements to the logical elements ,and allows the user to
|
|
20
35
|
* move through the score and make selections, for navigation and editing.
|
|
21
36
|
* @category SuiRender
|
|
22
37
|
*/
|
|
23
|
-
export class SuiTracker extends SuiMapper {
|
|
38
|
+
export class SuiTracker extends SuiMapper implements TrackerKeyHandler {
|
|
24
39
|
idleTimer: number = Date.now();
|
|
25
40
|
musicCursorGlyph: SVGSVGElement | null = null;
|
|
41
|
+
deferPlayAdvance: boolean = false;
|
|
26
42
|
static get strokes(): Record<string, StrokeInfo> {
|
|
27
43
|
return {
|
|
28
44
|
suggestion: {
|
|
@@ -75,6 +91,21 @@ export class SuiTracker extends SuiMapper {
|
|
|
75
91
|
getIdleTime(): number {
|
|
76
92
|
return this.idleTimer;
|
|
77
93
|
}
|
|
94
|
+
playSelection(artifact: SmoSelection) {
|
|
95
|
+
if (!this.deferPlayAdvance && this.score) {
|
|
96
|
+
SuiOscillator.playSelectionNow(artifact, this.score, 1);
|
|
97
|
+
} else {
|
|
98
|
+
this.deferPlayAdvance = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
deferNextAutoPlay() {
|
|
102
|
+
if (this.score) {
|
|
103
|
+
// don't play on advance if we've just added a note and played it because they overlap
|
|
104
|
+
if (this.score.preferences.autoAdvance && this.score.preferences.autoPlay) {
|
|
105
|
+
this.deferPlayAdvance = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
78
109
|
|
|
79
110
|
getSelectedModifier() {
|
|
80
111
|
if (this.modifierSelections.length) {
|
|
@@ -96,7 +127,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
96
127
|
return rv;
|
|
97
128
|
}
|
|
98
129
|
|
|
99
|
-
advanceModifierSelection(
|
|
130
|
+
advanceModifierSelection(keyEv?: KeyEvent) {
|
|
100
131
|
if (!keyEv) {
|
|
101
132
|
return;
|
|
102
133
|
}
|
|
@@ -220,14 +251,15 @@ export class SuiTracker extends SuiMapper {
|
|
|
220
251
|
return 0;
|
|
221
252
|
}
|
|
222
253
|
if (!this.mapping && this.autoPlay && skipPlay === false && this.score) {
|
|
223
|
-
|
|
254
|
+
this.playSelection(artifact);
|
|
224
255
|
}
|
|
225
256
|
this.selections.push(artifact);
|
|
226
257
|
this.deferHighlight();
|
|
227
258
|
this._createLocalModifiersList();
|
|
228
259
|
return (artifact.note as SmoNote).tickCount;
|
|
229
260
|
}
|
|
230
|
-
moveHome(
|
|
261
|
+
moveHome(keyEvent?: KeyEvent) {
|
|
262
|
+
const evKey = keyEvent ?? defaultKeyEvent();
|
|
231
263
|
this.idleTimer = Date.now();
|
|
232
264
|
const ls = this.selections[0].staff;
|
|
233
265
|
if (evKey.ctrlKey) {
|
|
@@ -235,7 +267,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
235
267
|
const homeSel = this._getClosestTick({ staff: ls.staffId,
|
|
236
268
|
measure: 0, voice: mm.getActiveVoice(), tick: 0, pitches: [] });
|
|
237
269
|
if (evKey.shiftKey) {
|
|
238
|
-
this._selectBetweenSelections(
|
|
270
|
+
this._selectBetweenSelections(this.selections[0], homeSel);
|
|
239
271
|
} else {
|
|
240
272
|
this.selections = [homeSel];
|
|
241
273
|
this.deferHighlight();
|
|
@@ -253,7 +285,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
253
285
|
measure: mm.measureNumber.measureIndex, voice: mm.getActiveVoice(),
|
|
254
286
|
tick: 0, pitches: [] });
|
|
255
287
|
if (evKey.shiftKey) {
|
|
256
|
-
this._selectBetweenSelections(
|
|
288
|
+
this._selectBetweenSelections(this.selections[0], homeSel);
|
|
257
289
|
} else if (homeSel?.measure?.svg?.logicalBox) {
|
|
258
290
|
this.selections = [homeSel];
|
|
259
291
|
this.scroller.scrollVisibleBox(homeSel.measure.svg.logicalBox);
|
|
@@ -262,9 +294,10 @@ export class SuiTracker extends SuiMapper {
|
|
|
262
294
|
}
|
|
263
295
|
}
|
|
264
296
|
}
|
|
265
|
-
moveEnd(
|
|
297
|
+
moveEnd(keyEvent?: KeyEvent) {
|
|
266
298
|
this.idleTimer = Date.now();
|
|
267
299
|
const ls = this.selections[0].staff;
|
|
300
|
+
const evKey = keyEvent ?? defaultKeyEvent();
|
|
268
301
|
if (evKey.ctrlKey) {
|
|
269
302
|
const lm = ls.measures[ls.measures.length - 1];
|
|
270
303
|
const voiceIx = lm.getActiveVoice();
|
|
@@ -272,7 +305,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
272
305
|
const endSel = this._getClosestTick({ staff: ls.staffId,
|
|
273
306
|
measure: ls.measures.length - 1, voice: voiceIx, tick: voice.notes.length - 1, pitches: [] });
|
|
274
307
|
if (evKey.shiftKey) {
|
|
275
|
-
this._selectBetweenSelections(
|
|
308
|
+
this._selectBetweenSelections(this.selections[0], endSel);
|
|
276
309
|
} else {
|
|
277
310
|
this.selections = [endSel];
|
|
278
311
|
this.deferHighlight();
|
|
@@ -292,7 +325,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
292
325
|
const endSel = this._getClosestTick({ staff: ls.staffId,
|
|
293
326
|
measure: lm.measureNumber.measureIndex, voice: lm.getActiveVoice(), tick: ticks - 1, pitches: [] });
|
|
294
327
|
if (evKey.shiftKey) {
|
|
295
|
-
this._selectBetweenSelections(
|
|
328
|
+
this._selectBetweenSelections(this.selections[0], endSel);
|
|
296
329
|
} else {
|
|
297
330
|
this.selections = [endSel];
|
|
298
331
|
this.deferHighlight();
|
|
@@ -342,7 +375,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
342
375
|
artifact.measure.setActiveVoice(nselect.voice);
|
|
343
376
|
this.selections.push(artifact);
|
|
344
377
|
if (this.autoPlay && this.score) {
|
|
345
|
-
|
|
378
|
+
this.playSelection(artifact);
|
|
346
379
|
}
|
|
347
380
|
this.deferHighlight();
|
|
348
381
|
this._createLocalModifiersList();
|
|
@@ -350,10 +383,11 @@ export class SuiTracker extends SuiMapper {
|
|
|
350
383
|
}
|
|
351
384
|
|
|
352
385
|
// if we are being moved right programmatically, avoid playing the selected note.
|
|
353
|
-
moveSelectionRight(
|
|
386
|
+
moveSelectionRight() {
|
|
354
387
|
if (this.selections.length === 0 || this.score === null) {
|
|
355
388
|
return;
|
|
356
389
|
}
|
|
390
|
+
const skipPlay = !this.score.preferences.autoPlay;
|
|
357
391
|
// const original = JSON.parse(JSON.stringify(this.getExtremeSelection(-1).selector));
|
|
358
392
|
const nselect = this._getOffsetSelection(1);
|
|
359
393
|
// skip any measures that are not displayed due to rest or repetition
|
|
@@ -479,7 +513,8 @@ export class SuiTracker extends SuiMapper {
|
|
|
479
513
|
artifact = SmoSelection.noteSelection(this.score, 0, 0, 0, 0);
|
|
480
514
|
}
|
|
481
515
|
if (!skipPlay && this.autoPlay && artifact) {
|
|
482
|
-
|
|
516
|
+
this.playSelection(artifact);
|
|
517
|
+
|
|
483
518
|
}
|
|
484
519
|
if (!artifact) {
|
|
485
520
|
return;
|
|
@@ -538,7 +573,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
538
573
|
SmoSelector.neq(sel.selector, selection.selector)
|
|
539
574
|
);
|
|
540
575
|
if (this.autoPlay && this.score) {
|
|
541
|
-
|
|
576
|
+
this.playSelection(selection);
|
|
542
577
|
}
|
|
543
578
|
ar.push(selection);
|
|
544
579
|
this.selections = ar;
|
|
@@ -565,7 +600,11 @@ export class SuiTracker extends SuiMapper {
|
|
|
565
600
|
}
|
|
566
601
|
this.idleTimer = Date.now();
|
|
567
602
|
}
|
|
568
|
-
_selectBetweenSelections(
|
|
603
|
+
_selectBetweenSelections(s1: SmoSelection, s2: SmoSelection) {
|
|
604
|
+
const score = this.renderer.score ?? null;
|
|
605
|
+
if (!score) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
569
608
|
const min = SmoSelector.gt(s1.selector, s2.selector) ? s2 : s1;
|
|
570
609
|
const max = SmoSelector.lt(min.selector, s2.selector) ? s2 : s1;
|
|
571
610
|
this._selectFromToInStaff(score, min, max);
|
|
@@ -573,7 +612,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
573
612
|
this.highlightQueue.selectionCount = this.selections.length;
|
|
574
613
|
this.deferHighlight();
|
|
575
614
|
}
|
|
576
|
-
selectSuggestion(
|
|
615
|
+
selectSuggestion(ev: KeyEvent) {
|
|
577
616
|
if (!this.suggestion || !this.suggestion.measure || this.score === null) {
|
|
578
617
|
return;
|
|
579
618
|
}
|
|
@@ -596,7 +635,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
596
635
|
if (ev.shiftKey) {
|
|
597
636
|
const sel1 = this.getExtremeSelection(-1);
|
|
598
637
|
if (sel1.selector.staff === this.suggestion.selector.staff) {
|
|
599
|
-
this._selectBetweenSelections(
|
|
638
|
+
this._selectBetweenSelections(sel1, this.suggestion);
|
|
600
639
|
return;
|
|
601
640
|
}
|
|
602
641
|
}
|
|
@@ -608,7 +647,7 @@ export class SuiTracker extends SuiMapper {
|
|
|
608
647
|
return;
|
|
609
648
|
}
|
|
610
649
|
if (this.autoPlay) {
|
|
611
|
-
|
|
650
|
+
this.playSelection(this.suggestion);
|
|
612
651
|
}
|
|
613
652
|
|
|
614
653
|
const preselected = this.selections[0] ?
|
|
@@ -364,6 +364,7 @@ export class VxMeasure implements VxMeasureIf {
|
|
|
364
364
|
vexNotes.push(vexNote);
|
|
365
365
|
}
|
|
366
366
|
const vexBeam = new VF.Beam(vexNotes);
|
|
367
|
+
vexBeam.breakSecondaryAt(bg.secondaryBeamBreaks);
|
|
367
368
|
this.beamToVexMap[bg.attrs.id] = vexBeam;
|
|
368
369
|
this.vexBeamGroups.push(vexBeam);
|
|
369
370
|
}
|
package/src/smo/data/common.ts
CHANGED
|
@@ -237,6 +237,7 @@ export function keyEventMatch(ev1: KeyEvent, ev2: KeyEvent): boolean {
|
|
|
237
237
|
ev1.ctrlKey === ev2.ctrlKey &&
|
|
238
238
|
ev1.altKey === ev2.altKey && ev1.shiftKey === ev2.shiftKey
|
|
239
239
|
}
|
|
240
|
+
export type keyHandler = (key?: KeyEvent) => void;
|
|
240
241
|
/**
|
|
241
242
|
* @internal
|
|
242
243
|
*/
|
package/src/smo/data/measure.ts
CHANGED
|
@@ -64,6 +64,7 @@ export interface MeasureTick {
|
|
|
64
64
|
*/
|
|
65
65
|
export interface ISmoBeamGroup {
|
|
66
66
|
notes: SmoNote[],
|
|
67
|
+
secondaryBeamBreaks: number[],
|
|
67
68
|
voice: number,
|
|
68
69
|
attrs: SmoAttrs
|
|
69
70
|
}
|
|
@@ -1246,6 +1247,19 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
|
|
|
1246
1247
|
return max;
|
|
1247
1248
|
}
|
|
1248
1249
|
|
|
1250
|
+
/**
|
|
1251
|
+
* For pasting, paste into the target measure if the voice exists, else paste into
|
|
1252
|
+
* voice 0
|
|
1253
|
+
* @param voiceIndex
|
|
1254
|
+
* @returns
|
|
1255
|
+
*/
|
|
1256
|
+
getTicksFromThisOrAnyVoice(voiceIndex: number): number {
|
|
1257
|
+
if (this.voices.length > voiceIndex) {
|
|
1258
|
+
return this.getTicksFromVoice(voiceIndex);
|
|
1259
|
+
} else {
|
|
1260
|
+
return this.getTicksFromVoice(0);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1249
1263
|
/**
|
|
1250
1264
|
* Count the number of ticks in a specific voice
|
|
1251
1265
|
* @param voiceIndex
|