1006 lines
32 KiB
JavaScript
1006 lines
32 KiB
JavaScript
|
|
/**
|
||
|
|
* A class responsible for managing defined game keybinding.
|
||
|
|
* Each keybinding is a string key/value pair belonging to a certain namespace and a certain store scope.
|
||
|
|
*
|
||
|
|
* When Foundry Virtual Tabletop is initialized, a singleton instance of this class is constructed within the global
|
||
|
|
* Game object as as game.keybindings.
|
||
|
|
*
|
||
|
|
* @see {@link Game#keybindings}
|
||
|
|
* @see {@link SettingKeybindingConfig}
|
||
|
|
* @see {@link KeybindingsConfig}
|
||
|
|
*/
|
||
|
|
class ClientKeybindings {
|
||
|
|
constructor() {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Registered Keybinding actions
|
||
|
|
* @type {Map<string, KeybindingActionConfig>}
|
||
|
|
*/
|
||
|
|
this.actions = new Map();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A mapping of a string key to possible Actions that might execute off it
|
||
|
|
* @type {Map<string, KeybindingAction[]>}
|
||
|
|
*/
|
||
|
|
this.activeKeys = new Map();
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A stored cache of Keybind Actions Ids to Bindings
|
||
|
|
* @type {Map<string, KeybindingActionBinding[]>}
|
||
|
|
*/
|
||
|
|
this.bindings = undefined;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A count of how many registered keybindings there are
|
||
|
|
* @type {number}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
this._registered = 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A timestamp which tracks the last time a pan operation was performed
|
||
|
|
* @type {number}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
this._moveTime = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
static MOVEMENT_DIRECTIONS = {
|
||
|
|
UP: "up",
|
||
|
|
LEFT: "left",
|
||
|
|
DOWN: "down",
|
||
|
|
RIGHT: "right"
|
||
|
|
};
|
||
|
|
|
||
|
|
static ZOOM_DIRECTIONS = {
|
||
|
|
IN: "in",
|
||
|
|
OUT: "out"
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* An alias of the movement key set tracked by the keyboard
|
||
|
|
* @returns {Set<string>}>
|
||
|
|
*/
|
||
|
|
get moveKeys() {
|
||
|
|
return game.keyboard.moveKeys;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initializes the keybinding values for all registered actions
|
||
|
|
*/
|
||
|
|
initialize() {
|
||
|
|
|
||
|
|
// Create the bindings mapping for all actions which have been registered
|
||
|
|
this.bindings = new Map(Object.entries(game.settings.get("core", "keybindings")));
|
||
|
|
for ( let k of Array.from(this.bindings.keys()) ) {
|
||
|
|
if ( !this.actions.has(k) ) this.bindings.delete(k);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Register bindings for all actions
|
||
|
|
for ( let [action, config] of this.actions) {
|
||
|
|
let bindings = config.uneditable;
|
||
|
|
bindings = config.uneditable.concat(this.bindings.get(action) ?? config.editable);
|
||
|
|
this.bindings.set(action, bindings);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create a mapping of keys which trigger actions
|
||
|
|
this.activeKeys = new Map();
|
||
|
|
for ( let [key, action] of this.actions ) {
|
||
|
|
let bindings = this.bindings.get(key);
|
||
|
|
for ( let binding of bindings ) {
|
||
|
|
if ( !binding ) continue;
|
||
|
|
if ( !this.activeKeys.has(binding.key) ) this.activeKeys.set(binding.key, []);
|
||
|
|
let actions = this.activeKeys.get(binding.key);
|
||
|
|
actions.push({
|
||
|
|
action: key,
|
||
|
|
key: binding.key,
|
||
|
|
name: action.name,
|
||
|
|
requiredModifiers: binding.modifiers,
|
||
|
|
optionalModifiers: action.reservedModifiers,
|
||
|
|
onDown: action.onDown,
|
||
|
|
onUp: action.onUp,
|
||
|
|
precedence: action.precedence,
|
||
|
|
order: action.order,
|
||
|
|
repeat: action.repeat,
|
||
|
|
restricted: action.restricted
|
||
|
|
});
|
||
|
|
this.activeKeys.set(binding.key, actions.sort(this.constructor._compareActions));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register a new keybinding
|
||
|
|
*
|
||
|
|
* @param {string} namespace The namespace the Keybinding Action belongs to
|
||
|
|
* @param {string} action A unique machine-readable id for the Keybinding Action
|
||
|
|
* @param {KeybindingActionConfig} data Configuration for keybinding data
|
||
|
|
*
|
||
|
|
* @example Define a keybinding which shows a notification
|
||
|
|
* ```js
|
||
|
|
* game.keybindings.register("myModule", "showNotification", {
|
||
|
|
* name: "My Settings Keybinding",
|
||
|
|
* hint: "A description of what will occur when the Keybinding is executed.",
|
||
|
|
* uneditable: [
|
||
|
|
* {
|
||
|
|
* key: "Digit1",
|
||
|
|
* modifiers: ["Control"]
|
||
|
|
* }
|
||
|
|
* ],
|
||
|
|
* editable: [
|
||
|
|
* {
|
||
|
|
* key: "F1"
|
||
|
|
* }
|
||
|
|
* ],
|
||
|
|
* onDown: () => { ui.notifications.info("Pressed!") },
|
||
|
|
* onUp: () => {},
|
||
|
|
* restricted: true, // Restrict this Keybinding to gamemaster only?
|
||
|
|
* reservedModifiers: ["Alt"], // On ALT, the notification is permanent instead of temporary
|
||
|
|
* precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL
|
||
|
|
* });
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
register(namespace, action, data) {
|
||
|
|
if ( this.bindings ) throw new Error("You cannot register a Keybinding after the init hook");
|
||
|
|
if ( !namespace || !action ) throw new Error("You must specify both the namespace and action portion of the Keybinding action");
|
||
|
|
action = `${namespace}.${action}`;
|
||
|
|
data.namespace = namespace;
|
||
|
|
data.precedence = data.precedence ?? CONST.KEYBINDING_PRECEDENCE.NORMAL;
|
||
|
|
data.order = this._registered++;
|
||
|
|
data.uneditable = this.constructor._validateBindings(data.uneditable ?? []);
|
||
|
|
data.editable = this.constructor._validateBindings(data.editable ?? []);
|
||
|
|
data.repeat = data.repeat ?? false;
|
||
|
|
data.reservedModifiers = this.constructor._validateModifiers(data.reservedModifiers ?? []);
|
||
|
|
this.actions.set(action, data);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the current Bindings of a given namespace's Keybinding Action
|
||
|
|
*
|
||
|
|
* @param {string} namespace The namespace under which the setting is registered
|
||
|
|
* @param {string} action The keybind action to retrieve
|
||
|
|
* @returns {KeybindingActionBinding[]}
|
||
|
|
*
|
||
|
|
* @example Retrieve the current Keybinding Action Bindings
|
||
|
|
* ```js
|
||
|
|
* game.keybindings.get("myModule", "showNotification");
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
get(namespace, action) {
|
||
|
|
if ( !namespace || !action ) throw new Error("You must specify both namespace and key portions of the keybind");
|
||
|
|
action = `${namespace}.${action}`;
|
||
|
|
const keybind = this.actions.get(action);
|
||
|
|
if ( !keybind ) throw new Error("This is not a registered keybind action");
|
||
|
|
return this.bindings.get(action) || [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set the editable Bindings of a Keybinding Action for a certain namespace and Action
|
||
|
|
*
|
||
|
|
* @param {string} namespace The namespace under which the Keybinding is registered
|
||
|
|
* @param {string} action The Keybinding action to set
|
||
|
|
* @param {KeybindingActionBinding[]} bindings The Bindings to assign to the Keybinding
|
||
|
|
*
|
||
|
|
* @example Update the current value of a keybinding
|
||
|
|
* ```js
|
||
|
|
* game.keybindings.set("myModule", "showNotification", [
|
||
|
|
* {
|
||
|
|
* key: "F2",
|
||
|
|
* modifiers: [ "CONTROL" ]
|
||
|
|
* }
|
||
|
|
* ]);
|
||
|
|
* ```
|
||
|
|
*/
|
||
|
|
async set(namespace, action, bindings) {
|
||
|
|
if ( !namespace || !action ) throw new Error("You must specify both namespace and action portions of the Keybind");
|
||
|
|
action = `${namespace}.${action}`;
|
||
|
|
const keybind = this.actions.get(action);
|
||
|
|
if ( !keybind ) throw new Error("This is not a registered keybind");
|
||
|
|
if ( keybind.restricted && !game.user.isGM ) throw new Error("Only a GM can edit this keybind");
|
||
|
|
const mapping = game.settings.get("core", "keybindings");
|
||
|
|
|
||
|
|
// Set to default if value is undefined and return
|
||
|
|
if ( bindings === undefined ) {
|
||
|
|
delete mapping[action];
|
||
|
|
return game.settings.set("core", "keybindings", mapping);
|
||
|
|
}
|
||
|
|
bindings = this.constructor._validateBindings(bindings);
|
||
|
|
|
||
|
|
// Verify no reserved Modifiers were set as Keys
|
||
|
|
for ( let binding of bindings ) {
|
||
|
|
if ( keybind.reservedModifiers.includes(binding.key) ) {
|
||
|
|
throw new Error(game.i18n.format("KEYBINDINGS.ErrorReservedModifier", {key: binding.key}));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save editable bindings to setting
|
||
|
|
mapping[action] = bindings;
|
||
|
|
await game.settings.set("core", "keybindings", mapping);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reset all client keybindings back to their default configuration.
|
||
|
|
*/
|
||
|
|
async resetDefaults() {
|
||
|
|
const setting = game.settings.settings.get("core.keybindings");
|
||
|
|
return game.settings.set("core", "keybindings", setting.default);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* A helper method that, when given a value, ensures that the returned value is a standardized Binding array
|
||
|
|
* @param {KeybindingActionBinding[]} values An array of keybinding assignments to be validated
|
||
|
|
* @returns {KeybindingActionBinding[]} An array of keybinding assignments confirmed as valid
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _validateBindings(values) {
|
||
|
|
if ( !(values instanceof Array) ) throw new Error(game.i18n.localize("KEYBINDINGS.MustBeArray"));
|
||
|
|
for ( let binding of values ) {
|
||
|
|
if ( !binding.key ) throw new Error("Each KeybindingActionBinding must contain a valid key designation");
|
||
|
|
if ( KeyboardManager.PROTECTED_KEYS.includes(binding.key) ) {
|
||
|
|
throw new Error(game.i18n.format("KEYBINDINGS.ErrorProtectedKey", { key: binding.key }));
|
||
|
|
}
|
||
|
|
binding.modifiers = this._validateModifiers(binding.modifiers ?? []);
|
||
|
|
}
|
||
|
|
return values;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate that assigned modifiers are allowed
|
||
|
|
* @param {string[]} keys An array of modifiers which may be valid
|
||
|
|
* @returns {string[]} An array of modifiers which are confirmed as valid
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _validateModifiers(keys) {
|
||
|
|
const modifiers = [];
|
||
|
|
for ( let key of keys ) {
|
||
|
|
if ( key in KeyboardManager.MODIFIER_KEYS ) key = KeyboardManager.MODIFIER_KEYS[key]; // backwards-compat
|
||
|
|
if ( !Object.values(KeyboardManager.MODIFIER_KEYS).includes(key) ) {
|
||
|
|
throw new Error(game.i18n.format("KEYBINDINGS.ErrorIllegalModifier", { key, allowed: modifiers.join(",") }));
|
||
|
|
}
|
||
|
|
modifiers.push(key);
|
||
|
|
}
|
||
|
|
return modifiers;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Compares two Keybinding Actions based on their Order
|
||
|
|
* @param {KeybindingAction} a The first Keybinding Action
|
||
|
|
* @param {KeybindingAction} b the second Keybinding Action
|
||
|
|
* @returns {number}
|
||
|
|
* @internal
|
||
|
|
*/
|
||
|
|
static _compareActions(a, b) {
|
||
|
|
if (a.precedence === b.precedence) return a.order - b.order;
|
||
|
|
return a.precedence - b.precedence;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ---------------------------------------- */
|
||
|
|
/* Core Keybinding Actions */
|
||
|
|
/* ---------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Register core keybindings.
|
||
|
|
* @param {string} view The active game view
|
||
|
|
* @internal
|
||
|
|
*/
|
||
|
|
_registerCoreKeybindings(view) {
|
||
|
|
const {SHIFT, CONTROL, ALT} = KeyboardManager.MODIFIER_KEYS;
|
||
|
|
|
||
|
|
// General Purpose - All Views
|
||
|
|
game.keybindings.register("core", "dismiss", {
|
||
|
|
name: "KEYBINDINGS.Dismiss",
|
||
|
|
uneditable: [
|
||
|
|
{key: "Escape"}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onDismiss,
|
||
|
|
precedence: CONST.KEYBINDING_PRECEDENCE.DEFERRED
|
||
|
|
});
|
||
|
|
|
||
|
|
// Game View Only
|
||
|
|
if ( view !== "game" ) return;
|
||
|
|
game.keybindings.register("core", "cycleView", {
|
||
|
|
name: "KEYBINDINGS.CycleView",
|
||
|
|
editable: [
|
||
|
|
{key: "Tab"}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onCycleView,
|
||
|
|
reservedModifiers: [SHIFT],
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
|
||
|
|
game.keybindings.register("core", "measuredRulerMovement", {
|
||
|
|
name: "KEYBINDINGS.MoveAlongMeasuredRuler",
|
||
|
|
editable: [
|
||
|
|
{key: "Space"}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onMeasuredRulerMovement,
|
||
|
|
precedence: CONST.KEYBINDING_PRECEDENCE.PRIORITY,
|
||
|
|
reservedModifiers: [SHIFT, CONTROL]
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "pause", {
|
||
|
|
name: "KEYBINDINGS.Pause",
|
||
|
|
restricted: true,
|
||
|
|
editable: [
|
||
|
|
{key: "Space"}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onPause,
|
||
|
|
precedence: CONST.KEYBINDING_PRECEDENCE.DEFERRED
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "delete", {
|
||
|
|
name: "KEYBINDINGS.Delete",
|
||
|
|
uneditable: [
|
||
|
|
{key: "Delete"}
|
||
|
|
],
|
||
|
|
editable: [
|
||
|
|
{key: "Backspace"}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onDelete
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "highlight", {
|
||
|
|
name: "KEYBINDINGS.Highlight",
|
||
|
|
editable: [
|
||
|
|
{key: "AltLeft"},
|
||
|
|
{key: "AltRight"}
|
||
|
|
],
|
||
|
|
onUp: ClientKeybindings._onHighlight,
|
||
|
|
onDown: ClientKeybindings._onHighlight
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "selectAll", {
|
||
|
|
name: "KEYBINDINGS.SelectAll",
|
||
|
|
uneditable: [
|
||
|
|
{key: "KeyA", modifiers: [CONTROL]}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onSelectAllObjects
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "undo", {
|
||
|
|
name: "KEYBINDINGS.Undo",
|
||
|
|
uneditable: [
|
||
|
|
{key: "KeyZ", modifiers: [CONTROL]}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onUndo
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "copy", {
|
||
|
|
name: "KEYBINDINGS.Copy",
|
||
|
|
uneditable: [
|
||
|
|
{key: "KeyC", modifiers: [CONTROL]}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onCopy
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "paste", {
|
||
|
|
name: "KEYBINDINGS.Paste",
|
||
|
|
uneditable: [
|
||
|
|
{key: "KeyV", modifiers: [CONTROL]}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onPaste,
|
||
|
|
reservedModifiers: [ALT, SHIFT]
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "sendToBack", {
|
||
|
|
name: "KEYBINDINGS.SendToBack",
|
||
|
|
editable: [
|
||
|
|
{key: "BracketLeft"}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings.#onSendToBack
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "bringToFront", {
|
||
|
|
name: "KEYBINDINGS.BringToFront",
|
||
|
|
editable: [
|
||
|
|
{key: "BracketRight"}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings.#onBringToFront
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "target", {
|
||
|
|
name: "KEYBINDINGS.Target",
|
||
|
|
editable: [
|
||
|
|
{key: "KeyT"}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onTarget,
|
||
|
|
reservedModifiers: [SHIFT]
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "characterSheet", {
|
||
|
|
name: "KEYBINDINGS.ToggleCharacterSheet",
|
||
|
|
editable: [
|
||
|
|
{key: "KeyC"}
|
||
|
|
],
|
||
|
|
onDown: ClientKeybindings._onToggleCharacterSheet,
|
||
|
|
precedence: CONST.KEYBINDING_PRECEDENCE.PRIORITY
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "panUp", {
|
||
|
|
name: "KEYBINDINGS.PanUp",
|
||
|
|
uneditable: [
|
||
|
|
{key: "ArrowUp"},
|
||
|
|
{key: "Numpad8"}
|
||
|
|
],
|
||
|
|
editable: [
|
||
|
|
{key: "KeyW"}
|
||
|
|
],
|
||
|
|
onUp: context => this._onPan(context, [ClientKeybindings.MOVEMENT_DIRECTIONS.UP]),
|
||
|
|
onDown: context => this._onPan(context, [ClientKeybindings.MOVEMENT_DIRECTIONS.UP]),
|
||
|
|
reservedModifiers: [CONTROL, SHIFT],
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "panLeft", {
|
||
|
|
name: "KEYBINDINGS.PanLeft",
|
||
|
|
uneditable: [
|
||
|
|
{key: "ArrowLeft"},
|
||
|
|
{key: "Numpad4"}
|
||
|
|
],
|
||
|
|
editable: [
|
||
|
|
{key: "KeyA"}
|
||
|
|
],
|
||
|
|
onUp: context => this._onPan(context, [ClientKeybindings.MOVEMENT_DIRECTIONS.LEFT]),
|
||
|
|
onDown: context => this._onPan(context, [ClientKeybindings.MOVEMENT_DIRECTIONS.LEFT]),
|
||
|
|
reservedModifiers: [CONTROL, SHIFT],
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "panDown", {
|
||
|
|
name: "KEYBINDINGS.PanDown",
|
||
|
|
uneditable: [
|
||
|
|
{key: "ArrowDown"},
|
||
|
|
{key: "Numpad2"}
|
||
|
|
],
|
||
|
|
editable: [
|
||
|
|
{key: "KeyS"}
|
||
|
|
],
|
||
|
|
onUp: context => this._onPan(context, [ClientKeybindings.MOVEMENT_DIRECTIONS.DOWN]),
|
||
|
|
onDown: context => this._onPan(context, [ClientKeybindings.MOVEMENT_DIRECTIONS.DOWN]),
|
||
|
|
reservedModifiers: [CONTROL, SHIFT],
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "panRight", {
|
||
|
|
name: "KEYBINDINGS.PanRight",
|
||
|
|
uneditable: [
|
||
|
|
{key: "ArrowRight"},
|
||
|
|
{key: "Numpad6"}
|
||
|
|
],
|
||
|
|
editable: [
|
||
|
|
{key: "KeyD"}
|
||
|
|
],
|
||
|
|
onUp: context => this._onPan(context, [ClientKeybindings.MOVEMENT_DIRECTIONS.RIGHT]),
|
||
|
|
onDown: context => this._onPan(context, [ClientKeybindings.MOVEMENT_DIRECTIONS.RIGHT]),
|
||
|
|
reservedModifiers: [CONTROL, SHIFT],
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "panUpLeft", {
|
||
|
|
name: "KEYBINDINGS.PanUpLeft",
|
||
|
|
uneditable: [
|
||
|
|
{key: "Numpad7"}
|
||
|
|
],
|
||
|
|
onUp: context => this._onPan(context,
|
||
|
|
[ClientKeybindings.MOVEMENT_DIRECTIONS.UP, ClientKeybindings.MOVEMENT_DIRECTIONS.LEFT]),
|
||
|
|
onDown: context => this._onPan(context,
|
||
|
|
[ClientKeybindings.MOVEMENT_DIRECTIONS.UP, ClientKeybindings.MOVEMENT_DIRECTIONS.LEFT]),
|
||
|
|
reservedModifiers: [CONTROL, SHIFT],
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "panUpRight", {
|
||
|
|
name: "KEYBINDINGS.PanUpRight",
|
||
|
|
uneditable: [
|
||
|
|
{key: "Numpad9"}
|
||
|
|
],
|
||
|
|
onUp: context => this._onPan(context,
|
||
|
|
[ClientKeybindings.MOVEMENT_DIRECTIONS.UP, ClientKeybindings.MOVEMENT_DIRECTIONS.RIGHT]),
|
||
|
|
onDown: context => this._onPan(context,
|
||
|
|
[ClientKeybindings.MOVEMENT_DIRECTIONS.UP, ClientKeybindings.MOVEMENT_DIRECTIONS.RIGHT]),
|
||
|
|
reservedModifiers: [CONTROL, SHIFT],
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "panDownLeft", {
|
||
|
|
name: "KEYBINDINGS.PanDownLeft",
|
||
|
|
uneditable: [
|
||
|
|
{key: "Numpad1"}
|
||
|
|
],
|
||
|
|
onUp: context => this._onPan(context,
|
||
|
|
[ClientKeybindings.MOVEMENT_DIRECTIONS.DOWN, ClientKeybindings.MOVEMENT_DIRECTIONS.LEFT]),
|
||
|
|
onDown: context => this._onPan(context,
|
||
|
|
[ClientKeybindings.MOVEMENT_DIRECTIONS.DOWN, ClientKeybindings.MOVEMENT_DIRECTIONS.LEFT]),
|
||
|
|
reservedModifiers: [CONTROL, SHIFT],
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "panDownRight", {
|
||
|
|
name: "KEYBINDINGS.PanDownRight",
|
||
|
|
uneditable: [
|
||
|
|
{key: "Numpad3"}
|
||
|
|
],
|
||
|
|
onUp: context => this._onPan(context,
|
||
|
|
[ClientKeybindings.MOVEMENT_DIRECTIONS.DOWN, ClientKeybindings.MOVEMENT_DIRECTIONS.RIGHT]),
|
||
|
|
onDown: context => this._onPan(context,
|
||
|
|
[ClientKeybindings.MOVEMENT_DIRECTIONS.DOWN, ClientKeybindings.MOVEMENT_DIRECTIONS.RIGHT]),
|
||
|
|
reservedModifiers: [CONTROL, SHIFT],
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "zoomIn", {
|
||
|
|
name: "KEYBINDINGS.ZoomIn",
|
||
|
|
uneditable: [
|
||
|
|
{key: "NumpadAdd"}
|
||
|
|
],
|
||
|
|
editable: [
|
||
|
|
{key: "PageUp"}
|
||
|
|
],
|
||
|
|
onDown: context => { ClientKeybindings._onZoom(context, ClientKeybindings.ZOOM_DIRECTIONS.IN); },
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "zoomOut", {
|
||
|
|
name: "KEYBINDINGS.ZoomOut",
|
||
|
|
uneditable: [
|
||
|
|
{key: "NumpadSubtract"}
|
||
|
|
],
|
||
|
|
editable: [
|
||
|
|
{key: "PageDown"}
|
||
|
|
],
|
||
|
|
onDown: context => { ClientKeybindings._onZoom(context, ClientKeybindings.ZOOM_DIRECTIONS.OUT); },
|
||
|
|
repeat: true
|
||
|
|
});
|
||
|
|
for ( const number of Array.fromRange(9, 1).concat([0]) ) {
|
||
|
|
game.keybindings.register("core", `executeMacro${number}`, {
|
||
|
|
name: game.i18n.format("KEYBINDINGS.ExecuteMacro", { number }),
|
||
|
|
editable: [{key: `Digit${number}`}],
|
||
|
|
onDown: context => ClientKeybindings._onMacroExecute(context, number),
|
||
|
|
precedence: CONST.KEYBINDING_PRECEDENCE.DEFERRED
|
||
|
|
});
|
||
|
|
}
|
||
|
|
for ( const page of Array.fromRange(5, 1) ) {
|
||
|
|
game.keybindings.register("core", `swapMacroPage${page}`, {
|
||
|
|
name: game.i18n.format("KEYBINDINGS.SwapMacroPage", { page }),
|
||
|
|
editable: [{key: `Digit${page}`, modifiers: [ALT]}],
|
||
|
|
onDown: context => ClientKeybindings._onMacroPageSwap(context, page),
|
||
|
|
precedence: CONST.KEYBINDING_PRECEDENCE.DEFERRED
|
||
|
|
});
|
||
|
|
}
|
||
|
|
game.keybindings.register("core", "pushToTalk", {
|
||
|
|
name: "KEYBINDINGS.PTTKey",
|
||
|
|
editable: [{key: "Backquote"}],
|
||
|
|
onDown: game.webrtc._onPTTStart.bind(game.webrtc),
|
||
|
|
onUp: game.webrtc._onPTTEnd.bind(game.webrtc),
|
||
|
|
precedence: CONST.KEYBINDING_PRECEDENCE.PRIORITY,
|
||
|
|
repeat: false
|
||
|
|
});
|
||
|
|
game.keybindings.register("core", "focusChat", {
|
||
|
|
name: "KEYBINDINGS.FocusChat",
|
||
|
|
editable: [{key: "KeyC", modifiers: [SHIFT]}],
|
||
|
|
onDown: ClientKeybindings._onFocusChat,
|
||
|
|
precedence: CONST.KEYBINDING_PRECEDENCE.PRIORITY,
|
||
|
|
repeat: false
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Select all action
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onSelectAllObjects(context) {
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
canvas.activeLayer.controlAll();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Cycle View actions
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onCycleView(context) {
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
|
||
|
|
// Attempt to cycle tokens, otherwise re-center the canvas
|
||
|
|
if ( canvas.tokens.active ) {
|
||
|
|
let cycled = canvas.tokens.cycleTokens(!context.isShift, false);
|
||
|
|
if ( !cycled ) canvas.recenter();
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Dismiss actions
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static async _onDismiss(context) {
|
||
|
|
|
||
|
|
// Save fog of war if there are pending changes
|
||
|
|
if ( canvas.ready ) canvas.fog.commit();
|
||
|
|
|
||
|
|
// Case 1 - dismiss an open context menu
|
||
|
|
if (ui.context && ui.context.menu.length) {
|
||
|
|
await ui.context.close();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Case 2 - dismiss an open Tour
|
||
|
|
if (Tour.tourInProgress) {
|
||
|
|
Tour.activeTour.exit();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Case 3 - close open UI windows
|
||
|
|
const closingApps = [];
|
||
|
|
for ( const app of Object.values(ui.windows) ) {
|
||
|
|
closingApps.push(app.close({closeKey: true}).then(() => !app.rendered));
|
||
|
|
}
|
||
|
|
for ( const app of foundry.applications.instances.values() ) {
|
||
|
|
if ( app.hasFrame ) closingApps.push(app.close({closeKey: true}).then(() => !app.rendered));
|
||
|
|
}
|
||
|
|
const closedApp = (await Promise.all(closingApps)).some(c => c); // Confirm an application actually closed
|
||
|
|
if ( closedApp ) return true;
|
||
|
|
|
||
|
|
// Case 4 (GM) - release controlled objects (if not in a preview)
|
||
|
|
if ( game.view !== "game" ) return;
|
||
|
|
if (game.user.isGM && (canvas.activeLayer instanceof PlaceablesLayer) && canvas.activeLayer.controlled.length) {
|
||
|
|
if ( !canvas.activeLayer.preview?.children.length ) canvas.activeLayer.releaseAll();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Case 5 - toggle the main menu
|
||
|
|
ui.menu.toggle();
|
||
|
|
// Save the fog immediately rather than waiting for the 3s debounced save as part of commitFog.
|
||
|
|
if ( canvas.ready ) await canvas.fog.save();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Open Character sheet for current token or controlled actor
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onToggleCharacterSheet(context) {
|
||
|
|
return game.toggleCharacterSheet();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle action to target the currently hovered token.
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onTarget(context) {
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
const layer = canvas.activeLayer;
|
||
|
|
if ( !(layer instanceof TokenLayer) ) return false;
|
||
|
|
const hovered = layer.hover;
|
||
|
|
if ( !hovered || hovered.document.isSecret ) return false;
|
||
|
|
hovered.setTarget(!hovered.isTargeted, {releaseOthers: !context.isShift});
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle action to send the currently controlled placeables to the back.
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
*/
|
||
|
|
static #onSendToBack(context) {
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
return canvas.activeLayer?._sendToBackOrBringToFront(false) ?? false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle action to bring the currently controlled placeables to the front.
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
*/
|
||
|
|
static #onBringToFront(context) {
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
return canvas.activeLayer?._sendToBackOrBringToFront(true) ?? false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle DELETE Keypress Events
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onDelete(context) {
|
||
|
|
// Remove hotbar Macro
|
||
|
|
if ( ui.hotbar._hover ) {
|
||
|
|
game.user.assignHotbarMacro(null, ui.hotbar._hover);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delete placeables from Canvas layer
|
||
|
|
else if ( canvas.ready && ( canvas.activeLayer instanceof PlaceablesLayer ) ) {
|
||
|
|
canvas.activeLayer._onDeleteKey(context.event);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle keyboard movement once a small delay has elapsed to allow for multiple simultaneous key-presses.
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @param {InteractionLayer} layer The active InteractionLayer instance
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_handleMovement(context, layer) {
|
||
|
|
if ( !this.moveKeys.size ) return;
|
||
|
|
|
||
|
|
// Get controlled objects
|
||
|
|
let objects = layer.placeables.filter(o => o.controlled);
|
||
|
|
if ( objects.length === 0 ) return;
|
||
|
|
|
||
|
|
// Get the directions of movement
|
||
|
|
let directions = this.moveKeys;
|
||
|
|
const grid = canvas.grid;
|
||
|
|
const diagonals = (grid.type !== CONST.GRID_TYPES.SQUARE) || (grid.diagonals !== CONST.GRID_DIAGONALS.ILLEGAL);
|
||
|
|
if ( !diagonals ) directions = new Set(Array.from(directions).slice(-1));
|
||
|
|
|
||
|
|
// Define movement offsets and get moved directions
|
||
|
|
let dx = 0;
|
||
|
|
let dy = 0;
|
||
|
|
if ( directions.has(ClientKeybindings.MOVEMENT_DIRECTIONS.LEFT) ) dx -= 1;
|
||
|
|
if ( directions.has(ClientKeybindings.MOVEMENT_DIRECTIONS.RIGHT) ) dx += 1;
|
||
|
|
if ( directions.has(ClientKeybindings.MOVEMENT_DIRECTIONS.UP) ) dy -= 1;
|
||
|
|
if ( directions.has(ClientKeybindings.MOVEMENT_DIRECTIONS.DOWN) ) dy += 1;
|
||
|
|
|
||
|
|
// Perform the shift or rotation
|
||
|
|
layer.moveMany({dx, dy, rotate: context.isShift});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle panning the canvas using CTRL + directional keys
|
||
|
|
*/
|
||
|
|
_handleCanvasPan() {
|
||
|
|
|
||
|
|
// Determine movement offsets
|
||
|
|
let dx = 0;
|
||
|
|
let dy = 0;
|
||
|
|
if (this.moveKeys.has(ClientKeybindings.MOVEMENT_DIRECTIONS.LEFT)) dx -= 1;
|
||
|
|
if (this.moveKeys.has(ClientKeybindings.MOVEMENT_DIRECTIONS.UP)) dy -= 1;
|
||
|
|
if (this.moveKeys.has(ClientKeybindings.MOVEMENT_DIRECTIONS.RIGHT)) dx += 1;
|
||
|
|
if (this.moveKeys.has(ClientKeybindings.MOVEMENT_DIRECTIONS.DOWN)) dy += 1;
|
||
|
|
|
||
|
|
// Clear the pending set
|
||
|
|
this.moveKeys.clear();
|
||
|
|
|
||
|
|
// Pan by the grid size
|
||
|
|
const s = canvas.dimensions.size;
|
||
|
|
return canvas.animatePan({
|
||
|
|
x: canvas.stage.pivot.x + (dx * s),
|
||
|
|
y: canvas.stage.pivot.y + (dy * s),
|
||
|
|
duration: 100
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Measured Ruler Movement Action
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onMeasuredRulerMovement(context) {
|
||
|
|
if ( !canvas.ready ) return;
|
||
|
|
const ruler = canvas.controls.ruler;
|
||
|
|
if ( ruler.state !== Ruler.STATES.MEASURING ) return;
|
||
|
|
ruler._onMoveKeyDown(context);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Pause Action
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onPause(context) {
|
||
|
|
game.togglePause(undefined, true);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Highlight action
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onHighlight(context) {
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
canvas.highlightObjects(!context.up);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Pan action
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @param {string[]} movementDirections The Directions being panned in
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
_onPan(context, movementDirections) {
|
||
|
|
|
||
|
|
// Case 1: Check for Tour
|
||
|
|
if ( (Tour.tourInProgress) && (!context.repeat) && (!context.up) ) {
|
||
|
|
Tour.onMovementAction(movementDirections);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Case 2: Check for Canvas
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
|
||
|
|
// Remove Keys on Up
|
||
|
|
if ( context.up ) {
|
||
|
|
for ( let d of movementDirections ) {
|
||
|
|
this.moveKeys.delete(d);
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Keep track of when we last moved
|
||
|
|
const now = Date.now();
|
||
|
|
const delta = now - this._moveTime;
|
||
|
|
|
||
|
|
// Track the movement set
|
||
|
|
for ( let d of movementDirections ) {
|
||
|
|
this.moveKeys.add(d);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle canvas pan using CTRL
|
||
|
|
if ( context.isControl ) {
|
||
|
|
if ( ["KeyW", "KeyA", "KeyS", "KeyD"].includes(context.key) ) return false;
|
||
|
|
this._handleCanvasPan();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Delay 50ms before shifting tokens in order to capture diagonal movements
|
||
|
|
const layer = canvas.activeLayer;
|
||
|
|
if ( (layer === canvas.tokens) || (layer === canvas.tiles) ) {
|
||
|
|
if ( delta < 100 ) return true; // Throttle keyboard movement once per 100ms
|
||
|
|
setTimeout(() => this._handleMovement(context, layer), 50);
|
||
|
|
}
|
||
|
|
this._moveTime = now;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Macro executions
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @param {number} number The numbered macro slot to execute
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onMacroExecute(context, number) {
|
||
|
|
const slot = ui.hotbar.macros.find(m => m.key === number);
|
||
|
|
if ( slot.macro ) {
|
||
|
|
slot.macro.execute();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Macro page swaps
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @param {number} page The numbered macro page to activate
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onMacroPageSwap(context, page) {
|
||
|
|
ui.hotbar.changePage(page);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle action to copy data to clipboard
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onCopy(context) {
|
||
|
|
// Case 1 - attempt a copy operation on the PlaceablesLayer
|
||
|
|
if (window.getSelection().toString() !== "") return false;
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
let layer = canvas.activeLayer;
|
||
|
|
if ( layer instanceof PlaceablesLayer ) layer.copyObjects();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Paste action
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onPaste(context ) {
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
let layer = canvas.activeLayer;
|
||
|
|
if ( (layer instanceof PlaceablesLayer) && layer._copy.length ) {
|
||
|
|
const pos = canvas.mousePosition;
|
||
|
|
layer.pasteObjects(pos, {hidden: context.isAlt, snap: !context.isShift});
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle Undo action
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onUndo(context) {
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
|
||
|
|
// Undo history for a PlaceablesLayer
|
||
|
|
const layer = canvas.activeLayer;
|
||
|
|
if ( !(layer instanceof PlaceablesLayer) ) return false;
|
||
|
|
layer.undoHistory();
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Handle presses to keyboard zoom keys
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @param {ClientKeybindings.ZOOM_DIRECTIONS} zoomDirection The direction to zoom
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onZoom(context, zoomDirection ) {
|
||
|
|
if ( !canvas.ready ) return false;
|
||
|
|
const delta = zoomDirection === ClientKeybindings.ZOOM_DIRECTIONS.IN ? 1.05 : 0.95;
|
||
|
|
canvas.animatePan({scale: delta * canvas.stage.scale.x, duration: 100});
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Bring the chat window into view and focus the input
|
||
|
|
* @param {KeyboardEventContext} context The context data of the event
|
||
|
|
* @returns {boolean}
|
||
|
|
* @private
|
||
|
|
*/
|
||
|
|
static _onFocusChat(context) {
|
||
|
|
const sidebar = ui.sidebar._element[0];
|
||
|
|
ui.sidebar.activateTab(ui.chat.tabName);
|
||
|
|
|
||
|
|
// If the sidebar is collapsed and the chat popover is not visible, open it
|
||
|
|
if ( sidebar.classList.contains("collapsed") && !ui.chat._popout ) {
|
||
|
|
const popout = ui.chat.createPopout();
|
||
|
|
popout._render(true).then(() => {
|
||
|
|
popout.element.find("#chat-message").focus();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
else {
|
||
|
|
ui.chat.element.find("#chat-message").focus();
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|