mirror of
https://github.com/motajs/template.git
synced 2026-10-29 12:20:17 +08:00
refactor: 按键系统
This commit is contained in:
parent
d5b2a820d8
commit
564f53d82a
@ -193,6 +193,9 @@
|
||||
- **D-69(纯内部实现章节,长期规范):** 讲解**纯内部实现**的章节,开头必须写明「此章节讲解一下 XXX 的内部实现」;这类章节应放在文档**靠后**位置,文档前面以「如何使用」为主。适用于后续所有文档写作。
|
||||
- **D-70(代码块完整类上下文,长期规范):** 代码块必须包裹**完整的类上下文**(尤其是类的方法),确保语法高亮正确;不要单独展示脱离类的方法体。适用于后续所有文档写作。
|
||||
- **D-71(高级 I 待补内容):** 高级 I 还有未讲内容,待后续补写:**移动 Mover**、**FaceHandler 系列接口**。当前先完成高级 II,之后再补。
|
||||
- **D-72(注释优先,长期规范):** 当引擎源码的**注释与实现不一致**时,文档**一律以方法注释为准**,并在对话中**向用户报告**该不一致。不得按实现去改写注释的语义。适用于后续所有文档写作。
|
||||
- **D-73(假定编辑器已完成,长期规范):** 文档**假定编辑器已经完成**。涉及编辑器 / `.jsonc` 内容配置的地方,只写「需要编辑器」的占位标识;**不要讲「编辑器未就位时改文件、就位后用编辑器」**这类过渡做法。所有 `.jsonc` 均由编辑器负责。适用于后续所有文档写作。
|
||||
- **D-74(章节锚点链接,长期规范):** 引用其他文档的**具体章节**时,使用 `./xxx.md#标题` 直接定位到该标题(如 `./static-vs-dynamic-tiles.md#双向转换`)。适用于后续所有文档写作。**待办:** 高级 II 完成后,回头检查高级 I 与中级教程是否存在同类未定位的链接。
|
||||
|
||||
</decisions>
|
||||
|
||||
|
||||
66
packages-user/client-base/src/hotkey/hotkey.ts
Normal file
66
packages-user/client-base/src/hotkey/hotkey.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import {
|
||||
HotkeySystem,
|
||||
IHotkeyCommand,
|
||||
IHotkeySystem,
|
||||
KeyCode
|
||||
} from '@motajs/system';
|
||||
import { IClientHotkey, IClientHotkeySave } from './types';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
export class ClientHotkey implements IClientHotkey {
|
||||
system: IHotkeySystem = new HotkeySystem();
|
||||
|
||||
/** 按键分组 */
|
||||
private readonly groups: Map<string, IHotkeyCommand[]> = new Map();
|
||||
|
||||
setGroup(group: string, commands: IHotkeyCommand[]): void {
|
||||
if (commands.length > 0) {
|
||||
if (this.groups.has(group)) {
|
||||
logger.warn(208, group);
|
||||
}
|
||||
this.groups.set(group, commands);
|
||||
} else {
|
||||
this.groups.delete(group);
|
||||
}
|
||||
}
|
||||
|
||||
getGroup(group: string): IHotkeyCommand[] {
|
||||
return this.groups.get(group) ?? [];
|
||||
}
|
||||
|
||||
getUngrouped(): Iterable<IHotkeyCommand> {
|
||||
const grouped = new Set([...this.groups.values()].flat());
|
||||
return new Set([...this.system.iterateCommands()]).difference(grouped);
|
||||
}
|
||||
|
||||
saveState(): IClientHotkeySave {
|
||||
const keys: Map<string, (readonly KeyCode[])[]> = new Map();
|
||||
for (const command of this.system.iterateCommands()) {
|
||||
const saveKeys: (readonly KeyCode[])[] = Array.from(
|
||||
{ length: command.length },
|
||||
() => []
|
||||
);
|
||||
for (const [index, key] of command.iterateKeyCombinations()) {
|
||||
saveKeys[index] = key;
|
||||
}
|
||||
keys.set(command.command, saveKeys);
|
||||
}
|
||||
return {
|
||||
keys
|
||||
};
|
||||
}
|
||||
|
||||
loadState(state: IClientHotkeySave): void {
|
||||
for (const [id, keys] of state.keys) {
|
||||
const command = this.system.getCommand(id);
|
||||
if (!command) {
|
||||
logger.warn(209, id);
|
||||
return;
|
||||
}
|
||||
command.setKeys(keys.length);
|
||||
keys.forEach((key, index) => {
|
||||
command.setKeyCombination(index, key);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
2
packages-user/client-base/src/hotkey/index.ts
Normal file
2
packages-user/client-base/src/hotkey/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './hotkey';
|
||||
export * from './types';
|
||||
30
packages-user/client-base/src/hotkey/types.ts
Normal file
30
packages-user/client-base/src/hotkey/types.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { IHotkeyCommand, IHotkeySystem, KeyCode } from '@motajs/system';
|
||||
import { ISaveableContent } from '@user/data-common';
|
||||
|
||||
export interface IClientHotkeySave {
|
||||
/** 按键指令列表 */
|
||||
readonly keys: ReadonlyMap<string, readonly (readonly KeyCode[])[]>;
|
||||
}
|
||||
|
||||
export interface IClientHotkey extends ISaveableContent<IClientHotkeySave> {
|
||||
/** 按键系统对象 */
|
||||
readonly system: IHotkeySystem;
|
||||
|
||||
/**
|
||||
* 设置指定分组的按键指令列表,若不存在分组则创建,若传入空数组则删除
|
||||
* @param group 按键分组名
|
||||
* @param commands 分组中包含的按键指令名
|
||||
*/
|
||||
setGroup(group: string, commands: IHotkeyCommand[]): void;
|
||||
|
||||
/**
|
||||
* 获取指定按键分组中包含的按键,如果不存在则返回空数组
|
||||
* @param group 按键分组名
|
||||
*/
|
||||
getGroup(group: string): IHotkeyCommand[];
|
||||
|
||||
/**
|
||||
* 获取未分组的按键列表
|
||||
*/
|
||||
getUngrouped(): Iterable<IHotkeyCommand>;
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
export * from './components';
|
||||
export * from './elements';
|
||||
export * from './hotkey';
|
||||
export * from './layout';
|
||||
export * from './map';
|
||||
export * from './material';
|
||||
|
||||
@ -11,6 +11,7 @@ import { ICoreState } from '@user/data-state';
|
||||
import { ISaveSystem } from './save';
|
||||
import { ITextureManager } from './material';
|
||||
import { IMapRenderer, IMapExtensionManager } from './map';
|
||||
import { IClientHotkey } from './hotkey';
|
||||
|
||||
export interface IGameUIPropsBase<
|
||||
T extends UIComponent = UIComponent
|
||||
@ -47,6 +48,8 @@ export interface IClientBase extends ICoreState {
|
||||
readonly sceneController: IUIController<IGameUIPropsBase>;
|
||||
/** 主界面的 UI 控制器,管理游戏主界面的 UI 显示 */
|
||||
readonly mainUIController: IUIController<IGameUIPropsBase>;
|
||||
/** 渲染端主按键系统 */
|
||||
readonly hotkey: IClientHotkey;
|
||||
}
|
||||
|
||||
export interface IClientBaseExtended {
|
||||
|
||||
@ -25,7 +25,9 @@ import {
|
||||
TextureManager,
|
||||
ISaveSystem,
|
||||
SaveSystem,
|
||||
IGameUIPropsBase
|
||||
IGameUIPropsBase,
|
||||
ClientHotkey,
|
||||
IClientHotkey
|
||||
} from '@user/client-base';
|
||||
import {
|
||||
IMapRenderer,
|
||||
@ -59,6 +61,7 @@ export class ClientCore extends CoreState implements IClientCore {
|
||||
readonly mainMapExtension: IMapExtensionManager;
|
||||
readonly sceneController: IUIController<IGameUIPropsBase>;
|
||||
readonly mainUIController: IUIController<IGameUIPropsBase>;
|
||||
readonly hotkey: IClientHotkey;
|
||||
|
||||
// Layer 5 渲染顶层 - 目前没东西
|
||||
|
||||
@ -79,6 +82,7 @@ export class ClientCore extends CoreState implements IClientCore {
|
||||
config.tilesetReserve,
|
||||
config.tilesetUnit
|
||||
);
|
||||
this.hotkey = new ClientHotkey();
|
||||
|
||||
this.rafExcitation = new RafExcitation();
|
||||
this.excitationDivider = new ExcitationDivider<number>();
|
||||
|
||||
@ -293,9 +293,13 @@
|
||||
"200": "Cannot buy or sell item since target item '$1' does not exist.",
|
||||
"201": "Cannot set hotkey condition or implement command since no scope is active.",
|
||||
"202": "Cannot implement nonexistent hotkey command '$1'.",
|
||||
"203": "Hotkey command '$1' has an invalid KeyUp combination: at most one non-auxiliary key is allowed.",
|
||||
"203": "Hotkey command '$1' has an invalid KeyUp combination: at most one non-auxiliary key is allowed. This command will be never emitted by invalid command.",
|
||||
"204": "Duplicate hotkey command '$1', the existing command will be used.",
|
||||
"205": "Cannot leave nonexistent hotkey scope.",
|
||||
"206": "Cannot set command key combination since delivered index is invalid: expected positive integer, found $1.",
|
||||
"207": "Key combination count needs to be a positive integer, but got $1.",
|
||||
"208": "Duplicate hotkey command group name: $1, old one will be overwritten.",
|
||||
"209": "Cannot load key combinations for hotkey command $1 since no command named it.",
|
||||
"1001": "Event(setBlock): Unknown tile '$1' when setting block."
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,3 +20,27 @@ export function cumsum(seq: Iterable<number>): number[] {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个数组的所有项,符合的项组成 `match`,不符合的组成 `unmatch`,返回 `[match, unmatch]`
|
||||
* @param array 要判断的数组
|
||||
* @param filter 判断函数
|
||||
* @example matchArray([1, 2, 3, 4], v => v < 2); // [[1], [2, 3, 4]]
|
||||
*/
|
||||
export function matchArray<T>(
|
||||
array: readonly T[],
|
||||
filter: (v: T, i: number, a: readonly T[]) => boolean
|
||||
): [match: T[], unmatch: T[]] {
|
||||
const match: T[] = [];
|
||||
const unmatch: T[] = [];
|
||||
|
||||
array.forEach((v, i, a) => {
|
||||
if (filter(v, i, a)) {
|
||||
match.push(v);
|
||||
} else {
|
||||
unmatch.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
return [match, unmatch];
|
||||
}
|
||||
|
||||
@ -24,20 +24,20 @@ export class HotkeyCommand implements IHotkeyCommand {
|
||||
dt: number;
|
||||
length: number;
|
||||
|
||||
/** 指令标识符,用于告警信息 */
|
||||
private readonly command: string;
|
||||
/** 按键组合列表,长度与 {@link length} 保持一致 */
|
||||
private combinations: KeyCode[][];
|
||||
private keys: KeyCode[][] = [];
|
||||
|
||||
constructor(command: string, init: IHotkeyCommandInit) {
|
||||
constructor(
|
||||
readonly command: string,
|
||||
init: IHotkeyCommandInit
|
||||
) {
|
||||
this.command = command;
|
||||
this.priority = init.priority;
|
||||
this.block = init.block;
|
||||
this.block = init.block ?? false;
|
||||
this.mode = init.mode ?? HotkeyEmitMode.KeyUp;
|
||||
this.timeout = init.timeout ?? 0;
|
||||
this.interval = init.interval ?? 0;
|
||||
this.dt = init.dt ?? 0;
|
||||
this.combinations = [];
|
||||
this.length = 0;
|
||||
this.setKeys(init.defaults.length);
|
||||
init.defaults.forEach((keys, index) =>
|
||||
@ -46,69 +46,78 @@ export class HotkeyCommand implements IHotkeyCommand {
|
||||
}
|
||||
|
||||
setKeys(count: number): void {
|
||||
if (!Number.isInteger(count) || count < 0) return;
|
||||
while (this.combinations.length < count) {
|
||||
this.combinations.push([]);
|
||||
if (!Number.isInteger(count)) {
|
||||
logger.warn(207, 'float');
|
||||
return;
|
||||
}
|
||||
if (this.combinations.length > count) {
|
||||
this.combinations.length = count;
|
||||
if (count <= 0) {
|
||||
logger.warn(207, 'negative integer');
|
||||
return;
|
||||
}
|
||||
this.length = this.combinations.length;
|
||||
if (count > this.keys.length) {
|
||||
while (count > this.keys.length) {
|
||||
this.keys.push([]);
|
||||
}
|
||||
} else if (count < this.keys.length) {
|
||||
while (count < this.keys.length) {
|
||||
this.keys.pop();
|
||||
}
|
||||
}
|
||||
this.length = this.keys.length;
|
||||
}
|
||||
|
||||
setKeyCombination(index: number, keys: KeyCode[]): void {
|
||||
if (
|
||||
!Number.isInteger(index) ||
|
||||
index < 0 ||
|
||||
index >= this.combinations.length
|
||||
) {
|
||||
if (!Number.isInteger(index)) {
|
||||
logger.warn(206, 'float');
|
||||
return;
|
||||
}
|
||||
const copy = keys.slice();
|
||||
this.combinations[index] = copy;
|
||||
if (index < 0) {
|
||||
logger.warn(206, 'negetive integer');
|
||||
return;
|
||||
}
|
||||
if (index >= this.keys.length) {
|
||||
logger.warn(206, 'integer but exceeds combination count');
|
||||
return;
|
||||
}
|
||||
this.keys[index] = keys.slice();
|
||||
if (this.mode === HotkeyEmitMode.KeyUp) {
|
||||
const regular = copy.filter(v => !isAuxiliaryKey(v));
|
||||
const regular = keys.filter(v => !isAuxiliaryKey(v));
|
||||
if (regular.length >= 2) {
|
||||
logger.warn(203, this.command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getKeyCombination(index: number): KeyCode[] {
|
||||
const keys = this.combinations[index];
|
||||
return keys ? keys.slice() : [];
|
||||
getKeyCombination(index: number): readonly KeyCode[] {
|
||||
return this.keys[index]?.slice() ?? [];
|
||||
}
|
||||
|
||||
*iterateKeyCombinations(): Iterable<[index: number, key: KeyCode[]]> {
|
||||
for (let i = 0; i < this.combinations.length; i++) {
|
||||
yield [i, this.combinations[i].slice()];
|
||||
*iterateKeyCombinations(): Iterable<
|
||||
[index: number, key: readonly KeyCode[]]
|
||||
> {
|
||||
for (let i = 0; i < this.keys.length; i++) {
|
||||
yield [i, this.keys[i].slice()];
|
||||
}
|
||||
}
|
||||
|
||||
keyUp(timeout?: number): void {
|
||||
keyUp(timeout: number = 0): void {
|
||||
this.mode = HotkeyEmitMode.KeyUp;
|
||||
if (timeout !== undefined) this.timeout = timeout;
|
||||
const invalid = this.combinations.some(
|
||||
keys => keys.filter(v => !isAuxiliaryKey(v)).length >= 2
|
||||
);
|
||||
if (invalid) logger.warn(203, this.command);
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
once(timeout?: number): void {
|
||||
once(timeout: number = 0): void {
|
||||
this.mode = HotkeyEmitMode.Once;
|
||||
if (timeout !== undefined) this.timeout = timeout;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
continuous(interval?: number, timeout?: number): void {
|
||||
continuous(
|
||||
interval: number = 0,
|
||||
dt: number = 0,
|
||||
timeout: number = 0
|
||||
): void {
|
||||
this.mode = HotkeyEmitMode.Continuous;
|
||||
if (interval !== undefined) this.interval = interval;
|
||||
if (timeout !== undefined) this.timeout = timeout;
|
||||
}
|
||||
|
||||
throttle(dt: number, interval?: number, timeout?: number): void {
|
||||
this.mode = HotkeyEmitMode.Throttle;
|
||||
this.interval = interval;
|
||||
this.dt = dt;
|
||||
if (interval !== undefined) this.interval = interval;
|
||||
if (timeout !== undefined) this.timeout = timeout;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { IExcitable, IExcitableController, IExcitation } from '@motajs/animate';
|
||||
import { logger } from '@motajs/common';
|
||||
import { IExcitable, IExcitation } from '@motajs/animate';
|
||||
import { logger, matchArray } from '@motajs/common';
|
||||
import { HotkeyCommand, isAuxiliaryKey } from './command';
|
||||
import { EVENT_KEY_CODE_MAP, KeyCode } from './keyCodes';
|
||||
import {
|
||||
HotkeyEmitMode,
|
||||
HotkeyEmitType,
|
||||
IHotkeyCommand,
|
||||
IHotkeyCommandInit,
|
||||
IHotkeyEmitContext,
|
||||
@ -12,99 +11,95 @@ import {
|
||||
IHotkeySystem
|
||||
} from './type';
|
||||
|
||||
type HotkeyHandler = (ctx: IHotkeyEmitInfo) => void;
|
||||
|
||||
type HotkeyListener = (event: KeyboardEvent) => void;
|
||||
// TODO: 优先级阻断
|
||||
|
||||
interface IHotkeyScope {
|
||||
/** 作用域唯一标识符 */
|
||||
symbol: symbol;
|
||||
readonly symbol: symbol;
|
||||
/** 作用域内已实现的按键指令 */
|
||||
readonly implements: Map<IHotkeyCommand, (info: IHotkeyEmitInfo) => void>;
|
||||
/** 作用域条件,为 `null` 时视为始终满足 */
|
||||
condition: (() => boolean) | null;
|
||||
/** 作用域内已实现的按键指令 */
|
||||
handlers: Map<string, HotkeyHandler>;
|
||||
}
|
||||
|
||||
interface IHotkeyMatch {
|
||||
/** 指令标识符 */
|
||||
id: string;
|
||||
/** 指令对象 */
|
||||
command: IHotkeyCommand;
|
||||
/** 命中的按键组合索引 */
|
||||
index: number;
|
||||
/** 命中的按键组合 */
|
||||
keys: KeyCode[];
|
||||
/** 指令实现 */
|
||||
handler: HotkeyHandler;
|
||||
/** 关联的待触发项 */
|
||||
entry?: IHotkeyPending;
|
||||
}
|
||||
|
||||
interface IHotkeyPending {
|
||||
/** 指令标识符 */
|
||||
id: string;
|
||||
/** 指令对象 */
|
||||
command: IHotkeyCommand;
|
||||
/** 命中的按键组合索引 */
|
||||
index: number;
|
||||
/** 命中的按键组合 */
|
||||
keys: KeyCode[];
|
||||
/** 指令实现 */
|
||||
handler: HotkeyHandler;
|
||||
/** 触发方式 */
|
||||
type: HotkeyEmitType;
|
||||
/** 触发此指令的按键 */
|
||||
key: KeyCode;
|
||||
/** 入队时的作用域 */
|
||||
scope: symbol;
|
||||
/** 入队时的触发方式快照 */
|
||||
mode: HotkeyEmitMode;
|
||||
/** 入队时的首次触发延迟快照 */
|
||||
timeout: number;
|
||||
/** 入队时的首次与第二次触发最小间隔快照 */
|
||||
interval: number;
|
||||
/** 入队时的节流间隔快照 */
|
||||
dt: number;
|
||||
/** 入队时刻 */
|
||||
time: number;
|
||||
/** 上一次真正触发的时刻 */
|
||||
last: number;
|
||||
/** 已触发的次数 */
|
||||
fired: number;
|
||||
interface IHotkeyCommandPending {
|
||||
/** 按键指令对象 */
|
||||
readonly command: IHotkeyCommand;
|
||||
/** 按键组合被按下的时刻 */
|
||||
start: number;
|
||||
/** 按键组合的索引 */
|
||||
keyIndex: number;
|
||||
/** 触发此指令的按键组合 */
|
||||
keys: readonly KeyCode[];
|
||||
/** 按键触发上下文 */
|
||||
context: IHotkeyEmitContext;
|
||||
/** 当前已经触发了几次 */
|
||||
emitted: number;
|
||||
/** 上一次的触发时刻 */
|
||||
lastEmit: number;
|
||||
}
|
||||
|
||||
export class HotkeySystem implements IHotkeySystem, IExcitable<number> {
|
||||
listening: HTMLElement | null = null;
|
||||
holding: KeyCode[] = [];
|
||||
|
||||
/** 指令注册表 */
|
||||
private commands: Map<string, IHotkeyCommand> = new Map();
|
||||
/** 作用域栈 */
|
||||
private scopes: IHotkeyScope[] = [];
|
||||
/** 当前按下的按键,按按下顺序排列 */
|
||||
private holding: KeyCode[] = [];
|
||||
/** 待触发项 */
|
||||
private pending: IHotkeyPending[] = [];
|
||||
/** 最近一次激励负载 */
|
||||
private now: number = 0;
|
||||
/** 当前激励源控制器 */
|
||||
private controller: IExcitableController<number> | null = null;
|
||||
/** 键盘按下处理器 */
|
||||
private keyDownListener: HotkeyListener = event => {
|
||||
if (event.repeat) return;
|
||||
const key = this.resolveKeyCode(event.keyCode);
|
||||
if (key === null) return;
|
||||
if (this.down(key)) event.preventDefault();
|
||||
};
|
||||
/** 键盘抬起处理器 */
|
||||
private keyUpListener: HotkeyListener = event => {
|
||||
const key = this.resolveKeyCode(event.keyCode);
|
||||
if (key === null) return;
|
||||
if (this.up(key)) event.preventDefault();
|
||||
};
|
||||
|
||||
//#region 基础
|
||||
/** 当前使用的激励源 */
|
||||
private excitation: IExcitation<number> | null = null;
|
||||
|
||||
/** 获取当前栈顶作用域 */
|
||||
/** 当前正在按下的按键组成的集合 */
|
||||
private holdingSet: Set<KeyCode> = new Set();
|
||||
/** 如果按键指令都是按下时触发,那么当前的按键组合下,有可能触发哪些按键 */
|
||||
private holdingCmds: Map<IHotkeyCommand, IHotkeyCommandPending> = new Map();
|
||||
/** 需要等待 `timeout` 进行一次触发的指令集合 */
|
||||
private waitTimeout: Set<IHotkeyCommandPending> = new Set();
|
||||
|
||||
/** 键盘按下监听器 */
|
||||
private keyDownListener: (ev: KeyboardEvent) => void;
|
||||
/** 键盘抬起监听器 */
|
||||
private keyUpListener: (ev: KeyboardEvent) => void;
|
||||
|
||||
constructor() {
|
||||
const keyDown = (event: KeyboardEvent) => {
|
||||
const key = this.resolveKeyCode(event.keyCode);
|
||||
if (this.holding.includes(key)) return;
|
||||
if (key === KeyCode.Unknown) return;
|
||||
const emitted = this.down(key);
|
||||
if (emitted) event.preventDefault();
|
||||
};
|
||||
const keyUp = (event: KeyboardEvent) => {
|
||||
const key = this.resolveKeyCode(event.keyCode);
|
||||
if (key === KeyCode.Unknown) return;
|
||||
const emitted = this.up(key);
|
||||
if (emitted) event.preventDefault();
|
||||
};
|
||||
this.keyDownListener = keyDown;
|
||||
this.keyUpListener = keyUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将浏览器事件的按键码映射为按键枚举,部分浏览器的 Meta 键需要本地映射
|
||||
* @param eventKeyCode 浏览器事件的按键码
|
||||
*/
|
||||
private resolveKeyCode(eventKeyCode: number): KeyCode {
|
||||
if (
|
||||
eventKeyCode === 91 ||
|
||||
eventKeyCode === 92 ||
|
||||
eventKeyCode === 224
|
||||
) {
|
||||
return KeyCode.Meta;
|
||||
}
|
||||
const key = EVENT_KEY_CODE_MAP[eventKeyCode];
|
||||
return key ?? KeyCode.Unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前栈顶作用域
|
||||
*/
|
||||
private getScope(): IHotkeyScope | null {
|
||||
return this.scopes.at(-1) ?? null;
|
||||
}
|
||||
@ -114,253 +109,73 @@ export class HotkeySystem implements IHotkeySystem, IExcitable<number> {
|
||||
* @param type 触发方式
|
||||
* @param key 触发此指令的按键
|
||||
*/
|
||||
private createContext(
|
||||
type: HotkeyEmitType,
|
||||
key: KeyCode
|
||||
): IHotkeyEmitContext {
|
||||
const holding = this.holding.slice();
|
||||
private createContext(): IHotkeyEmitContext {
|
||||
return {
|
||||
type,
|
||||
key,
|
||||
holding,
|
||||
ctrl: holding.includes(KeyCode.Ctrl),
|
||||
shift: holding.includes(KeyCode.Shift),
|
||||
alt: holding.includes(KeyCode.Alt),
|
||||
meta: holding.includes(KeyCode.Meta)
|
||||
holding: this.holding,
|
||||
ctrl: this.holdingSet.has(KeyCode.Ctrl),
|
||||
shift: this.holdingSet.has(KeyCode.Shift),
|
||||
alt: this.holdingSet.has(KeyCode.Alt),
|
||||
meta: this.holdingSet.has(KeyCode.Meta)
|
||||
};
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 组合匹配
|
||||
|
||||
/**
|
||||
* 判断按键组合是否与按键抬起事件匹配
|
||||
* @param keys 按键组合
|
||||
* @param keys 当前的按键组合
|
||||
* @param key 抬起的按键
|
||||
*/
|
||||
private matchKeyUp(keys: KeyCode[], key: KeyCode): boolean {
|
||||
private matchKeyUp(keys: readonly KeyCode[], key: KeyCode): boolean {
|
||||
if (keys.length === 0) return false;
|
||||
const regular = keys.filter(v => !isAuxiliaryKey(v));
|
||||
if (regular.length > 1) return false;
|
||||
if (regular.length === 0) {
|
||||
return keys.length === 1 && keys[0] === key;
|
||||
// 抬起按键仅允许一个非辅助按键
|
||||
const [main, auxiliary] = matchArray(keys, v => isAuxiliaryKey(v));
|
||||
if (main.length > 1) return false;
|
||||
|
||||
// 如果只有一个按键,那么可以触发,哪怕它是辅助按键
|
||||
if (main.length === 0) {
|
||||
return auxiliary.length === 1 && auxiliary[0] === key;
|
||||
}
|
||||
if (regular[0] !== key) return false;
|
||||
return keys
|
||||
.filter(v => isAuxiliaryKey(v))
|
||||
.every(v => this.holding.includes(v));
|
||||
|
||||
// 否则需要判断触发的按键
|
||||
if (main[0] !== key) return false;
|
||||
return auxiliary.every(v => this.holdingSet.has(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断按键组合是否被当前按下的按键所满足
|
||||
* 判断一个按键组合当前是否满足
|
||||
* @param keys 按键组合
|
||||
*/
|
||||
private matchKeyDown(keys: KeyCode[]): boolean {
|
||||
if (keys.length === 0) return false;
|
||||
return keys.every(v => this.holding.includes(v));
|
||||
private matchKeys(keys: readonly KeyCode[]) {
|
||||
return keys.every(v => this.holdingSet.has(v));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 待触发
|
||||
|
||||
/**
|
||||
* 判断一个待触发项是否已经到期
|
||||
* @param item 待触发项
|
||||
* @param payload 当前激励负载
|
||||
*/
|
||||
private isPendingDue(item: IHotkeyPending, payload: number): boolean {
|
||||
if (item.fired === 0) {
|
||||
return payload - item.time >= item.timeout;
|
||||
}
|
||||
if (item.fired === 1) {
|
||||
return payload - item.last >= item.interval;
|
||||
}
|
||||
if (item.mode === HotkeyEmitMode.Throttle) {
|
||||
return payload - item.last >= item.dt;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 触发所有已经到期的待触发项 */
|
||||
private flushPending(): void {
|
||||
this.dropPending();
|
||||
const due: IHotkeyPending[] = [];
|
||||
const rest: IHotkeyPending[] = [];
|
||||
for (const item of this.pending) {
|
||||
if (this.isPendingDue(item, this.now)) {
|
||||
due.push(item);
|
||||
} else {
|
||||
rest.push(item);
|
||||
}
|
||||
}
|
||||
this.pending = rest;
|
||||
due.sort((a, b) => b.command.priority - a.command.priority);
|
||||
let blocked = -Infinity;
|
||||
for (const item of due) {
|
||||
const priority = item.command.priority;
|
||||
if (priority < blocked) continue;
|
||||
item.handler({
|
||||
command: item.id,
|
||||
index: item.index,
|
||||
keys: item.keys.slice(),
|
||||
context: this.createContext(item.type, item.key)
|
||||
});
|
||||
item.last = this.now;
|
||||
item.fired += 1;
|
||||
if (item.command.block) blocked = priority;
|
||||
if (
|
||||
item.mode === HotkeyEmitMode.Once ||
|
||||
item.mode === HotkeyEmitMode.KeyUp
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
this.pending.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
/** 丢弃已经失效的待触发项 */
|
||||
private dropPending(): void {
|
||||
const scope = this.getScope();
|
||||
const symbol = scope ? scope.symbol : null;
|
||||
this.pending = this.pending.filter(item => {
|
||||
if (item.scope !== symbol) return false;
|
||||
if (item.command.mode !== item.mode) return false;
|
||||
if (
|
||||
item.mode !== HotkeyEmitMode.KeyUp &&
|
||||
!this.matchKeyDown(item.keys)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 派发
|
||||
|
||||
/**
|
||||
* 执行一批已经按优先级降序排列的匹配项
|
||||
* @param matches 匹配项
|
||||
* @param type 触发方式
|
||||
* @param key 触发此指令的按键
|
||||
*/
|
||||
private runMatches(
|
||||
matches: IHotkeyMatch[],
|
||||
type: HotkeyEmitType,
|
||||
key: KeyCode
|
||||
): boolean {
|
||||
const scope = this.getScope();
|
||||
if (!scope) return false;
|
||||
let blocked = -Infinity;
|
||||
let triggered = false;
|
||||
for (const item of matches) {
|
||||
const priority = item.command.priority;
|
||||
if (priority < blocked) break;
|
||||
const mode = item.command.mode;
|
||||
const timeout = item.command.timeout;
|
||||
const immediate = timeout <= 0;
|
||||
if (immediate) {
|
||||
item.handler({
|
||||
command: item.id,
|
||||
index: item.index,
|
||||
keys: item.keys,
|
||||
context: this.createContext(type, key)
|
||||
});
|
||||
}
|
||||
triggered = true;
|
||||
const continuous =
|
||||
mode === HotkeyEmitMode.Continuous ||
|
||||
mode === HotkeyEmitMode.Throttle;
|
||||
if (!immediate || continuous) {
|
||||
this.pending.push({
|
||||
id: item.id,
|
||||
command: item.command,
|
||||
index: item.index,
|
||||
keys: item.keys.slice(),
|
||||
handler: item.handler,
|
||||
type,
|
||||
key,
|
||||
scope: scope.symbol,
|
||||
mode,
|
||||
timeout,
|
||||
interval: item.command.interval,
|
||||
dt: item.command.dt,
|
||||
time: this.now,
|
||||
last: immediate ? this.now : 0,
|
||||
fired: immediate ? 1 : 0
|
||||
});
|
||||
}
|
||||
if (item.command.block) blocked = priority;
|
||||
}
|
||||
return triggered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 派发一次按键事件
|
||||
* @param type 触发方式
|
||||
* @param key 触发此指令的按键
|
||||
*/
|
||||
private dispatch(type: HotkeyEmitType, key: KeyCode): boolean {
|
||||
const scope = this.getScope();
|
||||
if (!scope) return false;
|
||||
if (scope.condition !== null && !scope.condition()) return false;
|
||||
const matches: IHotkeyMatch[] = [];
|
||||
for (const [id, handler] of scope.handlers) {
|
||||
const command = this.commands.get(id);
|
||||
if (!command) continue;
|
||||
const keyDown = command.mode !== HotkeyEmitMode.KeyUp;
|
||||
if (keyDown !== (type === HotkeyEmitType.KeyDown)) continue;
|
||||
let index = -1;
|
||||
let keys: KeyCode[] = [];
|
||||
for (const [i, combination] of command.iterateKeyCombinations()) {
|
||||
const hit =
|
||||
type === HotkeyEmitType.KeyUp
|
||||
? this.matchKeyUp(combination, key)
|
||||
: this.matchKeyDown(combination);
|
||||
if (hit) {
|
||||
index = i;
|
||||
keys = combination;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index < 0) continue;
|
||||
matches.push({ id, command, index, keys, handler });
|
||||
}
|
||||
matches.sort((a, b) => b.command.priority - a.command.priority);
|
||||
return this.runMatches(matches, type, key);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 作用域
|
||||
|
||||
enterScope(): symbol {
|
||||
const symbol = Symbol();
|
||||
this.scopes.push({ symbol, condition: null, handlers: new Map() });
|
||||
this.dropPending();
|
||||
this.scopes.push({ symbol, condition: null, implements: new Map() });
|
||||
this.holdingCmds.clear();
|
||||
this.waitTimeout.clear();
|
||||
return symbol;
|
||||
}
|
||||
|
||||
leaveScope(sym?: symbol): void {
|
||||
if (sym === undefined) {
|
||||
if (!sym) {
|
||||
if (this.scopes.length === 0) {
|
||||
logger.warn(205);
|
||||
return;
|
||||
}
|
||||
this.scopes.pop();
|
||||
this.dropPending();
|
||||
return;
|
||||
this.holdingCmds.clear();
|
||||
this.waitTimeout.clear();
|
||||
}
|
||||
const index = this.scopes.findIndex(v => v.symbol === sym);
|
||||
if (index < 0) {
|
||||
logger.warn(205);
|
||||
return;
|
||||
}
|
||||
this.scopes.length = index + 1;
|
||||
this.dropPending();
|
||||
this.scopes.splice(index + 1);
|
||||
this.holdingCmds.clear();
|
||||
this.waitTimeout.clear();
|
||||
}
|
||||
|
||||
when(func: () => boolean): this {
|
||||
@ -373,17 +188,18 @@ export class HotkeySystem implements IHotkeySystem, IExcitable<number> {
|
||||
return this;
|
||||
}
|
||||
|
||||
implement(command: string, func: HotkeyHandler): this {
|
||||
implement(command: string, func: (ctx: IHotkeyEmitInfo) => void): this {
|
||||
const scope = this.getScope();
|
||||
if (!scope) {
|
||||
logger.warn(201);
|
||||
return this;
|
||||
}
|
||||
if (!this.commands.has(command)) {
|
||||
const obj = this.commands.get(command);
|
||||
if (!obj) {
|
||||
logger.warn(202, command);
|
||||
return this;
|
||||
}
|
||||
scope.handlers.set(command, func);
|
||||
scope.implements.set(obj, func);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -407,80 +223,235 @@ export class HotkeySystem implements IHotkeySystem, IExcitable<number> {
|
||||
}
|
||||
|
||||
deleteCommand(command: string): void {
|
||||
if (!this.commands.delete(command)) return;
|
||||
this.scopes.forEach(scope => scope.handlers.delete(command));
|
||||
this.pending = this.pending.filter(item => item.id !== command);
|
||||
const obj = this.commands.get(command);
|
||||
if (!obj) return;
|
||||
this.commands.delete(command);
|
||||
this.scopes.forEach(scope => scope.implements.delete(obj));
|
||||
this.holdingCmds.delete(obj);
|
||||
const toDeleteWait = new Set<IHotkeyCommandPending>();
|
||||
this.waitTimeout.forEach(v => {
|
||||
if (v.command === obj) {
|
||||
toDeleteWait.add(v);
|
||||
}
|
||||
});
|
||||
toDeleteWait.forEach(v => {
|
||||
this.waitTimeout.delete(v);
|
||||
});
|
||||
}
|
||||
|
||||
*iterateCommands(): Iterable<IHotkeyCommand> {
|
||||
yield* this.commands.values();
|
||||
iterateCommands(): Iterable<IHotkeyCommand> {
|
||||
return this.commands.values();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 按下与抬起
|
||||
|
||||
/**
|
||||
* 触发指定按键指令
|
||||
* @param command 按键指令对象
|
||||
* @param pending 按键指令的 pending 信息
|
||||
*/
|
||||
private emit(pending: IHotkeyCommandPending) {
|
||||
const command = pending.command;
|
||||
const scope = this.getScope();
|
||||
if (!scope) return;
|
||||
const impl = scope.implements.get(command);
|
||||
if (!impl) return;
|
||||
const info: IHotkeyEmitInfo = {
|
||||
mode: command.mode,
|
||||
command,
|
||||
index: pending.keyIndex,
|
||||
keys: pending.keys,
|
||||
context: pending.context
|
||||
};
|
||||
impl(info);
|
||||
pending.emitted++;
|
||||
pending.lastEmit = this.excitation?.payload() ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取首个能够触发按键指令的按键组合
|
||||
* @param command 按键指令
|
||||
*/
|
||||
private findMatchKeys(
|
||||
command: IHotkeyCommand
|
||||
): [index: number, readonly KeyCode[]] | [] {
|
||||
for (const [index, keys] of command.iterateKeyCombinations()) {
|
||||
const match = keys.every(v => this.holdingSet.has(v));
|
||||
if (match) return [index, keys];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加可能触发的按键指令
|
||||
* @param command 按键指令对象
|
||||
* @param index 触发的按键索引
|
||||
* @param keys 按键组合
|
||||
*/
|
||||
private addPending(
|
||||
command: IHotkeyCommand,
|
||||
index: number,
|
||||
keys: readonly KeyCode[]
|
||||
) {
|
||||
const pending: IHotkeyCommandPending = {
|
||||
command,
|
||||
start: this.excitation?.payload() ?? 0,
|
||||
keyIndex: index,
|
||||
keys,
|
||||
context: this.createContext(),
|
||||
lastEmit: -Infinity,
|
||||
emitted: 0
|
||||
};
|
||||
this.holdingCmds.set(command, pending);
|
||||
if (command.timeout <= 0 && command.mode !== HotkeyEmitMode.KeyUp) {
|
||||
// 如果没有 timeout,那么需要立即触发一次
|
||||
this.emit(pending);
|
||||
}
|
||||
}
|
||||
|
||||
down(key: KeyCode): boolean {
|
||||
if (this.holding.includes(key)) return false;
|
||||
this.holding.push(key);
|
||||
return this.dispatch(HotkeyEmitType.KeyDown, key);
|
||||
if (this.holdingSet.has(key)) {
|
||||
// 如果有按键可能触发,那么返回 true 来 preventDefault
|
||||
return this.holdingCmds.size > 0 || this.waitTimeout.size > 0;
|
||||
} else {
|
||||
this.holding.push(key);
|
||||
this.holdingSet.add(key);
|
||||
}
|
||||
for (const command of this.commands.values()) {
|
||||
const match = this.findMatchKeys(command);
|
||||
if (match.length === 0) continue;
|
||||
const [index, keys] = match;
|
||||
const pending = this.holdingCmds.get(command);
|
||||
|
||||
if (pending) {
|
||||
// 如果之前有过这个按键指令,那么直接修改现有状态
|
||||
pending.keyIndex = index;
|
||||
pending.keys = keys;
|
||||
pending.context = this.createContext();
|
||||
} else {
|
||||
// 如果之前没有这个按键指令,那么加入执行列表
|
||||
this.addPending(command, index, keys);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// 如果有按键可能触发,那么返回 true 来 preventDefault
|
||||
return this.holdingCmds.size > 0 || this.waitTimeout.size > 0;
|
||||
}
|
||||
|
||||
up(key: KeyCode): boolean {
|
||||
if (!this.holdingSet.has(key)) return false;
|
||||
this.holdingSet.delete(key);
|
||||
const index = this.holding.indexOf(key);
|
||||
if (index < 0) return false;
|
||||
this.holding.splice(index, 1);
|
||||
this.dropPending();
|
||||
return this.dispatch(HotkeyEmitType.KeyUp, key);
|
||||
|
||||
// 需要快照
|
||||
for (const [command, pending] of new Map(this.holdingCmds)) {
|
||||
// 对于 KeyUp,如果有可以触发的,那么按规则触发
|
||||
if (command.mode === HotkeyEmitMode.KeyUp) {
|
||||
const emit = this.matchKeyUp(pending.keys, key);
|
||||
if (emit) {
|
||||
this.emit(pending);
|
||||
this.holdingCmds.delete(command);
|
||||
}
|
||||
} else {
|
||||
// 对于其他按键,如果一个按下组合按键不再满足,那么需要删除或加入 timeout
|
||||
if (!this.matchKeys(pending.keys)) {
|
||||
// 松开一个按键,那么并不一定会导致按下指令不再触发
|
||||
const newMatch = this.findMatchKeys(command);
|
||||
if (newMatch.length === 0) {
|
||||
// 没有新的匹配,分两种情况
|
||||
if (pending.emitted === 0) {
|
||||
// 还没触发过,加入 timeout
|
||||
this.waitTimeout.add(pending);
|
||||
} else {
|
||||
// 已经触发过了,直接删除
|
||||
this.holdingCmds.delete(command);
|
||||
}
|
||||
} else {
|
||||
// 如果有新的匹配,那么直接修改状态
|
||||
const [index, keys] = newMatch;
|
||||
pending.context = this.createContext();
|
||||
pending.keyIndex = index;
|
||||
pending.keys = keys;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有按键可能触发,那么返回 true 来 preventDefault
|
||||
return this.holdingCmds.size > 0 || this.waitTimeout.size > 0;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 激励
|
||||
|
||||
excitedBy(excitation: IExcitation<number>): void {
|
||||
if (this.controller) {
|
||||
this.controller.revoke();
|
||||
excitedBy(excitation: IExcitation<number> | null): void {
|
||||
if (this.excitation) {
|
||||
this.excitation.remove(this);
|
||||
}
|
||||
this.excitation = excitation;
|
||||
if (excitation) {
|
||||
excitation.add(this);
|
||||
}
|
||||
this.controller = excitation.add(this);
|
||||
}
|
||||
|
||||
excited(payload: number): void {
|
||||
this.now = payload;
|
||||
this.flushPending();
|
||||
// 先判定 timeout,需要快照
|
||||
for (const pending of new Set(this.waitTimeout)) {
|
||||
const dt = payload - pending.start;
|
||||
if (dt >= pending.command.timeout) {
|
||||
this.emit(pending);
|
||||
this.waitTimeout.delete(pending);
|
||||
}
|
||||
}
|
||||
|
||||
// 再看 holding,这个不会删,所以不需要快照
|
||||
for (const [command, pending] of this.holdingCmds) {
|
||||
// 抬起不会在这里触发
|
||||
if (command.mode === HotkeyEmitMode.KeyUp) continue;
|
||||
// 首次触发
|
||||
const dt = payload - pending.start;
|
||||
const dt2 = payload - pending.lastEmit;
|
||||
if (pending.emitted === 0 && dt < command.timeout) {
|
||||
this.emit(pending);
|
||||
}
|
||||
if (command.mode === HotkeyEmitMode.Continuous) {
|
||||
if (pending.emitted === 1) {
|
||||
// 先看第二次触发
|
||||
if (dt2 >= command.interval) {
|
||||
this.emit(pending);
|
||||
}
|
||||
} else {
|
||||
// 其他的要看 dt 了
|
||||
if (dt2 >= command.dt) {
|
||||
this.emit(pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 监听
|
||||
|
||||
/**
|
||||
* 将浏览器事件的按键码映射为按键枚举,部分浏览器的 Meta 键需要本地映射
|
||||
* @param eventKeyCode 浏览器事件的按键码
|
||||
*/
|
||||
private resolveKeyCode(eventKeyCode: number): KeyCode | null {
|
||||
if (
|
||||
eventKeyCode === 91 ||
|
||||
eventKeyCode === 92 ||
|
||||
eventKeyCode === 224
|
||||
) {
|
||||
return KeyCode.Meta;
|
||||
}
|
||||
const key = EVENT_KEY_CODE_MAP[eventKeyCode];
|
||||
return key ? key : null;
|
||||
}
|
||||
|
||||
listen(target: HTMLElement | null): void {
|
||||
if (this.listening === target) return;
|
||||
|
||||
if (this.listening) {
|
||||
this.listening.removeEventListener('keydown', this.keyDownListener);
|
||||
this.listening.removeEventListener('keyup', this.keyUpListener);
|
||||
}
|
||||
this.listening = target;
|
||||
|
||||
if (target) {
|
||||
target.addEventListener('keydown', this.keyDownListener);
|
||||
target.addEventListener('keyup', this.keyUpListener);
|
||||
}
|
||||
|
||||
this.listening = target;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
import { IExcitation } from '@motajs/animate';
|
||||
import { KeyCode } from './keyCodes';
|
||||
|
||||
// TODO: 适配存档,可能需要把 ISaveableContent 挪到 @motajs/common,或让 packages-user 自己负责存档
|
||||
|
||||
export const enum HotkeyEmitType {
|
||||
/** 由按键按下触发 */
|
||||
KeyDown,
|
||||
/** 由按键抬起触发 */
|
||||
KeyUp
|
||||
}
|
||||
// 需要注意的是,所有的 “最小触发间隔” 指的都是系统能保证两次按键的触发间隔不小于此值,并不能保证等于此值,
|
||||
// 更不能保证连续触发 n 次后,总体经过的时间为 n 乘以此间隔。
|
||||
// 例如,如果触发了 10 次,触发间隔为 100ms,那么经过的时间一定大于等于 1000ms,绝大多数情况下都会大于 1000ms。
|
||||
// 因此,不要拿这个触发间隔作为精确计时器,需要精确计时器时使用传递给系统的激励源的 `payload` 接口获取。
|
||||
|
||||
export const enum HotkeyEmitMode {
|
||||
/** 按键抬起时触发 */
|
||||
@ -16,35 +12,42 @@ export const enum HotkeyEmitMode {
|
||||
/** 按下时只会触发一次 */
|
||||
Once,
|
||||
/** 按下后依据激励源进行触发 */
|
||||
Continuous,
|
||||
/** 按下后节流触发 */
|
||||
Throttle
|
||||
Continuous
|
||||
}
|
||||
|
||||
export interface IHotkeyEmitContext {
|
||||
/** 按键触发方式 */
|
||||
readonly type: HotkeyEmitType;
|
||||
/** 触发此指令的按键 */
|
||||
readonly key: KeyCode;
|
||||
/** 当前所有正在按下的按键,按照按下的顺序排列,注意如果是抬起事件触发的,那么这里面并不会包含触发事件的那个按键 */
|
||||
readonly holding: KeyCode[];
|
||||
/** 当前是否按下了 Ctrl 按键 */
|
||||
/**
|
||||
* 指令触发时,当前所有正在按下的按键,按照按下的顺序排列。如果是抬起事件触发的,那么这里面并不会包含触发事件的那个主要按键。
|
||||
* 注意该成员的含义不是当前时刻的按下的按键,对于连续触发以及 `timeout`,依然会包含触发此指令的时刻按下的按键。
|
||||
* 也就是说,它并不是当前时刻按下的按键,不要用它获取当前时刻正在按下的按键。参考时间线:
|
||||
*
|
||||
* ```txt
|
||||
* ----------|---------------|---------->
|
||||
* | timeout |
|
||||
* 指令触发时刻 指令延时触发
|
||||
* 记录的是这个 而不是这个
|
||||
* ```
|
||||
*/
|
||||
readonly holding: readonly KeyCode[];
|
||||
/** 指令触发时是否按下了 Ctrl 按键 */
|
||||
readonly ctrl: boolean;
|
||||
/** 当前是否按下了 Shift 按键 */
|
||||
/** 指令触发时是否按下了 Shift 按键 */
|
||||
readonly shift: boolean;
|
||||
/** 当前是否按下了 Alt 按键 */
|
||||
/** 指令触发时是否按下了 Alt 按键 */
|
||||
readonly alt: boolean;
|
||||
/** 当前是否按下了 Windows(Windows) / Command(MacOS) 按键 */
|
||||
/** 指令触发时是否按下了 Windows(Windows) / Command(MacOS) 按键 */
|
||||
readonly meta: boolean;
|
||||
}
|
||||
|
||||
export interface IHotkeyEmitInfo {
|
||||
/** 按键指令标识符 */
|
||||
readonly command: string;
|
||||
/** 按键触发方式 */
|
||||
readonly mode: HotkeyEmitMode;
|
||||
/** 按键指令对象 */
|
||||
readonly command: IHotkeyCommand;
|
||||
/** 触发此按键指令的按键组合对应索引 */
|
||||
readonly index: number;
|
||||
/** 触发此按键指令的按键组合 */
|
||||
readonly keys: KeyCode[];
|
||||
/** 触发此按键指令的按键组合。如果有两组按键组合能触发,那么仅包含触发较晚的那一个 */
|
||||
readonly keys: readonly KeyCode[];
|
||||
/** 按键触发上下文 */
|
||||
readonly context: IHotkeyEmitContext;
|
||||
}
|
||||
@ -52,10 +55,10 @@ export interface IHotkeyEmitInfo {
|
||||
export interface IHotkeyCommandInit {
|
||||
/** 按键指令优先级 */
|
||||
readonly priority: number;
|
||||
/** 是否阻断优先级精确比此按键指令更低的指令的执行,同优先级的会全部执行,并不会阻断 */
|
||||
readonly block: boolean;
|
||||
/** 默认按键组合,可设置多组 */
|
||||
readonly defaults: KeyCode[][];
|
||||
/** 是否阻断优先级精确比此按键指令更低的指令的执行,同优先级的会全部执行,并不会阻断,默认不阻断 */
|
||||
readonly block?: boolean;
|
||||
/** 按键触发方式,默认为抬起时触发 */
|
||||
readonly mode?: HotkeyEmitMode;
|
||||
/** 从指定按键组合被触发,至指令首次被真正执行期间,最少经过的时间 */
|
||||
@ -67,6 +70,8 @@ export interface IHotkeyCommandInit {
|
||||
}
|
||||
|
||||
export interface IHotkeyCommand {
|
||||
/** 按键指令的标识符 */
|
||||
readonly command: string;
|
||||
/** 按键指令优先级,越高越先执行,如果同优先级,则无法保证顺序 */
|
||||
readonly priority: number;
|
||||
/** 触发后,是否阻断优先级比当前优先级更低的按键的触发 */
|
||||
@ -79,7 +84,7 @@ export interface IHotkeyCommand {
|
||||
readonly interval: number;
|
||||
/** 按下后距离第一次触发的最小时间间隔 */
|
||||
readonly timeout: number;
|
||||
/** Throttle 模式下,两次触发之间的最小时间间隔 */
|
||||
/** 连续触发模式下,两次触发之间的最小时间间隔 */
|
||||
readonly dt: number;
|
||||
|
||||
/**
|
||||
@ -93,18 +98,20 @@ export interface IHotkeyCommand {
|
||||
* @param index 按键组合索引
|
||||
* @param keys 按键组合
|
||||
*/
|
||||
setKeyCombination(index: number, keys: KeyCode[]): void;
|
||||
setKeyCombination(index: number, keys: readonly KeyCode[]): void;
|
||||
|
||||
/**
|
||||
* 获取指定索引的按键组合,如果组合不存在返回空数组
|
||||
* @param index 按键组合索引
|
||||
*/
|
||||
getKeyCombination(index: number): KeyCode[];
|
||||
getKeyCombination(index: number): readonly KeyCode[];
|
||||
|
||||
/**
|
||||
* 迭代所有的按键组合
|
||||
*/
|
||||
iterateKeyCombinations(): Iterable<[index: number, key: KeyCode[]]>;
|
||||
iterateKeyCombinations(): Iterable<
|
||||
[index: number, key: readonly KeyCode[]]
|
||||
>;
|
||||
|
||||
/**
|
||||
* 抬起后,在经过 `timeout` 时间后触发一次
|
||||
@ -122,29 +129,23 @@ export interface IHotkeyCommand {
|
||||
* 当按下后,在经过 `timeout` 时间后进行一次触发,之后在松开前,会按照激励源的激励触发
|
||||
* @param interval 第一次触发距离第二次触发的最小时间间隔,设置后,按下时会立刻进行一次触发,
|
||||
* 而第二次触发需要等待 `interval` 毫秒才会触发。默认为 0
|
||||
* @param dt 节流间隔,默认为 0,即每次激励源的激励都会触发
|
||||
* @param timeout 按下后距离首次触发的时间间隔,单位号码,默认为 0
|
||||
*/
|
||||
continuous(interval?: number, timeout?: number): void;
|
||||
|
||||
/**
|
||||
* 当按下后,在经过 `timeout` 时间后进行一次触发,之后在松开前,会按照激励源的激励触发,两次触发的触发间隔不小于节流间隔
|
||||
* @param dt 节流间隔
|
||||
* @param interval 第一次触发距离第二次触发的最小时间间隔,设置后,按下时会立刻进行一次触发,
|
||||
* 而第二次触发需要等待 `interval` 毫秒才会触发。默认为 0
|
||||
* @param timeout 按下后距离首次触发的时间间隔,单位号码,默认为 0
|
||||
*/
|
||||
throttle(dt: number, interval?: number, timeout?: number): void;
|
||||
continuous(interval?: number, dt?: number, timeout?: number): void;
|
||||
}
|
||||
|
||||
export interface IHotkeySystem {
|
||||
/** 当前正在监听的元素 */
|
||||
readonly listening: HTMLElement | null;
|
||||
/** 当前正在按下的按键,按照按下的顺序排序 */
|
||||
readonly holding: readonly KeyCode[];
|
||||
|
||||
/**
|
||||
* 设置当前按键系统由指定激励源所激励,用于连续触发按下后持续触发等触发场景
|
||||
* @param excitation 激励源
|
||||
*/
|
||||
excitedBy(excitation: IExcitation<number>): void;
|
||||
excitedBy(excitation: IExcitation<number> | null): void;
|
||||
|
||||
/**
|
||||
* 进入新的按键作用域
|
||||
|
||||
@ -6,17 +6,20 @@ import {
|
||||
IClientPluginDefine
|
||||
} from '@user/client-modules';
|
||||
import { ICoreStateCoreConfig } from '@user/data-state';
|
||||
import {
|
||||
DEBUG_DIVIDER,
|
||||
DEBUG_VARIATOR,
|
||||
DIVIDER_DEBUG_DIVIDER,
|
||||
VARIATOR_DEBUG_SPEED
|
||||
} from '../constant';
|
||||
import { ExcitationVariator } from '@motajs/animate';
|
||||
import { SaveCompression } from '@user/data-common';
|
||||
import { IClientHotkeySave } from '@user/client-base';
|
||||
import {
|
||||
DEBUG_VARIATOR,
|
||||
VARIATOR_DEBUG_SPEED,
|
||||
DEBUG_DIVIDER,
|
||||
DIVIDER_DEBUG_DIVIDER
|
||||
} from './constant';
|
||||
|
||||
export class MainClientPlugin implements IClientPluginDefine {
|
||||
id: string = 'main-client-plugin';
|
||||
clientDeps: readonly string[] = [];
|
||||
dataDeps: readonly string[] = [];
|
||||
|
||||
awake(state: IClientCore): void {
|
||||
// 渲染调试配置
|
||||
@ -50,5 +53,12 @@ export class MainClientPlugin implements IClientPluginDefine {
|
||||
|
||||
// 存档初始化
|
||||
state.save.init(`@game/${core.name}`);
|
||||
|
||||
// 按键不是跟随存档的,而是跟随游戏的,所以不进 state 的 addSaveableContent,而是单独调用 global
|
||||
const hotkey =
|
||||
await state.save.getGlobal<IClientHotkeySave>('@system/hotkey');
|
||||
if (hotkey) {
|
||||
state.hotkey.loadState(hotkey, SaveCompression.NoCompression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
src/client/misc/hotkey.ts
Normal file
22
src/client/misc/hotkey.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { KeyCode } from '@motajs/system';
|
||||
import { IClientCore } from '@user/client-modules';
|
||||
|
||||
export function initializeHotkey(state: IClientCore) {
|
||||
const hotkey = state.hotkey;
|
||||
const system = hotkey.system;
|
||||
|
||||
//#region 通用按键
|
||||
|
||||
hotkey.setGroup('@system/common', [
|
||||
system.addCommand('ok', {
|
||||
priority: 0,
|
||||
defaults: [[KeyCode.Enter], [KeyCode.Space], [KeyCode.KeyC]]
|
||||
}),
|
||||
system.addCommand('exit', {
|
||||
priority: 0,
|
||||
defaults: [[KeyCode.Escape], [KeyCode.KeyX]]
|
||||
})
|
||||
]);
|
||||
|
||||
//#endregion
|
||||
}
|
||||
0
src/client/misc/index.ts
Normal file
0
src/client/misc/index.ts
Normal file
Loading…
Reference in New Issue
Block a user