Skip to content

Commit

Permalink
beam and voice buttons
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronDavidNewman committed Sep 22, 2024
1 parent 66a2d29 commit 52dd9a7
Show file tree
Hide file tree
Showing 7 changed files with 381 additions and 15 deletions.
22 changes: 22 additions & 0 deletions changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,28 @@


## Changes to Smoosic
### September, 2023
Thanks to [Nenad Strangar](https://github.com/strangarnenad) we now have nested tuplets!

This is the first major UI reboot. Originally, I didn't even want to have a UI, I only wanted to focus on rendering and optimization,
and let other people adapt the UI to their taste.

But at the same time, I didn't make it so obvious how to do this. And it's hard to get people to try Smoosic out if they have to first
memorize a bunch of keyboard commands. Also, my own vanity doesn't like having an ugly interface on my pet project.

Here are some highlights:

* It should be possible to find a menu option/dialog box for any operation in Smoosic. But using the keyboard for basic note editing and navigation is still encouraged.
* The button ribbon on the top was getting a little out of control, and the icons were cryptic. I moved most of these functions into menu/dialog options from the left menu. The ribbon is now reserved for a few commonly-used operations.
* The menus on the left are broken down in terms of musical heirarchy: Score, Parts (Staves), Measures etc.

On the development side:

* I elected to use Bootstrap for the styling (not the UI). It seemed like a good balance between non-opinionated and not reinventing menus and buttons.
* I strongly considered using Vue.js or ReactJs as a UI framework, but at this point it doesn't seem like a good fit. I may move towards some light Vue usage at some point, with the goal of using reactive variables where appropriate.
* I am moving away from the huge JSON files to outline UI elements. Instead, menus and dialogs can be created with just a few lines of code and pushed into the UI with code. This should allow for more adaptive UI elements.


### November, 2022
Where to start...Smoosic has been disassembled and reassembled multiple times. A project like this is never 'complete', but pretty much everything that I intended to do with Smoosic is now working, to some extent:

Expand Down
2 changes: 1 addition & 1 deletion release/library/soprano/Postillionlied.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/application/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ import { SuiMenuBase } from '../ui/menus/menu';
import { SuiScoreMenu } from '../ui/menus/score';
import { SuiTextMenu } from '../ui/menus/text';
import { SuiPartMenu } from '../ui/menus/parts';
import { SuiVoiceMenu } from '../ui/menus/voices';
import { SuiBeamMenu } from '../ui/menus/beams';
import { SuiPartSelectionMenu } from '../ui/menus/partSelection';
import { SuiDynamicsMenu } from '../ui/menus/dynamics';
import { SuiTimeSignatureMenu } from '../ui/menus/timeSignature';
Expand Down Expand Up @@ -188,7 +190,7 @@ export const Smo = {
SuiMenuManager, SuiMenuBase, SuiScoreMenu, SuiFileMenu,
SuiDynamicsMenu, SuiTimeSignatureMenu, SuiKeySignatureMenu, SuiStaffModifierMenu,
SuiLanguageMenu, SuiMeasureMenu, SuiNoteMenu, SmoLanguage, SmoTranslator, SuiPartMenu,
SuiPartSelectionMenu, SuiTextMenu,
SuiPartSelectionMenu, SuiTextMenu, SuiVoiceMenu, SuiBeamMenu,
// Dialogs
SuiGraceNoteAdapter, SuiGraceNoteDialog, SuiGraceNoteButtonsComponent,
SuiTempoDialog, SuiInstrumentDialog, SuiModifierDialogFactory, SuiLibraryDialog,
Expand Down
203 changes: 203 additions & 0 deletions src/ui/menus/beams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { SuiMenuBase, SuiMenuParams, MenuDefinition, SuiMenuHandler, SuiMenuShowOption,
SuiConfiguredMenuOption, SuiConfiguredMenu } from './menu';

declare var $: any;
export class SuiBeamMenu extends SuiConfiguredMenu {
constructor(params: SuiMenuParams) {
super(params, 'Beams', SuiBeamMenuOptions);
}
}

const toggleBeamGroupMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.toggleBeamGroup();
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
for (let j = 0; j < mm.voices.length; ++j) {
const vv = mm.voices[j];
for (let k = 0; k < vv.notes.length; ++k) {
const nn = vv.notes[k];
if (nn) {
if (nn.noteType === 'n' && nn.tickCount < 4096) {
return true;
}
}
}
}
}
return false;
},
menuChoice: {
icon: 'icon smo-icon icon-beamBreak',
text: 'Toggle Beam Group',
hotkey: 'x',
value: 'toggleBeamMenuOption'
}
}
const beamSelectionsMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.beamSelections();
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
for (let j = 0; j < mm.voices.length; ++j) {
const vv = mm.voices[j];
for (let k = 0; k < vv.notes.length; ++k) {
const nn = vv.notes[k];
if (nn) {
if (nn.noteType === 'n' && nn.tickCount < 4096) {
return true;
}
}
}
}
}
return false;
},
menuChoice: {
icon: 'icon smo-icon icon-beam',
text: 'Beam Selections',
hotkey: 'Shift-X',
value: 'beamSelectionsMenuOption'
}
}
const toggleBeamDirectionMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.toggleBeamDirection();
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
for (let j = 0; j < mm.voices.length; ++j) {
const vv = mm.voices[j];
for (let k = 0; k < vv.notes.length; ++k) {
const nn = vv.notes[k];
if (nn) {
if (nn.noteType === 'n') {
return true;
}
}
}
}
}
return false;
},
menuChoice: {
icon: 'icon icon-smo icon-flagFlip',
text: 'Toggle Stem Direction (auto, up, down)',
hotkey: 'Shift-B',
value: 'toggleBeamDirection'
}
}
const tripletMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.makeTuplet(3);
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
for (let j = 0; j < mm.voices.length; ++j) {
const vv = mm.voices[j];
for (let k = 0; k < vv.notes.length; ++k) {
const nn = vv.notes[k];
if (nn) {
if (nn.noteType === 'n') {
return true;
}
}
}
}
}
return false;
},
menuChoice: {
icon: ' icon icon-smo icon-triplet',
text: 'Make Triplet',
hotkey: 'Ctrl-3',
value: 'tripletMenuOption'
}
}
const quintupletMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.makeTuplet(3);
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
for (let j = 0; j < mm.voices.length; ++j) {
const vv = mm.voices[j];
for (let k = 0; k < vv.notes.length; ++k) {
const nn = vv.notes[k];
if (nn) {
if (nn.noteType === 'n') {
return true;
}
}
}
}
}
return false;
},
menuChoice: {
icon: 'icon-smo icon-quint',
text: 'Make 5-tuplet',
hotkey: 'Ctrl-5',
value: 'quintupletMenuOption'
}
}
const sevenTupletMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.makeTuplet(3);
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
for (let j = 0; j < mm.voices.length; ++j) {
const vv = mm.voices[j];
for (let k = 0; k < vv.notes.length; ++k) {
const nn = vv.notes[k];
if (nn) {
if (nn.noteType === 'n') {
return true;
}
}
}
}
}
return false;
},
menuChoice: {
icon: 'icon-smo icon icon-septuplet',
hotkey: 'Ctrl-7',
text: 'Make 7-tuplet',
value: 'sevenTupletMenuOption'
}
}
const removeTupletMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.unmakeTuplet();
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
for (let j = 0; j < mm.voices.length; ++j) {
const vv = mm.voices[j];
for (let k = 0; k < vv.notes.length; ++k) {
const nn = vv.notes[k];
if (nn) {
if (nn.isTuplet) {
return true;
}
}
}
}
}
return false;
},
menuChoice: {
icon: 'icon icon-smo icon-no_tuplet',
text: 'Unmake tuplet',
hotkey: 'Ctrl-0',
value: 'unmakeTuplet'
}
}
const SuiBeamMenuOptions: SuiConfiguredMenuOption[] = [toggleBeamGroupMenuOption,
beamSelectionsMenuOption, toggleBeamDirectionMenuOption, tripletMenuOption, quintupletMenuOption,
sevenTupletMenuOption, removeTupletMenuOption
];
26 changes: 25 additions & 1 deletion src/ui/menus/parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,33 @@ export const tabStaveMenuOption: SuiConfiguredMenuOption = {
});
}
}
export const moveUpMenuOption: SuiConfiguredMenuOption = {
menuChoice: {
icon: 'icon-smo icon-arrow-up',
text: 'Move Part Up',
value: 'partUp'
}, display: (menu: SuiMenuBase) => {
return menu.view.score.staves.length > 1;
},
handler: async (menu: SuiMenuBase) => {
await menu.view.moveStaffUpDown(-1);
}
}
export const moveDownMenuOption: SuiConfiguredMenuOption = {
menuChoice: {
icon: 'icon-smo icon-arrow-up',
text: 'Move Part Down',
value: 'partDown'
}, display: (menu: SuiMenuBase) => {
return menu.view.score.staves.length > 1;
},
handler: async (menu: SuiMenuBase) => {
await menu.view.moveStaffUpDown(1);
}
}
export const SuiPartMenuOptions: SuiConfiguredMenuOption[] = [
createNotePartMenuOption, removePartMenuOption, partPropertiesMenuOption, pageLayoutMenuOption, viewPartialScoreMenuOption,
editInstrumentMenuOption, viewFullScoreMenuOption, tabStaveMenuOption
editInstrumentMenuOption, viewFullScoreMenuOption, tabStaveMenuOption, moveUpMenuOption, moveDownMenuOption
];

