smoosic
Version:
<sub>[Github site](https://github.com/Smoosic/smoosic) | [source documentation](https://smoosic.github.io/Smoosic/release/docs/modules.html) | [change notes](https://aarondavidnewman.github.io/Smoosic/changes.html) | [application](https://smoosic.github.i
331 lines (309 loc) • 10.7 kB
text/typescript
// [Smoosic](https://github.com/AaronDavidNewman/Smoosic)
// Copyright (c) Aaron David Newman 2021.
import { KeyEvent, keyEventMatch } from '../smo/data/common';
import { SuiExceptionHandler } from '../ui/exceptions';
import { Qwerty } from '../ui/qwerty';
import { SuiModifierDialogFactory } from '../ui/dialogs/factory';
import { SuiPiano } from '../render/sui/piano'
import { SuiHelp } from '../ui/help';
import { CompleteNotifier, ModalComponent } from '../ui/common';
import { SuiTracker } from '../render/sui/tracker';
import { defaultEditorKeys } from '../ui/keyBindings/default/editorKeys';
import { defaultTrackerKeys } from '../ui/keyBindings/default/trackerKeys';
import { SuiScoreViewOperations } from '../render/sui/scoreViewOperations';
import { BrowserEventSource, EventHandler } from '../ui/eventSource';
import { SuiKeyCommands } from './keyCommands';
import { KeyBinding, ModalEventHandler } from './common';
import { ModifierTab } from '../smo/xform/selections';
import { SvgHelpers } from '../render/sui/svgHelpers';
import { SuiMenuManager } from '../ui/menus/manager';
import { SmoConfiguration } from './configuration';
import { SuiDom } from './dom';
declare var $: any;
/**
* Handle keyboard/mouse events, and pass them to the renderer and other UI elements.
* @category SuiApplication
*/
export interface EventHandlerParams {
view: SuiScoreViewOperations,
eventSource: BrowserEventSource,
tracker: SuiTracker,
keyCommands: SuiKeyCommands,
menus: SuiMenuManager,
completeNotifier: CompleteNotifier,
keyBindings: KeyBinding[],
config: SmoConfiguration
}
/**
* this is the default keyboard/mouse handler for smoosic in application mode.
* It diverts key events to tracker or key commmands as appropriate, and mouse events to
* tracker. Modal elements take this control away temporarily.
*
* It also handles some global events such as window resize and scroll of the music region.
* @category SuiApplication
*/
export class SuiEventHandler implements ModalEventHandler {
static reentry: boolean = false;
static keyboardUi: Qwerty;
static showQwerty() {
SuiEventHandler.keyboardUi = Qwerty;
Qwerty.displayKb();
}
static instance: SuiEventHandler;
static debugMask: number = 0;
static altKeyPressed: boolean = false;
static ctrlKeyPressed: boolean = false;
static shiftKeyPressed: boolean = false;
view: SuiScoreViewOperations;
eventSource: BrowserEventSource;
tracker: SuiTracker;
keyBind: KeyBinding[];
completeNotifier: CompleteNotifier;
keyCommands: SuiKeyCommands;
resizing: boolean = false;
undoStatus: number = 0;
trackScrolling: boolean = false;
config: SmoConfiguration;
keyHandlerObj: any = null;
menus: SuiMenuManager;
piano: SuiPiano | null = null;
exhandler: SuiExceptionHandler;
constructor(params: EventHandlerParams) {
SuiEventHandler.instance = this;
this.view = params.view;
this.config = params.config;
this.menus = params.menus;
this.completeNotifier = params.completeNotifier;
this.eventSource = params.eventSource;
this.tracker = params.tracker; // needed for key event handling
this.keyBind = params.keyBindings;
this.keyCommands = params.keyCommands;
this.keyCommands.view = this.view;
this.resizing = false;
this.undoStatus = 0;
this.trackScrolling = false;
this.keyHandlerObj = null;
// create global exception instance
this.exhandler = new SuiExceptionHandler(this);
this.bindEvents();
this.bindResize();
this.createPiano();
}
static get scrollable() {
return '.musicRelief';
}
private static handleScrollEventDefer(handler: SuiEventHandler) {
if (handler.trackScrolling) {
return;
}
const scrollRegion: HTMLElement | null = document.getElementById(SuiDom.scrollRegionId);
setTimeout(() => {
handler.trackScrolling = false;
if (scrollRegion) {
const scrollLeft = scrollRegion.scrollLeft;
const scrollTop = scrollRegion.scrollTop;
handler.view.handleScrollEvent(scrollLeft, scrollTop);
}
}, 500);
}
handleScrollEvent() {
SuiEventHandler.handleScrollEventDefer(this);
}
createPiano() {
this.piano = new SuiPiano(this.view);
}
resizeEvent() {
var self = this;
if (this.resizing) {
return;
}
if (!this.piano) {
return;
}
if ($('body').hasClass('printing')) {
return;
}
this.resizing = true;
setTimeout(function () {
if (SuiEventHandler.debugMask) {
console.log('resizing');
}
self.resizing = false;
self.piano!.handleResize();
self.view.refreshViewport();
}, 1);
}
createModifierDialog(modifierSelection: ModifierTab) {
var parameters = {
modifier: modifierSelection.modifier,
view: this.view, eventSource: this.eventSource,
completeNotifier: this.completeNotifier, keyCommands: this.keyCommands,
ctor: '', // filled in by the factory
tracker: this.tracker,
startPromise: null,
id: 'modifier-dialog',
config: this.config
}
return SuiModifierDialogFactory.createModifierDialog(modifierSelection.modifier, parameters);
}
// If the user has selected a modifier via the mouse/touch, bring up mod dialog
// for that modifier
trackerModifierSelect(ev: KeyEvent) {
var modSelection = this.view.tracker.getSelectedModifier();
if (modSelection) {
var dialog = this.createModifierDialog(modSelection);
if (dialog) {
// this.view.tracker.selectSuggestion(ev);
return;
// this.unbindKeyboardForModal(dialog);
} else {
this.view.tracker.advanceModifierSelection(ev);
}
} else {
this.view.tracker.selectSuggestion(ev);
}
return;
}
// ### bindResize
// This handles both resizing of the music area (scrolling) and resizing of the window.
// The latter results in a redraw, the former just resets the client/logical map of elements
// in the tracker.
bindResize() {
const self = this;
const el: HTMLElement = $(SuiEventHandler.scrollable)[0];
// unit test programs don't have resize html
if (!el) {
return;
}
window.addEventListener('resize', function () {
self.resizeEvent();
});
let scrollCallback = () => {
self.handleScrollEvent();
};
el.onscroll = scrollCallback;
}
// ### renderElement
// return render element that is the DOM parent of the svg
get renderElement() {
return this.view.renderer.renderElement;
}
// ## editorKeyBindingDefaults
// ## Description:
// execute a simple command on the editor, based on a keystroke.
static get editorKeyBindingDefaults() {
return defaultEditorKeys.keys;
}
// ## trackerKeyBindingDefaults
// ### Description:
// Key bindings for the tracker. The tracker is the 'cursor' in the music
// that lets you select and edit notes.
static get trackerKeyBindingDefaults() {
return defaultTrackerKeys.keys;
}
helpControls() {
var self = this;
var rebind = function () {
self.bindEvents();
}
}
menuHelp() {
SuiHelp.displayHelp();
}
keyUp(evdata: any) {
if (!evdata.ctrlKey && SuiEventHandler.ctrlKeyPressed) {
$('body').removeClass('ctrl-key');
SuiEventHandler.ctrlKeyPressed = false;
}
if (!evdata.shiftKey && SuiEventHandler.shiftKeyPressed) {
$('body').removeClass('shift-key');
SuiEventHandler.shiftKeyPressed = false;
}
if (!evdata.altKey && SuiEventHandler.altKeyPressed) {
$('body').removeClass('alt-key');
SuiEventHandler.altKeyPressed = false;
}
}
handleMetaKeyDown(evdata: any) {
if (evdata.ctrlKey && !SuiEventHandler.ctrlKeyPressed) {
$('body').addClass('ctrl-key');
SuiEventHandler.ctrlKeyPressed = true;
}
if (evdata.shiftKey && !SuiEventHandler.shiftKeyPressed) {
$('body').addClass('shift-key');
SuiEventHandler.shiftKeyPressed = true;
}
if (evdata.altKey && !SuiEventHandler.altKeyPressed) {
$('body').addClass('alt-key');
SuiEventHandler.altKeyPressed = true;
}
}
async evKey(evdata: any) {
if ($('body').hasClass('translation-mode')) {
return;
}
this.handleMetaKeyDown(evdata);
if (SuiEventHandler.debugMask) {
console.log("KeyboardEvent: key='" + evdata.key + "' | code='" +
evdata.code + "'"
+ " shift='" + evdata.shiftKey + "' control='" + evdata.ctrlKey + "'" + " alt='" + evdata.altKey + "'");
}
evdata.preventDefault();
if (SuiEventHandler.keyboardUi) {
Qwerty.handleKeyEvent(evdata);
}
const dataCopy = SuiTracker.serializeEvent(evdata);
await this.view.renderer.updatePromise();
if (dataCopy.key == '?') {
SuiHelp.displayHelp();
}
if (dataCopy.key == 'Enter') {
this.trackerModifierSelect(dataCopy);
}
var binding: KeyBinding | undefined = this.keyBind.find((ev: KeyBinding) =>
ev.event === 'keydown' && ev.key === dataCopy.key &&
ev.ctrlKey === dataCopy.ctrlKey &&
ev.altKey === dataCopy.altKey && dataCopy.shiftKey === ev.shiftKey);
if (binding) {
try {
if (binding.module === 'tracker') {
(this.tracker as any)[binding.action](dataCopy);
} else {
(this.keyCommands as any)[binding.action](dataCopy);
}
} catch (e) {
if (typeof (e) === 'string') {
console.error(e);
}
this.exhandler.exceptionHandler(e);
}
}
}
mouseMove(ev: any) {
this.view.tracker.intersectingArtifact(SvgHelpers.smoBox({
x: ev.clientX,
y: ev.clientY
}));
}
mouseClick(ev: any) {
const dataCopy = SuiTracker.serializeEvent(ev);
this.view.renderer.updatePromise().then(() => {
this.view.tracker.selectSuggestion(dataCopy);
var modifier = this.view.tracker.getSelectedModifier();
if (modifier) {
this.createModifierDialog(modifier);
}
});
}
bindEvents() {
const self = this;
const tracker = this.view.tracker;
$('body').off('forceScrollEvent').on('forceScrollEvent', function () {
self.handleScrollEvent();
});
$('body').off('forceResizeEvent').on('forceResizeEvent', function () {
self.resizeEvent();
});
this.helpControls();
}
}