import GroupFilter from "./GroupFilter" ; import { Keys } from "./Keys" ; import SETTINGS from "../../common/Settings" ; /** Simple KeyMap for a Hotkey */ export interface KeyMap { /** The key code to be listned for */ key : string | String ; /** Does the Alt key need to be pressed at the same time? */ alt : boolean ; /** Does the Ctrl key need to be pressed at the same time? */ ctrl : boolean ; /** Does the Shift key need to be pressed at the same time? */ shift : boolean ; } /** Hotkey Configuration Registration */ export interface HotkeySetting { /** optional: Group to be included in with their own header. Default: General Group */ group ? : string | String ; /** Unique variable name to be used in the layout. Recommend: 'module-name.myHotkey' */ name : string | String ; /** Label to be displayed in the layout. This will be localized when injected into the HTML */ label : string | String ; /**
     * Accept repeated KeyDown events, this occurs if the user is holding the key down, it will
     * send additional events that are spaced out according to the user's key press repeat settings.
     */ repeat ? : boolean ; /** The default setting for this hotkey, can be a static KeyMap, or a function that returns the default. */ default : KeyMap | ( ( ) => KeyMap ) ; /** Function for retrieving the current hotkey setting. If defined, you must also provide a `set` function. */ get ? ( ) : KeyMap ; /** Function for saving the new hotkey setting. If defined, you must also provide a `get` function */ set ? ( value ) : Promise < KeyMap > ; /** Function to handle the execution of the hotkey */ deprecated handle ? ( self ) : void ; param self param event param repeated onKeyDown ? ( self , event , repeated ? ) : void ; param self param event onKeyUp ? ( self , event ) : void ; } /** Hotkey Group Configuration */ export interface HotkeyGroup { /** Unique name of the group. */ name : string | String ; /** Displayed in the HTML header for the group. */ label : string | String ; /** Optional description of the group */ description ? : string | String ; } interface SettingGroup { name : string | String ; label : string | String ; description : string | String ; items : HotkeySetting [ ] ; } /** This is just a helper for printing the list of errors with a call stack */ function printErrors ( errors ) { console . error ( errors . join ( ',\n' ) + '\n' + ( new Error ( ) . stack ) ) ; } export class _Hotkeys { private static readonly GENERAL = 'general' ; private static _handlers = new Map < number , Map < string , HotkeySetting [ ] > > ( ) ; private static _handled = new Set < string > ( ) ; private static _settings = new Map < string , SettingGroup > ( ) ; private static _settingsNames = new Set < string > ( ) ; static readonly keys = new Keys ( ) ; static get isShim ( ) : boolean { return false ; } private static _metaKey ( event ) { return ( event . altKey ? 0x1 : 0 ) | ( event . ctrlKey ? 0x2 : 0 ) | ( event . shiftKey ? 0x4 : 0 ) ; } private static _isMeta ( event ) { return event . key === 'Shift' || event . key === 'Ctrl' || event . key === 'Alt' ; } private static _handleKeyDown ( event ) { if ( this . _isMeta ( event ) ) return ; if ( document . activeElement instanceof HTMLInputElement ) return ; if ( document . activeElement instanceof HTMLTextAreaElement ) return ; if ( document . activeElement . getAttribute ( 'contenteditable' ) === 'true' ) return ; const metaKey = this . _metaKey ( event ) ; const metaHandlers = this . _handlers . get ( metaKey ) ; if ( ! metaHandlers || metaHandlers . size == 0 ) return ; const eventHandlers = metaHandlers . get ( event . code ) ; if ( ! eventHandlers || eventHandlers . length == 0 ) return ; event . preventDefault ( ) ; this . _handled . add ( event . code ) ; for ( const handler of eventHandlers ) { if ( ! handler . onKeyDown ) continue ; if ( event . repeat && ! handler . repeat ) continue ; handler . onKeyDown ( handler , event , event . repeat ) ; } } private static _handleKeyUp ( event ) { if ( ! this . _handled . has ( event . code ) ) return ; this . _handled . delete ( event . code ) ; const metaKey = this . _metaKey ( event ) ; const metaHandlers = this . _handlers . get ( metaKey ) ; if ( ! metaHandlers || metaHandlers . size == 0 ) return ; const eventHandlers = metaHandlers . get ( event . code ) ; if ( ! eventHandlers || eventHandlers . length == 0 ) return ; event . preventDefault ( ) ; this . _handled . add ( event . code ) ; for ( const handler of eventHandlers ) { if ( ! handler . onKeyUp ) continue ; handler . onKeyUp ( handler , event ) ; } } private static _init ( ) { window . addEventListener ( "keydown" , this . _handleKeyDown . bind ( this ) ) ; window . addEventListener ( "keyup" , this . _handleKeyUp . bind ( this ) ) ; this . _settings . set ( _Hotkeys . GENERAL , { name : _Hotkeys . GENERAL , label : 'DF_HOTKEYS.GeneralGroup_Label' , description : '' , items : [ ] } ) ; } private static _getOrDefault < K , V > ( map , key , defValue ) { if ( map . has ( key ) ) return map . get ( key ) ; map . set ( key , defValue ( ) ) ; return map . get ( key ) ; } param title param filters static async showConfig ( title , filters ) { throw new Error ( "The Shim does not contain the HotkeysConfig application. Please only use this function when DF Hotkeys is activated." ) ; } param title param filters static createConfig ( title , filters ) { throw new Error ( "The Shim does not contain the HotkeysConfig application. Please only use this function when DF Hotkeys is activated." ) ; } param config param throwOnFail throws returns static registerShortcut ( config , throwOnFail = true ) { const errors = [ ] ; if ( typeof ( config . name ) !== 'string' && ! ( config . name instanceof String ) ) errors . push ( 'Hotkeys.registerShortcut(): config.name must be a string!' ) ; if ( config . name . includes ( '::' ) ) errors . push ( 'Hotkeys.registerShortcut(): config.name cannot contain "::"' ) ; if ( typeof ( config . label ) !== 'string' && ! ( config . label instanceof String ) ) errors . push ( 'Hotkeys.registerShortcut(): config.label must be a string!' ) ; if ( config . group !== undefined && config . group !== null && typeof ( config . group ) !== 'string' && ! ( config . group instanceof String ) ) errors . push ( 'Hotkeys.registerShortcut(): config.group must be null, undefined, or a string!' ) ; if ( ! ! config . get && ! ( config . get instanceof Function ) ) errors . push ( 'Hotkeys.registerShortcut(): config.get must be a Function!' ) ; if ( ! ! config . set && ! ( config . set instanceof Function ) ) errors . push ( 'Hotkeys.registerShortcut(): config.set must be a Function!' ) ; if ( ! config . set !== ! config . get ) errors . push ( 'Hotkeys.registerShortcut(): If either `get` or `set` is defined, both must be defined!' ) ; if ( ! ( config . default instanceof Function ) && ( config . default . key === undefined || config . default . alt === undefined || config . default . ctrl === undefined || config . default . shift === undefined ) ) errors . push ( 'Hotkeys.registerShortcut(): config.default must be either a Function or a KeyMap!' ) ; if ( ! ! config . handle && ! ( config . handle instanceof Function ) ) errors . push ( 'Hotkeys.registerShortcut(): DEPRECATED! config.handle must be a Function!' ) ; if ( ! ! config . onKeyDown && ! ( config . onKeyDown instanceof Function ) ) errors . push ( 'Hotkeys.registerShortcut(): config.onKeyDown must be a Function!' ) ; if ( ! ! config . onKeyUp && ! ( config . onKeyUp instanceof Function ) ) errors . push ( 'Hotkeys.registerShortcut(): config.onKeyUp must be a Function!' ) ; if ( this . _settingsNames . has ( < string > config . name ) ) errors . push ( `Hotkeys.registerShortcut(): '${ config . name }' hotkey has already been registered!` ) ; if ( errors . length > 0 ) { if ( throwOnFail ) throw Error ( errors . join ( ',\n' ) ) ; printErrors ( errors ) ; return false ; } if ( config . handle ) { console . warn ( `Hotkeys: The configuration "${ config . name }" is using the deprecated 'handle()' function. Please use 'onKeyDown' and/or 'onKeyUp' instead.\nThis function will still work for now, but will be removed in a later update.` ) ; } if ( ! config . get ) { SETTINGS . register < KeyMap > ( 'KEYMAP.' + config . name , { scope : 'world' , config : false , type : SETTINGS . typeOf ( ) , default : config . default instanceof Function ? config . default ( ) : config . default } ) ; config . get = ( ) => SETTINGS . get ( 'KEYMAP.' + config . name ) ; config . set = value => SETTINGS . set ( 'KEYMAP.' + config . name , value ) ; } this . _settingsNames . add ( < string > config . name ) ; if ( ! config . group ) config . group = _Hotkeys . GENERAL ; else if ( ! this . _settings . has ( < string > config . group ) ) { if ( throwOnFail ) throw Error ( `Hotkeys.registerShortcut(): '${ config . group }' group does not exist. Please make sure you call Hotkeys.registerGroup() before adding hotkeys for a custom group.` ) ; else return false ; } this . _settings . get ( < string > config . group ) . items . push ( config ) ; const keyMap = config . get ( ) ; const metaKey = ( keyMap . alt ? 0x1 : 0 ) | ( keyMap . ctrl ? 0x2 : 0 ) | ( keyMap . shift ? 0x4 : 0 ) ; const metaHandlers = this . _getOrDefault ( this . _handlers , metaKey , ( ) => new Map ( ) ) ; const eventHandlers = this . _getOrDefault ( metaHandlers , keyMap . key , ( ) => [ ] ) ; eventHandlers . push ( config ) ; return true ; } param id returns static deregisterShortcut ( name ) { let found = false ; for ( const group of this . _settings . values ( ) ) { const idx = group . items . findIndex ( x => x . name === name ) ; if ( idx < 0 ) continue ; group . items . splice ( idx , 1 ) ; found = true ; break ; } if ( ! found ) return false ; for ( const meta of this . _handlers . values ( ) ) { for ( const handlers of meta . values ( ) ) { const idx = handlers . findIndex ( x => x . name === name ) ; if ( idx < 0 ) continue ; handlers . splice ( idx , 1 ) ; } } return true ; } param group param throwOnFail throws returns static registerGroup ( group , throwOnFail = true ) { const errors = [ ] ; if ( typeof ( group . name ) !== 'string' && ! ( group . name instanceof String ) ) errors . push ( 'Hotkeys.registerGroup(): group.name must be a string!' ) ; if ( group . name . includes ( '::' ) ) errors . push ( 'Hotkeys.registerGroup(): group.name cannot contain "::"' ) ; if ( typeof ( group . label ) !== 'string' && ! ( group . label instanceof String ) ) errors . push ( 'Hotkeys.registerGroup(): group.label must be a string!' ) ; if ( group . description !== undefined && group . description !== null && typeof ( group . description ) !== 'string' && ! ( group . description instanceof String ) ) errors . push ( 'Hotkeys.registerGroup(): group.description must be null, undefined, or a string!' ) ; if ( this . _settings . has ( < string > group . name ) ) errors . push ( `Hotkeys.registerGroup(): '${ group . name }' group has already been registered!` ) ; if ( errors . length > 0 ) { if ( throwOnFail ) throw Error ( errors . join ( ',\n' ) ) ; printErrors ( errors ) ; return false ; } this . _settings . set ( < string > group . name , { name : group . name , label : group . label , description : group . description ?? '' , items : [ ] } ) ; return true ; } }