export class SuiPartMenu extends SuiConfiguredMenu {
Expand Down
105 changes: 105 additions & 0 deletions src/ui/menus/voices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { SuiMenuBase, SuiMenuParams, MenuDefinition, SuiMenuHandler, SuiMenuShowOption,
SuiConfiguredMenuOption, SuiConfiguredMenu } from './menu';
import { createAndDisplayDialog } from '../dialogs/dialog';

declare var $: any;
export class SuiVoiceMenu extends SuiConfiguredMenu {
constructor(params: SuiMenuParams) {
super(params, 'Voices', SuiVoiceMenuOptions);
}
}

const selectVoiceOneMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.populateVoice(0);
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
if (mm.voices.length > 1) {
return true;
}
}
return false;
},
menuChoice: {
icon: '',
text: 'Voice 1',
value: 'voiceOne'
}
}
const selectVoiceTwoMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.populateVoice(1);
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
if (mm.voices.length < 4) {
return true;
}
}
return false;
},
menuChoice: {
icon: '',
text: 'Voice 2',
value: 'voiceTwo'
}
}
const selectVoiceThreeMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.populateVoice(2);
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
if (mm.voices.length < 4 && mm.voices.length > 1) {
return true;
}
}
return false;
},
menuChoice: {
icon: '',
text: 'Voice 3',
value: 'voiceThree'
}
}
const selectVoiceFourMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.populateVoice(3);
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
if (mm.voices.length < 4 && mm.voices.length > 2) {
return true;
}
}
return false;
},
menuChoice: {
icon: '',
text: 'Voice 4',
value: 'voiceFour'
}
}
const removeVoiceMenuOption: SuiConfiguredMenuOption = {
handler: async (menu: SuiMenuBase) => {
menu.view.depopulateVoice();
}, display: (menu: SuiMenuBase) => {
for (let i = 0; i < menu.view.tracker.selections.length; ++i) {
const mm = menu.view.tracker.selections[i].measure;
if (mm.activeVoice > 0) {
return true;
}
}
return false;
},
menuChoice: {
icon: '',
text: 'Remove Voice',
value: 'removeVoice'
}
}
const SuiVoiceMenuOptions: SuiConfiguredMenuOption[] = [
selectVoiceOneMenuOption, selectVoiceTwoMenuOption, selectVoiceThreeMenuOption, selectVoiceFourMenuOption,
removeVoiceMenuOption
];
Loading

0 comments on commit 52dd9a7

Please sign in to comment.