import { random } from 'lodash'; import { action, runInAction } from 'mobx'; import { Doc, DocListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkTool } from '../../fields/InkField'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, PromiseValue } from '../../fields/Types'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DragManager } from '../util/DragManager'; import { GroupManager } from '../util/GroupManager'; import { SelectionManager } from '../util/SelectionManager'; import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; import { UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { CollectionFreeFormViewChrome } from './collections/CollectionMenu'; import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; import { MainView } from './MainView'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { OpenWhereMod } from './nodes/DocumentView'; import { AnchorMenu } from './pdf/AnchorMenu'; const modifiers = ['control', 'meta', 'shift', 'alt']; type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo; type KeyControlInfo = { preventDefault: boolean; stopPropagation: boolean; }; export class KeyManager { public static Instance: KeyManager = new KeyManager(); private router = new Map(); constructor() { const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; // SHIFT CONTROL ALT META this.router.set('0000', this.unmodified); this.router.set(isMac ? '0001' : '0100', this.ctrl); this.router.set(isMac ? '0100' : '0010', this.alt); this.router.set(isMac ? '1001' : '1100', this.ctrl_shift); this.router.set('1000', this.shift); } public unhandle = action((e: KeyboardEvent) => { if (e.key?.toLowerCase() === 'shift') runInAction(() => (DocumentDecorations.Instance.AddToSelection = false)); }); public handle = action((e: KeyboardEvent) => { if (e.key?.toLowerCase() === 'shift') DocumentDecorations.Instance.AddToSelection = true; //if (!Doc.noviceMode && e.key.toLocaleLowerCase() === "shift") DocServer.UPDATE_SERVER_CACHE(true); const keyname = e.key && e.key.toLowerCase(); this.handleGreedy(keyname); if (modifiers.includes(keyname)) { return; } const bit = (value: boolean) => (value ? '1' : '0'); const modifierIndex = bit(e.shiftKey) + bit(e.ctrlKey) + bit(e.altKey) + bit(e.metaKey); const handleConstrained = this.router.get(modifierIndex); if (!handleConstrained) { return; } const control = handleConstrained(keyname, e); control.stopPropagation && e.stopPropagation(); control.preventDefault && e.preventDefault(); }); private handleGreedy = action((keyname: string) => { switch (keyname) { } }); private unmodified = action((keyname: string, e: KeyboardEvent) => { const hasFffView = SelectionManager.Views().some(dv => dv.props.CollectionFreeFormDocumentView?.()); switch (keyname) { case 'u': if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { return { stopPropagation: false, preventDefault: false }; } const ungroupings = SelectionManager.Views().slice(); UndoManager.RunInBatch(() => ungroupings.map(dv => (dv.layoutDoc.group = undefined)), 'ungroup'); SelectionManager.DeselectAll(); break; case 'g': if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { return { stopPropagation: false, preventDefault: false }; } const groupings = SelectionManager.Views().slice(); const randomGroup = random(0, 1000); const collectionView = groupings.reduce( (col, g) => (col === null || g.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView === col ? g.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView : undefined), null as null | undefined | CollectionFreeFormView ); if (collectionView) { UndoManager.RunInBatch(() => { collectionView._marqueeViewRef.current?.collection( e, true, groupings.map(g => g.rootDoc) ); }, 'grouping'); break; } UndoManager.RunInBatch(() => groupings.map(dv => (dv.layoutDoc.group = randomGroup)), 'group'); SelectionManager.DeselectAll(); break; case ' ': // MarqueeView.DragMarquee = !MarqueeView.DragMarquee; // bcz: this needs a better disclosure UI break; case 'escape': DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; InkStrokeProperties.Instance._controlButton = false; Doc.ActiveTool = InkTool.None; DragManager.CompleteWindowDrag?.(true); var doDeselect = true; if (SnappingManager.GetIsDragging()) { DragManager.AbortDrag(); } else if (CollectionDockingView.Instance?.HasFullScreen) { CollectionDockingView.Instance?.CloseFullScreen(); } else if (CollectionStackedTimeline.SelectingRegion) { CollectionStackedTimeline.SelectingRegion = undefined; doDeselect = false; } else { doDeselect = !ContextMenu.Instance.closeMenu(); } if (doDeselect) { SelectionManager.DeselectAll(); LightboxView.SetLightboxDoc(undefined); } // DictationManager.Controls.stop(); GoogleAuthenticationManager.Instance.cancel(); SharingManager.Instance.close(); if (!GroupManager.Instance.isOpen) SettingsManager.Instance.close(); GroupManager.Instance.close(); CollectionFreeFormViewChrome.Instance?.clearKeepPrimitiveMode(); window.getSelection()?.empty(); document.body.focus(); break; case 'enter': { //DocumentDecorations.Instance.onCloseClick(false); break; } case 'delete': case 'backspace': if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { if (LightboxView.LightboxDoc) { LightboxView.SetLightboxDoc(undefined); SelectionManager.DeselectAll(); } else DocumentDecorations.Instance.onCloseClick(true); return { stopPropagation: true, preventDefault: true }; } break; case 'arrowleft': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge(-1, 0)), 'nudge left'); return { stopPropagation: hasFffView, preventDefault: hasFffView }; case 'arrowright': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(1, 0)), 'nudge right'); return { stopPropagation: hasFffView, preventDefault: hasFffView }; case 'arrowup': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, -1)), 'nudge up'); return { stopPropagation: hasFffView, preventDefault: hasFffView }; case 'arrowdown': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, 1)), 'nudge down'); return { stopPropagation: hasFffView, preventDefault: hasFffView }; } return { stopPropagation: false, preventDefault: false, }; }); private shift = action((keyname: string) => { const stopPropagation = false; const preventDefault = false; switch (keyname) { case 'arrowleft': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(-10, 0)), 'nudge left'); break; case 'arrowright': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(10, 0)), 'nudge right'); break; case 'arrowup': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, -10)), 'nudge up'); break; case 'arrowdown': UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, 10)), 'nudge down'); break; case 'g': if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { return { stopPropagation: false, preventDefault: false }; } const groupings = SelectionManager.Views().slice(); const randomGroup = random(0, 1000); UndoManager.RunInBatch(() => groupings.map(dv => (dv.layoutDoc.group = randomGroup)), 'group'); SelectionManager.DeselectAll(); break; } return { stopPropagation: stopPropagation, preventDefault: preventDefault, }; }); private alt = action((keyname: string) => { const stopPropagation = true; const preventDefault = true; switch (keyname) { case 'ƒ': case 'f': const dv = SelectionManager.Views()?.[0]; UndoManager.RunInBatch(() => dv.props.CollectionFreeFormDocumentView?.().float(), 'float'); } return { stopPropagation: stopPropagation, preventDefault: preventDefault, }; }); private ctrl = action((keyname: string, e: KeyboardEvent) => { let stopPropagation = true; let preventDefault = true; switch (keyname) { case 'arrowright': if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { return { stopPropagation: false, preventDefault: false }; } MainView.Instance.mainFreeform && CollectionDockingView.AddSplit(MainView.Instance.mainFreeform, OpenWhereMod.right); break; case 'arrowleft': if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { return { stopPropagation: false, preventDefault: false }; } MainView.Instance.mainFreeform && CollectionDockingView.CloseSplit(MainView.Instance.mainFreeform); break; case 'backspace': if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') { return { stopPropagation: false, preventDefault: false }; } break; case 't': PromiseValue(Cast(Doc.UserDoc()['tabs-button-tools'], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv })); break; case 'i': const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports); if (importBtn) { MainView.Instance.selectMenu(importBtn); } break; case 's': const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails); if (trailsBtn) { MainView.Instance.selectMenu(trailsBtn); } break; case 'f': if (SelectionManager.Views().length === 1 && SelectionManager.Views()[0].ComponentView?.search) { SelectionManager.Views()[0].ComponentView?.search?.('', false, false); } else { const searchBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MySearcher); if (searchBtn) { MainView.Instance.selectMenu(searchBtn); } } break; case 'e': Doc.ActiveTool = (Doc.ActiveTool === InkTool.Eraser ? InkTool.None : InkTool.Eraser); break; case 'p': Doc.ActiveTool = (Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen); break; case 'r': preventDefault = false; break; case 'y': if (Doc.ActivePage !== 'home') { SelectionManager.DeselectAll(); UndoManager.Redo(); } stopPropagation = false; break; case 'z': if (Doc.ActivePage !== 'home') { SelectionManager.DeselectAll(); UndoManager.Undo(); } stopPropagation = false; break; case 'a': if (e.target !== document.body) { stopPropagation = false; preventDefault = false; } break; case 'v': stopPropagation = false; preventDefault = false; break; case 'x': if (SelectionManager.Views().length) { const bds = DocumentDecorations.Instance.Bounds; const pt = SelectionManager.Views()[0] .props.ScreenToLocalTransform() .transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2); const text = `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.Views() .map(dv => dv.Document[Id]) .join(':'); SelectionManager.Views().length && navigator.clipboard.writeText(text); DocumentDecorations.Instance.onCloseClick(true); stopPropagation = false; preventDefault = false; } break; case 'c': if ((document.activeElement as any)?.type !== 'text' && !AnchorMenu.Instance.Active && DocumentDecorations.Instance.Bounds.r - DocumentDecorations.Instance.Bounds.x > 2) { const bds = DocumentDecorations.Instance.Bounds; const pt = SelectionManager.Views()[0] .props.ScreenToLocalTransform() .transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2); const text = `__DashCloneId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.Views() .map(dv => dv.Document[Id]) .join(':'); SelectionManager.Views().length && navigator.clipboard.writeText(text); stopPropagation = false; } preventDefault = false; break; } return { stopPropagation: stopPropagation, preventDefault: preventDefault, }; }); public paste(e: ClipboardEvent) { const plain = e.clipboardData?.getData('text/plain'); // list of document ids, separated by ':'s if (!plain) return; const clone = plain.startsWith('__DashCloneId('); const docids = plain.split(':'); // hack! docids[0] is the top left of the selection rectangle const addDocument = SelectionManager.Views().lastElement()?.ComponentView?.addDocument; if (addDocument && (plain.startsWith('__DashDocId(') || clone)) { Doc.Paste(docids.slice(1), clone, addDocument); } } getClipboard() { return navigator.clipboard.readText(); } private ctrl_shift = action((keyname: string) => { const stopPropagation = true; const preventDefault = true; switch (keyname) { case 'z': UndoManager.Redo(); break; case 'p': Doc.ActiveTool = InkTool.Write; break; } return { stopPropagation: stopPropagation, preventDefault: preventDefault, }; }); }