smoosic 1.0.14 → 1.0.15
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 +29 -18
- package/package.json +1 -1
- package/release/smoosic.js +29 -18
- package/src/application/exports.ts +2 -1
- package/src/common/vex.ts +11 -0
- package/src/render/audio/samples.ts +2 -2
- package/src/render/sui/mapper.ts +10 -4
- package/src/render/sui/scoreRender.ts +5 -1
- package/src/render/sui/scoreViewOperations.ts +26 -0
- package/src/render/vex/vxMeasure.ts +1 -0
- package/src/smo/data/measure.ts +1 -0
- package/src/smo/data/noteModifiers.ts +42 -1
- package/src/smo/data/score.ts +10 -0
- package/src/smo/data/staffModifiers.ts +14 -5
- package/src/smo/data/systemStaff.ts +11 -0
- package/src/smo/xform/copypaste.ts +50 -1
- package/src/smo/xform/operations.ts +15 -0
- package/src/ui/dialogs/instrument.ts +11 -1
- package/src/ui/keyBindings/default/trackerKeys.ts +1 -1
- package/src/ui/menus/edit.ts +79 -0
- package/src/ui/menus/manager.ts +3 -0
- package/src/ui/ribbonLayout/default/defaultRibbon.ts +12 -2
|
@@ -98,6 +98,7 @@ import { TextCheckComponent } from '../ui/dialogs/components/textCheck';
|
|
|
98
98
|
import { SuiMenuManager} from '../ui/menus/manager';
|
|
99
99
|
import { SuiMenuBase, SuiMenuCustomizer } from '../ui/menus/menu';
|
|
100
100
|
import { SuiScoreMenu } from '../ui/menus/score';
|
|
101
|
+
import { SuiEditMenu } from '../ui/menus/edit';
|
|
101
102
|
import { SuiTextMenu } from '../ui/menus/text';
|
|
102
103
|
import { SuiPartMenu } from '../ui/menus/parts';
|
|
103
104
|
import { SuiVoiceMenu } from '../ui/menus/voices';
|
|
@@ -346,7 +347,7 @@ export const Smo = {
|
|
|
346
347
|
// Menus
|
|
347
348
|
SuiMenuManager, SuiMenuBase, SuiMenuCustomizer, SuiScoreMenu, SuiFileMenu,
|
|
348
349
|
SuiDynamicsMenu, SuiTimeSignatureMenu, SuiKeySignatureMenu, SuiStaffModifierMenu,
|
|
349
|
-
SuiLanguageMenu, SuiMeasureMenu, SuiNoteMenu, SmoLanguage, SmoTranslator, SuiPartMenu,
|
|
350
|
+
SuiLanguageMenu, SuiMeasureMenu, SuiNoteMenu, SuiEditMenu, SmoLanguage, SmoTranslator, SuiPartMenu,
|
|
350
351
|
SuiPartSelectionMenu, SuiTextMenu, SuiVoiceMenu, SuiBeamMenu,
|
|
351
352
|
// Dialogs
|
|
352
353
|
SuiGraceNoteAdapter, SuiGraceNoteDialog, SuiGraceNoteButtonsComponent,
|
package/src/common/vex.ts
CHANGED
|
@@ -86,6 +86,8 @@ export type TabNotePosition = VexTabNotePosition;
|
|
|
86
86
|
// @internal
|
|
87
87
|
export type TabNoteStruct = VexTabNoteStruct;
|
|
88
88
|
|
|
89
|
+
const lineDefaults = [4, 2, 0, 1, 3]; // lines to turn off if there are less than 5
|
|
90
|
+
|
|
89
91
|
/**
|
|
90
92
|
* @internal
|
|
91
93
|
*/
|
|
@@ -163,6 +165,7 @@ export interface SmoVexStaveParams {
|
|
|
163
165
|
canceledKey: string | null,
|
|
164
166
|
startX: number,
|
|
165
167
|
adjX: number,
|
|
168
|
+
lines: number,
|
|
166
169
|
context: any
|
|
167
170
|
}
|
|
168
171
|
export function createTabStave(box: SvgBox, spacing: number, numLines: number): TabStave {
|
|
@@ -224,6 +227,14 @@ export function createStave(params: SmoVexStaveParams) {
|
|
|
224
227
|
fill: 'none', 'stroke-width': 1, stroke: 'white'
|
|
225
228
|
});
|
|
226
229
|
}
|
|
230
|
+
if (params.lines < 5) {
|
|
231
|
+
const linesAr = [];
|
|
232
|
+
for (let i = 0; i < lineDefaults.length; ++i) {
|
|
233
|
+
const visible = lineDefaults[i] < params.lines;
|
|
234
|
+
linesAr.push({ visible });
|
|
235
|
+
}
|
|
236
|
+
stave.setConfigForLines(linesAr);
|
|
237
|
+
}
|
|
227
238
|
// stave.options.spaceAboveStaffLn = 0; // don't let vex place the staff, we want to.
|
|
228
239
|
stave.options.space_above_staff_ln = 0; // don't let vex place the staff, we want to.
|
|
229
240
|
// Add a clef and time signature.
|
|
@@ -382,14 +382,14 @@ export class SuiSampleMedia {
|
|
|
382
382
|
sample: 'sample-asax-a3',
|
|
383
383
|
family: 'wind',
|
|
384
384
|
instrument: 'tenorSax',
|
|
385
|
-
nativeFrequency: SmoAudioPitch.smoPitchToFrequency({ letter: 'a', accidental: 'n', octave: 3 },
|
|
385
|
+
nativeFrequency: SmoAudioPitch.smoPitchToFrequency({ letter: 'a', accidental: 'n', octave: 3 }, 12, null),
|
|
386
386
|
});
|
|
387
387
|
SuiSampleMedia.insertIntoMap({
|
|
388
388
|
sustain: 'sustained',
|
|
389
389
|
sample: 'sample-asax-c4',
|
|
390
390
|
family: 'wind',
|
|
391
391
|
instrument: 'tenorSax',
|
|
392
|
-
nativeFrequency: SmoAudioPitch.smoPitchToFrequency({ letter: 'c', accidental: 'n', octave: 4 },
|
|
392
|
+
nativeFrequency: SmoAudioPitch.smoPitchToFrequency({ letter: 'c', accidental: 'n', octave: 4 }, 12, null),
|
|
393
393
|
});
|
|
394
394
|
SuiSampleMedia.insertIntoMap({
|
|
395
395
|
sustain: 'sustained',
|
package/src/render/sui/mapper.ts
CHANGED
|
@@ -395,8 +395,10 @@ export abstract class SuiMapper {
|
|
|
395
395
|
}
|
|
396
396
|
return true;
|
|
397
397
|
}
|
|
398
|
-
|
|
399
|
-
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* This is the logic that stores the screen location of music after it's rendered
|
|
401
|
+
*/
|
|
400
402
|
mapMeasure(staff: SmoSystemStaff, measure: SmoMeasure, printing: boolean) {
|
|
401
403
|
let voiceIx = 0;
|
|
402
404
|
let selectedTicks = 0;
|
|
@@ -433,8 +435,12 @@ export abstract class SuiMapper {
|
|
|
433
435
|
tick,
|
|
434
436
|
pitches: []
|
|
435
437
|
};
|
|
436
|
-
if (
|
|
437
|
-
|
|
438
|
+
if (measure.repeatSymbol) {
|
|
439
|
+
// Some measures have a symbol that replaces the notes. This allows us to select
|
|
440
|
+
// the measure
|
|
441
|
+
const x = measure.svg.logicalBox.x + (measure.svg.logicalBox.width / voice.notes.length) * tick;
|
|
442
|
+
const width = measure.svg.logicalBox.width / voice.notes.length;
|
|
443
|
+
note.logicalBox = { x, y: measure.svg.logicalBox.y, width, height: measure.svg.logicalBox.height };
|
|
438
444
|
}
|
|
439
445
|
// create a selection for the newly rendered note
|
|
440
446
|
const selection = new SmoSelection({
|
|
@@ -107,7 +107,11 @@ export class SuiScoreRender {
|
|
|
107
107
|
// If this text is attached to the measure, base the block location on the rendered measure location.
|
|
108
108
|
if (newGroup.attachToSelector) {
|
|
109
109
|
// If this text is attached to a staff that is not visible, don't draw it.
|
|
110
|
-
|
|
110
|
+
let mappedStaff = this.score!.staves.find((staff) => staff.staffId === newGroup.selector!.staff);
|
|
111
|
+
if (this.score?.isPartExposed() && this.score.staves[0].partInfo.preserveTextGroups) {
|
|
112
|
+
mappedStaff = this.score!.staves.find((staff) =>
|
|
113
|
+
staff.getMappedStaffId() === newGroup.selector!.staff);
|
|
114
|
+
}
|
|
111
115
|
if (!mappedStaff) {
|
|
112
116
|
return;
|
|
113
117
|
}
|
|
@@ -1235,6 +1235,32 @@ export class SuiScoreViewOperations extends SuiScoreView {
|
|
|
1235
1235
|
this.replaceMeasureView(measureRange);
|
|
1236
1236
|
await this.renderer.updatePromise();
|
|
1237
1237
|
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Paste only the chords.
|
|
1240
|
+
*/
|
|
1241
|
+
async pasteChords(): Promise<void> {
|
|
1242
|
+
// We undo the whole score on a paste, since we don't yet know the
|
|
1243
|
+
// extent of the overlap
|
|
1244
|
+
this.renderer.preserveScroll();
|
|
1245
|
+
const selections: SmoSelection[] = this.getPasteMeasureList();
|
|
1246
|
+
const firstSelection = selections[0];
|
|
1247
|
+
const measureEnd = selections[selections.length - 1].selector.measure;
|
|
1248
|
+
const measureRange = [firstSelection.selector.measure, measureEnd];
|
|
1249
|
+
this.storeUndo.grouping = true;
|
|
1250
|
+
// Undo the paste by selecting all the affected measures
|
|
1251
|
+
for (let i = measureRange[0]; i <= measureRange[1]; ++i) {
|
|
1252
|
+
this._undoColumn('paste', i);
|
|
1253
|
+
this.renderer.unrenderColumn(this.score.staves[0].measures[i]);
|
|
1254
|
+
}
|
|
1255
|
+
this.storeUndo.grouping = false;
|
|
1256
|
+
const altSelection = this._getEquivalentSelection(firstSelection);
|
|
1257
|
+
const altTarget = altSelection!.selector;
|
|
1258
|
+
altTarget.tick = this.tracker.selections[0].selector.tick;
|
|
1259
|
+
this.storePaste.pasteChords(altTarget);
|
|
1260
|
+
// Refresh those measures.
|
|
1261
|
+
this.replaceMeasureView(measureRange);
|
|
1262
|
+
await this.renderer.updatePromise();
|
|
1263
|
+
}
|
|
1238
1264
|
/**
|
|
1239
1265
|
* specify a note head other than the default for the duration
|
|
1240
1266
|
* @param head
|
|
@@ -505,6 +505,7 @@ export class VxMeasure implements VxMeasureIf {
|
|
|
505
505
|
canceledKey,
|
|
506
506
|
startX: this.smoMeasure.svg.maxColumnStartX,
|
|
507
507
|
adjX: this.smoMeasure.svg.adjX,
|
|
508
|
+
lines: this.smoMeasure.lines,
|
|
508
509
|
context: this.context.getContext()
|
|
509
510
|
}
|
|
510
511
|
this.stave = createStave(smoVexStaveParams);
|
package/src/smo/data/measure.ts
CHANGED
|
@@ -861,6 +861,7 @@ export class SmoMeasure implements SmoMeasureParams, TickMappable {
|
|
|
861
861
|
// The measure expects to get concert KS in constructor and adjust for instrument. So do the
|
|
862
862
|
// opposite.
|
|
863
863
|
obj.keySignature = SmoMusic.vexKeySigWithOffset(obj.keySignature, -1 * obj.transposeIndex);
|
|
864
|
+
obj.lines = params.lines;
|
|
864
865
|
// Don't redisplay tempo for a new measure
|
|
865
866
|
const rv = new SmoMeasure(obj);
|
|
866
867
|
if (rv.tempo && rv.tempo.display) {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* @module /smo/data/noteModifiers
|
|
7
7
|
*/
|
|
8
8
|
import { SmoAttrs, Ticks, Pitch, getId, SmoObjectParams, Transposable, SvgBox, SmoModifierBase,
|
|
9
|
-
Clef, IsClef, SmoDynamicCtor
|
|
9
|
+
Clef, IsClef, SmoDynamicCtor,
|
|
10
|
+
IsPitchLetter} from './common';
|
|
10
11
|
import { smoSerialize } from '../../common/serializationHelpers';
|
|
11
12
|
import { SmoMusic } from './music';
|
|
12
13
|
import { defaultNoteScale, FontInfo, getChordSymbolGlyphFromCode } from '../../common/vex';
|
|
@@ -931,6 +932,46 @@ export class SmoLyric extends SmoNoteModifierBase {
|
|
|
931
932
|
this.adjustNoteWidthChord = val;
|
|
932
933
|
}
|
|
933
934
|
}
|
|
935
|
+
static transposeChordToKey(chord: SmoLyric, offset: number, srcKey: string, destKey: string): SmoLyric {
|
|
936
|
+
if (chord.parser !== SmoLyric.parsers.chord || offset === 0) {
|
|
937
|
+
return new SmoLyric(chord);
|
|
938
|
+
}
|
|
939
|
+
const nchord = new SmoLyric(chord);
|
|
940
|
+
let srcIx = 0;
|
|
941
|
+
const maxLen = chord.text.length - 1;
|
|
942
|
+
let destString = '';
|
|
943
|
+
while (srcIx < chord.text.length) {
|
|
944
|
+
let symbolBlock = false;
|
|
945
|
+
const nchar = chord.text[srcIx];
|
|
946
|
+
let lk = srcIx < maxLen ? chord.text[srcIx + 1] : null;
|
|
947
|
+
// make sure this chord start witha VEX pitch letter (A-G upper case)
|
|
948
|
+
if (IsPitchLetter(nchar.toLowerCase()) && nchar == nchar.toUpperCase()) {
|
|
949
|
+
if (lk === '@') {
|
|
950
|
+
symbolBlock = true;
|
|
951
|
+
srcIx += 1;
|
|
952
|
+
lk = srcIx < maxLen ? chord.text[srcIx + 1] : null;
|
|
953
|
+
}
|
|
954
|
+
const pitch: Pitch = {letter: nchar.toLowerCase() as any, accidental: 'n', octave: 4 };
|
|
955
|
+
if (lk !== null && (lk === 'b' || lk === '#')) {
|
|
956
|
+
pitch.accidental = lk;
|
|
957
|
+
srcIx += 1;
|
|
958
|
+
}
|
|
959
|
+
const npitch = SmoMusic.transposePitchForKey(pitch, srcKey, destKey, offset);
|
|
960
|
+
destString += npitch.letter.toUpperCase();
|
|
961
|
+
if (symbolBlock) {
|
|
962
|
+
destString += '@';
|
|
963
|
+
}
|
|
964
|
+
if (npitch.accidental !== 'n') {
|
|
965
|
+
destString += npitch.accidental;
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
destString += nchar;
|
|
969
|
+
}
|
|
970
|
+
srcIx += 1;
|
|
971
|
+
}
|
|
972
|
+
nchord.text = destString;
|
|
973
|
+
return nchord;
|
|
974
|
+
}
|
|
934
975
|
|
|
935
976
|
// ### getClassSelector
|
|
936
977
|
// returns a selector used to find this text block within a note.
|
package/src/smo/data/score.ts
CHANGED
|
@@ -722,6 +722,12 @@ export class SmoScore {
|
|
|
722
722
|
if (!isSmoScoreParams(params)) {
|
|
723
723
|
throw 'Bad score, missing params: ' + JSON.stringify(params, null, ' ');
|
|
724
724
|
}
|
|
725
|
+
if (params.staves.length === 1) {
|
|
726
|
+
const part = params.staves[0].partInfo;
|
|
727
|
+
if (part) {
|
|
728
|
+
part.expandMultimeasureRests = true;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
725
731
|
const score = new SmoScore(params);
|
|
726
732
|
score.textGroups = textGroups;
|
|
727
733
|
score.systemGroups = systemGroups;
|
|
@@ -950,8 +956,12 @@ export class SmoScore {
|
|
|
950
956
|
// immediately preceeding or post-ceding measure if it exists.
|
|
951
957
|
if (measureIndex < staff.measures.length) {
|
|
952
958
|
protomeasure = staff.measures[measureIndex];
|
|
959
|
+
const instrument = staff.getStaffInstrument(measureIndex);
|
|
960
|
+
protomeasure.lines = instrument.lines;
|
|
953
961
|
} else if (staff.measures.length) {
|
|
954
962
|
protomeasure = staff.measures[staff.measures.length - 1];
|
|
963
|
+
const instrument = staff.getStaffInstrument(staff.measures.length - 1);
|
|
964
|
+
protomeasure.lines = instrument.lines;
|
|
955
965
|
} else {
|
|
956
966
|
protomeasure = SmoMeasure.defaults;
|
|
957
967
|
}
|
|
@@ -158,7 +158,8 @@ export interface SmoInstrumentParams {
|
|
|
158
158
|
/**
|
|
159
159
|
* future, can be used to set sample
|
|
160
160
|
*/
|
|
161
|
-
mutes?: string,
|
|
161
|
+
mutes?: string,
|
|
162
|
+
lines: number
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
/**
|
|
@@ -174,8 +175,8 @@ export interface SmoInstrumentParamsSer extends SmoInstrumentParams {
|
|
|
174
175
|
function isSmoInstrumentParamsSer(params: Partial<SmoInstrumentParamsSer>): params is SmoInstrumentParamsSer {
|
|
175
176
|
return params?.ctor === 'SmoInstrument';
|
|
176
177
|
}
|
|
177
|
-
export type SmoInstrumentNumParamType = 'keyOffset' | 'midichannel' | 'midiport' | 'midiInstrument';
|
|
178
|
-
export const SmoInstrumentNumParams: SmoInstrumentNumParamType[] = ['keyOffset', 'midichannel', 'midiport', 'midiInstrument'];
|
|
178
|
+
export type SmoInstrumentNumParamType = 'keyOffset' | 'midichannel' | 'midiport' | 'midiInstrument' | 'lines';
|
|
179
|
+
export const SmoInstrumentNumParams: SmoInstrumentNumParamType[] = ['keyOffset', 'midichannel', 'midiport', 'midiInstrument', 'lines'];
|
|
179
180
|
export type SmoInstrumentStringParamType = 'instrumentName' | 'abbreviation' | 'family' | 'instrument';
|
|
180
181
|
export const SmoInstrumentStringParams: SmoInstrumentStringParamType[] = ['instrumentName', 'abbreviation', 'family', 'instrument'];
|
|
181
182
|
/**
|
|
@@ -187,7 +188,8 @@ export const SmoInstrumentStringParams: SmoInstrumentStringParamType[] = ['instr
|
|
|
187
188
|
*/
|
|
188
189
|
export class SmoInstrument extends StaffModifierBase {
|
|
189
190
|
static get attributes() {
|
|
190
|
-
return ['startSelector', 'endSelector',
|
|
191
|
+
return ['startSelector', 'endSelector',
|
|
192
|
+
'keyOffset', 'midichannel', 'midiport', 'instrumentName', 'abbreviation', 'instrument', 'family', 'lines'];
|
|
191
193
|
}
|
|
192
194
|
startSelector: SmoSelector;
|
|
193
195
|
endSelector: SmoSelector;
|
|
@@ -196,6 +198,7 @@ export class SmoInstrument extends StaffModifierBase {
|
|
|
196
198
|
keyOffset: number = 0;
|
|
197
199
|
clef: Clef = 'treble';
|
|
198
200
|
midiInstrument: number = 1;
|
|
201
|
+
lines: number = 5;
|
|
199
202
|
midichannel: number;
|
|
200
203
|
midiport: number;
|
|
201
204
|
family: string;
|
|
@@ -214,7 +217,8 @@ export class SmoInstrument extends StaffModifierBase {
|
|
|
214
217
|
midiInstrument: 1,
|
|
215
218
|
midiport: 0,
|
|
216
219
|
startSelector: SmoSelector.default,
|
|
217
|
-
endSelector: SmoSelector.default
|
|
220
|
+
endSelector: SmoSelector.default,
|
|
221
|
+
lines: 5
|
|
218
222
|
}));
|
|
219
223
|
}
|
|
220
224
|
static get defaultOscillatorParam(): SmoOscillatorInfo {
|
|
@@ -241,6 +245,11 @@ export class SmoInstrument extends StaffModifierBase {
|
|
|
241
245
|
} else {
|
|
242
246
|
name = (params as any).instrument;
|
|
243
247
|
}
|
|
248
|
+
if (typeof ((params as any).lines) === 'undefined') {
|
|
249
|
+
this.lines = 5;
|
|
250
|
+
} else {
|
|
251
|
+
this.lines = params.lines;
|
|
252
|
+
}
|
|
244
253
|
this.instrumentName = name;
|
|
245
254
|
this.family = params.family;
|
|
246
255
|
this.instrument = params.instrument;
|
|
@@ -380,6 +380,7 @@ export class SmoSystemStaff implements SmoObjectParams {
|
|
|
380
380
|
curInstrumentIndex += 1;
|
|
381
381
|
}
|
|
382
382
|
measure.transposeIndex = instrumentAr[curInstrumentIndex].instrument.keyOffset;
|
|
383
|
+
measure.lines = instrumentAr[curInstrumentIndex].instrument.lines;
|
|
383
384
|
params.measures.push(measure);
|
|
384
385
|
});
|
|
385
386
|
if (jsonObj.modifiers) {
|
|
@@ -416,6 +417,15 @@ export class SmoSystemStaff implements SmoObjectParams {
|
|
|
416
417
|
}
|
|
417
418
|
mod.associatedStaff = to; // this.staffId will remap to 'to' value
|
|
418
419
|
});
|
|
420
|
+
this.textBrackets.forEach((mod: SmoStaffTextBracket) => {
|
|
421
|
+
if (mod.startSelector.staff === from) {
|
|
422
|
+
mod.startSelector.staff = to;
|
|
423
|
+
}
|
|
424
|
+
if (mod.endSelector.staff === from) {
|
|
425
|
+
mod.endSelector.staff = to;
|
|
426
|
+
}
|
|
427
|
+
mod.associatedStaff = to; // this.staffId will remap to 'to' value
|
|
428
|
+
});
|
|
419
429
|
}
|
|
420
430
|
updateMeasureFormatsForPart() {
|
|
421
431
|
this.measures.forEach((measure, mix) => {
|
|
@@ -453,6 +463,7 @@ export class SmoSystemStaff implements SmoObjectParams {
|
|
|
453
463
|
const tabStave: SmoTabStave | undefined = this.getTabStaveForMeasure(SmoSelector.fromMeasure(measure));
|
|
454
464
|
measure.transposeToOffset(entry.instrument.keyOffset, targetKey, entry.instrument.clef);
|
|
455
465
|
measure.transposeIndex = entry.instrument.keyOffset;
|
|
466
|
+
measure.lines = entry.instrument.lines;
|
|
456
467
|
measure.keySignature = targetKey;
|
|
457
468
|
measure.setClef(entry.instrument.clef);
|
|
458
469
|
}
|
|
@@ -4,6 +4,7 @@ import { SmoSelection, SmoSelector } from './selections';
|
|
|
4
4
|
import { SmoNote } from '../data/note';
|
|
5
5
|
import { SmoMeasure, SmoVoice } from '../data/measure';
|
|
6
6
|
import { StaffModifierBase } from '../data/staffModifiers';
|
|
7
|
+
import { SmoLyric } from '../data/noteModifiers';
|
|
7
8
|
import {SmoTuplet, SmoTupletTree, SmoTupletTreeParams} from '../data/tuplet';
|
|
8
9
|
import { SmoMusic } from '../data/music';
|
|
9
10
|
import { SvgHelpers } from '../../render/sui/svgHelpers';
|
|
@@ -131,6 +132,14 @@ export class PasteBuffer {
|
|
|
131
132
|
const keyOffset = -1 * selection.measure.transposeIndex;
|
|
132
133
|
const destKey = SmoMusic.vexKeySignatureTranspose(originalKey, keyOffset).toLocaleLowerCase();
|
|
133
134
|
const note = SmoNote.transpose(SmoNote.clone(selection.note),[], keyOffset, selection.measure.keySignature, destKey) as SmoNote;
|
|
135
|
+
const chords: SmoLyric[] = note.getChords();
|
|
136
|
+
chords.forEach((chord) => {
|
|
137
|
+
note.removeLyric(chord);
|
|
138
|
+
});
|
|
139
|
+
chords.forEach((chord) => {
|
|
140
|
+
const nchord = SmoLyric.transposeChordToKey(chord, keyOffset, selection.measure.keySignature, destKey);
|
|
141
|
+
note.addLyric(nchord);
|
|
142
|
+
});
|
|
134
143
|
const pasteNote: PasteNote = {
|
|
135
144
|
selector,
|
|
136
145
|
note,
|
|
@@ -517,7 +526,47 @@ export class PasteBuffer {
|
|
|
517
526
|
}
|
|
518
527
|
serializedMeasure.voices = voices;
|
|
519
528
|
}
|
|
520
|
-
|
|
529
|
+
pasteChords(selector: SmoSelector) {
|
|
530
|
+
if (this.notes.length < 1) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!this.score) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
let srcTick = 0;
|
|
537
|
+
let destTick = 0;
|
|
538
|
+
let srcIndex = 0;
|
|
539
|
+
let selection = SmoSelection.noteSelection(this.score!, selector.staff, selector.measure, selector.voice, selector.tick);
|
|
540
|
+
while (selection && selection.note && srcIndex < this.notes.length) {
|
|
541
|
+
const srcNote = this.notes[srcIndex].note;
|
|
542
|
+
const chords = srcNote.getChords();
|
|
543
|
+
if (selection && selection.note) {
|
|
544
|
+
const destNote = selection.note;
|
|
545
|
+
if (chords.length) {
|
|
546
|
+
chords.forEach((chord) => {
|
|
547
|
+
destNote.removeLyric(chord);
|
|
548
|
+
});
|
|
549
|
+
chords.forEach((chord) => {
|
|
550
|
+
if (selection) {
|
|
551
|
+
const nchord = SmoLyric.transposeChordToKey(
|
|
552
|
+
chord, selection.measure.transposeIndex,this.notes[srcIndex].originalKey, selection.measure.keySignature);
|
|
553
|
+
destNote.addLyric(nchord);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
srcTick += srcNote.tickCount;
|
|
558
|
+
while (selection && selection.note && destTick < srcTick) {
|
|
559
|
+
destTick += selection.note.tickCount;
|
|
560
|
+
if (selection && selection.note) {
|
|
561
|
+
const curSelector = selection.selector;
|
|
562
|
+
selection = SmoSelection.nextNoteSelection(this.score,
|
|
563
|
+
curSelector.staff, curSelector.measure, curSelector.voice, curSelector.tick);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
srcIndex += 1;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
521
570
|
pasteSelections(selector: SmoSelector) {
|
|
522
571
|
let i = 0;
|
|
523
572
|
if (this.notes.length < 1) {
|
|
@@ -87,11 +87,26 @@ export class SmoOperation {
|
|
|
87
87
|
});
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Move a single stave up or down one. If last down, move to first.
|
|
92
|
+
* If first up, move to last
|
|
93
|
+
* @param score
|
|
94
|
+
* @param selection
|
|
95
|
+
* @param index
|
|
96
|
+
*/
|
|
90
97
|
static moveStaffUpDown(score: SmoScore, selection: SmoSelection, index: number) {
|
|
91
98
|
const index1 = selection.selector.staff;
|
|
92
99
|
const index2 = selection.selector.staff + index;
|
|
93
100
|
if (index2 < score.staves.length && index2 >= 0) {
|
|
94
101
|
score.swapStaves(index1, index2);
|
|
102
|
+
} else if (index2 === score.staves.length) {
|
|
103
|
+
for (let i = 0; i < (score.staves.length - 1); ++i) {
|
|
104
|
+
score.swapStaves((score.staves.length - 1) - i, (score.staves.length - 1) - (i + 1));
|
|
105
|
+
}
|
|
106
|
+
} else if (index2 < 0) {
|
|
107
|
+
for (let i = 0; i < (score.staves.length - 1); ++i) {
|
|
108
|
+
score.swapStaves(i, i + 1);
|
|
109
|
+
}
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
112
|
|
|
@@ -38,7 +38,12 @@ export class SuiInstrumentAdapter extends SuiComponentAdapter {
|
|
|
38
38
|
this.view.changeInstrument(this.instrument, this.selections);
|
|
39
39
|
this.instrument = new SmoInstrument(this.instrument);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
get lines() {
|
|
42
|
+
return this.instrument.lines;
|
|
43
|
+
}
|
|
44
|
+
set lines(value: number) {
|
|
45
|
+
this.writeNumParam('lines', value);
|
|
46
|
+
}
|
|
42
47
|
get transposeIndex() {
|
|
43
48
|
return this.instrument.keyOffset;
|
|
44
49
|
}
|
|
@@ -108,6 +113,11 @@ export class SuiInstrumentDialog extends SuiDialogAdapterBase<SuiInstrumentAdapt
|
|
|
108
113
|
label: 'Instrument Properties',
|
|
109
114
|
elements:
|
|
110
115
|
[{
|
|
116
|
+
smoName: 'lines',
|
|
117
|
+
defaultValue: 5,
|
|
118
|
+
control: 'SuiRockerComponent',
|
|
119
|
+
label: 'Staff lines (1-5)'
|
|
120
|
+
}, {
|
|
111
121
|
smoName: 'transposeIndex',
|
|
112
122
|
defaultValue: 0,
|
|
113
123
|
control: 'SuiRockerComponent',
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { SuiMenuBase, SuiMenuParams, SuiConfiguredMenuOption, SuiConfiguredMenu } from './menu';
|
|
2
|
+
|
|
3
|
+
declare var $: any;
|
|
4
|
+
/**
|
|
5
|
+
* Stuff you can do to notes
|
|
6
|
+
* @category SuiMenu
|
|
7
|
+
*/
|
|
8
|
+
export class SuiEditMenu extends SuiConfiguredMenu {
|
|
9
|
+
constructor(params: SuiMenuParams) {
|
|
10
|
+
super(params, 'Edit', SuiEditMenuOptions);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Copy Ctrl^C action
|
|
15
|
+
* @category SuiMenu
|
|
16
|
+
*/
|
|
17
|
+
const copyMenuOption: SuiConfiguredMenuOption = {
|
|
18
|
+
handler: async (menu: SuiMenuBase) => {
|
|
19
|
+
await menu.view.copy();
|
|
20
|
+
}, display: (menu: SuiMenuBase) => true,
|
|
21
|
+
menuChoice: {
|
|
22
|
+
icon: 'icon-copy',
|
|
23
|
+
text: 'Copy',
|
|
24
|
+
value: 'copyAction'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Copy Ctrl^C action
|
|
30
|
+
* @category SuiMenu
|
|
31
|
+
*/
|
|
32
|
+
const pasteMenuOption: SuiConfiguredMenuOption = {
|
|
33
|
+
handler: async (menu: SuiMenuBase) => {
|
|
34
|
+
await menu.view.paste();
|
|
35
|
+
}, display: (menu: SuiMenuBase) => true,
|
|
36
|
+
menuChoice: {
|
|
37
|
+
icon: 'icon-paste',
|
|
38
|
+
text: 'Paste',
|
|
39
|
+
value: 'pasteAction'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Copy Ctrl^C action
|
|
45
|
+
* @category SuiMenu
|
|
46
|
+
*/
|
|
47
|
+
const pasteChordsMenuOption: SuiConfiguredMenuOption = {
|
|
48
|
+
handler: async (menu: SuiMenuBase) => {
|
|
49
|
+
await menu.view.pasteChords();
|
|
50
|
+
}, display: (menu: SuiMenuBase) => true,
|
|
51
|
+
menuChoice: {
|
|
52
|
+
icon: '',
|
|
53
|
+
text: 'Paste Chords',
|
|
54
|
+
value: 'pasteChordsAction'
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Copy Ctrl^C action
|
|
60
|
+
* @category SuiMenu
|
|
61
|
+
*/
|
|
62
|
+
const undoMenuOption: SuiConfiguredMenuOption = {
|
|
63
|
+
handler: async (menu: SuiMenuBase) => {
|
|
64
|
+
await menu.view.undo();
|
|
65
|
+
}, display: (menu: SuiMenuBase) => true,
|
|
66
|
+
menuChoice: {
|
|
67
|
+
icon: 'icon-undo',
|
|
68
|
+
text: 'Undo',
|
|
69
|
+
value: 'undoAction'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Note menu. Stuff you can do to notes.
|
|
75
|
+
* @category SuiMenu
|
|
76
|
+
*/
|
|
77
|
+
const SuiEditMenuOptions: SuiConfiguredMenuOption[] = [
|
|
78
|
+
copyMenuOption, pasteMenuOption, pasteChordsMenuOption, undoMenuOption
|
|
79
|
+
];
|
package/src/ui/menus/manager.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { SuiStaffModifierMenu } from './staffModifier';
|
|
|
24
24
|
import { SuiMeasureMenu } from './measure';
|
|
25
25
|
import { SuiVoiceMenu } from './voices';
|
|
26
26
|
import { SuiNoteMenu } from './note';
|
|
27
|
+
import { SuiEditMenu } from './edit';
|
|
27
28
|
import { SuiTextMenu } from './text';
|
|
28
29
|
import { SuiPartSelectionMenu } from './partSelection';
|
|
29
30
|
import { SuiPartMenu } from './parts';
|
|
@@ -275,6 +276,8 @@ export class SuiMenuManager {
|
|
|
275
276
|
this.displayMenu(new SuiLanguageMenu(params));
|
|
276
277
|
} else if (action === 'SuiFileMenu') {
|
|
277
278
|
this.displayMenu(new SuiFileMenu(params));
|
|
279
|
+
} else if (action === 'SuiEditMenu') {
|
|
280
|
+
this.displayMenu(new SuiEditMenu(params));
|
|
278
281
|
} else if (action === 'SuiScoreMenu') {
|
|
279
282
|
this.displayMenu(new SuiScoreMenu(params));
|
|
280
283
|
} else if (action === 'SuiPartSelectionMenu') {
|
|
@@ -23,7 +23,7 @@ export class defaultRibbonLayout {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
static get leftRibbonIds() {
|
|
26
|
-
return ['helpDialog', 'languageMenu', 'fileMenu',
|
|
26
|
+
return ['helpDialog', 'languageMenu', 'fileMenu', 'editMenu',
|
|
27
27
|
'scoreMenu', 'partMenu', 'staffModifierMenu', 'measureModal', 'voiceMenu', 'beamMenu',
|
|
28
28
|
'tupletMenu', 'noteMenu', 'textMenu', 'libraryMenu',
|
|
29
29
|
];
|
|
@@ -174,11 +174,21 @@ export class defaultRibbonLayout {
|
|
|
174
174
|
ctor: 'SuiLanguageMenu',
|
|
175
175
|
group: 'scoreEdit',
|
|
176
176
|
id: 'languageMenu'
|
|
177
|
+
}, {
|
|
178
|
+
leftText: 'Edit',
|
|
179
|
+
rightText: 'Alt-e',
|
|
180
|
+
hotKey: 'e',
|
|
181
|
+
icon: '',
|
|
182
|
+
classes: 'file-modify nav-link link-body-emphasis hover-text',
|
|
183
|
+
action: 'menu',
|
|
184
|
+
ctor: 'SuiEditMenu',
|
|
185
|
+
group: 'scoreEdit',
|
|
186
|
+
id: 'editMenu'
|
|
177
187
|
}, {
|
|
178
188
|
leftText: 'File',
|
|
179
189
|
rightText: 'Alt-f',
|
|
180
190
|
hotKey: 'f',
|
|
181
|
-
icon: '',
|
|
191
|
+
icon: 'icon-clipboard',
|
|
182
192
|
classes: 'file-modify nav-link link-body-emphasis hover-text',
|
|
183
193
|
action: 'menu',
|
|
184
194
|
ctor: 'SuiFileMenu',
|