图形编辑器开发:快捷键的管理(图形编辑器怎么用)
cac55 2024-09-20 12:50 39 浏览 0 评论
大家好,我是前端西瓜哥。
快捷键操作在图形编辑器中是很高频的操作,能让用户快速高效地执行特定命令。
那么今天就来学习图形编辑器是如何做快捷键的管理的。
编辑器 github 地址:
https://github.com/F-star/suika
线上体验:
https://blog.fstars.wang/app/suika/
简单的快捷键绑定
我们先看看原生的键盘事件能否满足需求。
假设我们需要判断用户是否按下了 Ctrl + C(需要精准匹配),如果按下了就执行 copy 方法。
用原生事件,我们要这样写:
window.addEventListener('keydown', (e) => {
const { ctrlKey, shiftKey, altKey, metaKey } = e;
if (ctrlKey && !shiftKey && !altKey && !metaKey && e.code === 'KeyC') {
copy();
}
})
写法有点繁琐。我们希望能简化一下写法。
一开始我并不太在意快捷键绑定的管理,因为复杂度还没起来,就找了一个轮子 hotkeys-js。
import hotkeys from 'hotkeys-js';
hotkeys('ctrl+c', copy);
hotkeys-js 是原生事件的一层简单的封装,简化了写法并提高了可读性。
如果你的图形编辑器并不复杂,用一些易用性不错的快捷键库是不错的选择。
快捷键高级能力
原生事件和一些常见的快捷键库可以处理一些简单的场景,但图形编辑器的场景往往更复杂。
图形编辑器还需要的快捷键高级能力有:
- 给一个行为设置多个不同快捷键,比如 Delete 或 Backspace 都可以删除选中元素(这个大多第三方快捷键轮子是支持的);
- 可以根据不同操作系统绑定不同的快捷键,比如复制,我希望在 Windows 系统为 Ctrl+C,在 MacOS 系统则是 Command+C;
- 提供环境上下文,绑定的函数可以通过它决定是否被调用,比如我希望移动图形的时候不能执行 Delete 对应删除操作;
- 支持短路匹配,只执行第一个匹配条件。这是为了防止快捷键冲突,一个快捷键执行了多个行为。当然如果你就是希望一个快捷键要执行多个行为,那可以考虑补充一个 next 方法。
- 某个快捷键绑定可以设置为高优先级,比如激活某个工具时,要注册一些快捷键,需要高优先级,以便覆盖掉和其他的同名快捷键;
快捷键管理类
考虑上面这些功能点,我们来实现这个快捷键管理类 KeyBindingManager。
class KeyBindingManager {
// 传入一个入口类对象 Editor,之后需要用到它的变量
constructor(private editor: Editor) {}
}
keyBinding 对象
一份快捷键绑定(keyBinding)由下面几个部分组成:
(1)key,快捷键描述。理论上应该用 "Ctrl+C" 这种字符串来描述,但它实现起来比较麻烦,要解析,要转换(比如 / 要转成 Slash 去匹配 event.code)。
所以我换成了一个对象:{ CtrlKey: true, keyCode: 'KeyC' }。不用解析,不用转换,直接和 event 的属性对比即可。这个是 精准 匹配,即不能有多余的修饰键。
此外,key 也支持传入数组,这种情况比较少,对应一个行为有多个快捷键的情况。比如删除操作,我们可以传入 [{ keyCode: 'Delete' }, { keyCode: 'Backspace' }]。
(2)winKey,快捷键描述(Windows 特供版)。这个参数是可选的,如果不提供,所有系统都会使用 key 参数。如果提供,且用户操作系统为 Windows,会使用 winKey,忽略 key。
(3)when,是否满足上下文。也是可选的。when 是一个方法,可以通过它拿到一些上下文参数,通过这些参数决定返回的布尔值。如果为 true,表示匹配到了,并执行对应的响应行为;如果为 false,没匹配到,继续找下一个。when 可不提供,表示永远满足条件。
(4)action,快捷键匹配后要执行的方法。
TypeScript 类型签名为:
interface IKeyBinding {
key: IKey | IKey[];
winKey?: IKey | IKey[];
when?: (ctx: IWhenCtx) => boolean;
action: (e: KeyboardEvent) => void;
}
interface IKey {
ctrlKey?: boolean;
shiftKey?: boolean;
altKey?: boolean;
metaKey?: boolean;
// KeyboardEvent['code'] 或 '*'(匹配任何按键)
keyCode: string;
}
interface IWhenCtx {
isToolDragging: boolean; // 是否在拖拽中(比如移动工具移动图形中)
}
快捷键注册
我们需要用有序表来根据注册顺序保存 keyBinding 的,这里我选择用 Map 数据结构,它是一种有序数据结构。
class KeyBindingManager {
// 用 Map
private keyBindingMap = new Map<number, IKeyBinding>();
private id = 0;
//...
// 注册一个快捷键
register(keybinding: IKeyBinding) {
const id = this.id;
this.keyBindingMap.set(id, keybinding);
this.id++;
return id;
}
// 注销快捷键
unregister(id: number) {
this.keyBindingMap.delete(id);
}
}
注册方法 register 会返回一个唯一 id,如果需要注销,需要将这个 id 传给注销方法 unregister。
事件的解绑方式有 3 种,这里选择的是类似 setTimeout 返回一个订阅 id 的风格。
《事件订阅的几种实现风格》
实际上 3 种写法都没啥差别,都是要把绑定事件方法返回的结果保存下来,在合适的时机调用解绑方法。
哦对了,还有注册高优先级快捷键的方法:
class KeyBindingManager {
// ...
// 绑定一个高优先级快捷键绑定(会放到 Map 的开头)
registerWithHighPrior(keybinding: IKeyBinding) {
const id = this.id;
const map = new Map<number, IKeyBinding>();
map.set(id, keybinding);
for (const [key, val] of this.keyBindingMap) {
map.set(key, val);
}
this.keyBindingMap = map;
this.id++;
return id;
}
}
其实就是把这个快捷键注册到 Map 的开头。
如果你需要更细的粒度,比如低优先级、中优先级、高优先级,那你可以考虑传多一个优先级枚举值或一个数值,然后在正确的位置插入。感觉并没有太多需要用到这种粒度的场景。
短路匹配逻辑
然后就是快捷键的匹配逻辑:
- 匹配顺序根据注册顺序(有特例,就是前面说的高优先级快捷键绑定,会插队,插到队伍开头);
- 使用精准匹配(key 或 winKey),以及 when 方法是否为 true,都为 true 时执行 action;
- 使用短路逻辑,即只执行第一个匹配的(后面可能也有其他匹配的,但不执行)。这个其实是设计模式的责任链模式,像是 express 或 koa 的路由匹配机制也是责任链模式。
实现如下:
const isWindows =
navigator.platform.toLowerCase().includes('win') ||
navigator.userAgent.includes('Windows');
class KeyBindingManager {
// ...
// 绑定到原生键盘按下事件上
bindEvent() {
if (this.isBound) return;
this.isBound = true;
document.addEventListener('keydown', this.handleAction);
}
// 找到匹配的 keyBinding,执行其 action
private handleAction = (e: KeyboardEvent) => {
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement
) {
return;
}
let isMatch = false;
// 生成上下文对象,可根据需要扩充
const ctx: IWhenCtx = {
isToolDragging: this.editor.toolManager.isDragging,
};
for (const keyBinding of this.keyBindingMap.values()) {
// 先看看 when 是否为 true(when 可不提供)
if (!keyBinding.when || keyBinding.when(ctx)) {
// 如果是 Windows 操作系统,看看 winKey 对不对
if (isWindows && keyBinding.winKey) {
if (this.isKeyMatch(keyBinding.winKey, e)) {
isMatch = true;
}
}
// 其他操作系统,看 key 是否匹配
else if (this.isKeyMatch(keyBinding.key, e)) {
isMatch = true;
}
}
// 匹配
if (isMatch) {
e.preventDefault();
keyBinding.action(e); // 执行对应 action(行为)
break; // 结束,不继续遍历
}
}
};
private isKeyMatch(key: IKey | IKey[], e: KeyboardEvent): boolean {
if (Array.isArray(key)) {
return key.some((k) => this.isKeyMatch(k, e));
}
if (key.keyCode == '*') return true;
const {
ctrlKey = false,
shiftKey = false,
altKey = false,
metaKey = false,
} = key;
return (
ctrlKey == e.ctrlKey &&
shiftKey == e.shiftKey &&
altKey == e.altKey &&
metaKey == e.metaKey &&
key.keyCode == e.code
);
}
}
用法举例
类写好了,看看用法。
删除快捷键的写法:
const deleteAction = () => {
// 删除选中元素
};
editor.keybindingManager.register({
// Backspace 或 Delete 都可以删除
key: [{ keyCode: 'Backspace' }, { keyCode: 'Delete' }],
// 只能在没有发生拖拽的情况下下删除(比如移动图形时不能删除)
when: (ctx) => !ctx.isToolDragging,
action: deleteAction,
});
复制快捷键的写法:
const copyHandler = () => {
// 复制
}
editor.keybindingManager.register({
key: { metaKey: true, keyCode: 'KeyC' },
// Windows 环境下的快捷键
winKey: { ctrlKey: true, keyCode: 'KeyC' },
action: copyHandler,
});
一些优化点
- 如果你考虑一些非美式键盘,比如法语键盘,因为按键布局位置发生了变化,需要做键位的重映射,确保物理位置不变,确保用户的肌肉记忆有效。
- 简化快捷键描述的写法,使用类似 Ctrl+/ 的更简洁写法。如果你需要类似 VSCode 一样提供 JSON 文件给支持用户自己设置快捷键,这个还是要实现的。
结尾
我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。
相关推荐
- iphone6自定义铃声设置流程(iphone6怎么自定义来电铃声)
-
苹果iphone6自定义铃声如何设置,iphone6自定义铃声怎么设置,iphone6自定义铃声设置教程,下面小编给大家分享一下。设置自定义铃声首先要自己制作或者找到一个铃声音频。1、在电脑上面把已经...
- iphone手机三分钟更换铃声,无需电脑直接手机操作
-
iPhone怎么在手机上换铃声?无需电脑!1分钟教你给苹果手机换铃声众所周知,苹果手机的ios系统是比较封闭的,封闭系统就会给我们带来一些不便,这里要说的就是苹果手机更换个性化铃声就比较麻烦,因为io...
- iPhone手机个性铃声设置详细教程(iphone个性铃声怎么设置)
-
iPhone现在已成街机了。朋友聚在一起的时候,是不是总有种以为是自己手机响了的赶脚。那么,小编今天跟大家分享一下iPhone怎么换铃声?,让你轻松设置属于你的个性铃声。1:电脑端安装iTools,安...
- iPhone也能用自己喜欢的铃声了,2分钟包搞定!
-
听到超好听的铃声,怎样才能放进iPhone里?这貌似是一道千古难题。90%的iPhone小白:听到这么爽脆带感的iPhone铃声,我要用!我要用!我要用!视频然而打开iTunes发现,脑子一片空白……...
- 苹果iOS 26隐藏新铃声曝光:强调“玻璃质感”
-
IT之家6月20日消息,苹果在iOS26中隐藏了一个新的铃声,这是现有默认铃声“Reflection”的改版。“Reflection”自2017年iPhoneX问世以来一直是系...
- 苹果ios14充电提示音怎么设置 iPhone手机修改充电提示音教程
-
ios14充电提示音最近很火爆,大家都想要知道苹果设置充电提示音的方法,个性化的设置非常吸引大家,小编也会在这里教大家ios14苹果充电提示音的设置,操作的流程会直接分享在下方,各位玩家们都能一起来看...
- iPhone苹果16自定义来电铃声攻略(ios自定义铃声怎么设置)
-
在智能手机的个性化设置中,更改来电铃声是展现自我风格的一种方式。对于使用iPhone苹果16的用户来说,设置一个独特的来电铃声,不仅能提升接听体验,还能让日常生活更加多彩。以下是如何为iPhone苹果...
- 未越狱iPhone用户自定义来电铃声和短信铃声的教程
-
其实自定义来电铃声和短信铃声的过程没有什么区别,但要注意,来电铃声的播放时间不能超过40秒,短信铃声的播放时间不能超过30秒,这也就是说长度为30-40秒的仅在iPhone的来电铃声中出现,少于30秒...
- 苹果手机怎么设置闹钟铃声?更改为歌曲铃声,亲测有效
-
很不是有很多小伙伴每天早上都被苹果手机刺耳的“雷达”闹钟铃声给吵醒呢?想要更换一个舒缓的闹钟铃声,却发现自己鼓捣半天却无法更换喜欢的歌曲闹钟铃声。苹果手机怎么设置闹钟铃声?下面小编就来分享如何将苹果手...
- 独家教程:iPhone手机铃声制作与更换,一般人我不告诉他
-
今天刚好自己作铃声。。想到吧里好多人都会问怎么制作铃声。于是顺便截图发吧里。虽然百度一下铃声制作已经泛滥。但是还是会一直有小白问。所以这里会详细图文说明。先介绍小白式铃声制作。http://www....
- 苹果用户iTunes自制自定义铃声教程
-
怎么制作iphone6铃声,如何使用iTunes剪切音乐自制铃声,新版iTunes怎么自制铃声,相信很多苹果用户都想使用一些个性化铃声。那么就学习一下这个方法吧。1、在电脑上面打开iTunes,选择编...
- 轻松学会!苹果手机怎么设置铃声来电铃声(3个方法)
-
在日常生活中,手机的铃声扮演着非常重要的角色,不仅是接收来电的提示,更是展示个人品位和风格的方式之一。而对于苹果手机用户来说,定制来电铃声可以让手机更具个性化,并且让您在繁忙的环境中更容易识别重要的来...
- iPhone13怎么设置来电铃声?苹果13自定义铃声操作教程
-
我们知道,苹果iOS系统是相对封闭的,在很多功能设置上与安卓机大有不同。就如来电铃声,很多苹果用户就不喜欢使用默认的那些来电铃声,想要自定义铃声,却不知如何操作。最近,新购入iPhone13系列机型的...
- iPhone 免费铃声:iOS 26 终于让设置变得更简单
-
苹果终于开窍了——省去了最烦人的繁琐步骤。iOS26终结了多年来困扰iPhone用户的麻烦操作:现在无需通过GarageBand应用,仅需轻点几下就能设置自定义铃声。完全免费、直接操作且极...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- iphone6自定义铃声设置流程(iphone6怎么自定义来电铃声)
- iphone手机三分钟更换铃声,无需电脑直接手机操作
- iPhone手机个性铃声设置详细教程(iphone个性铃声怎么设置)
- iPhone也能用自己喜欢的铃声了,2分钟包搞定!
- 苹果iOS 26隐藏新铃声曝光:强调“玻璃质感”
- 苹果ios14充电提示音怎么设置 iPhone手机修改充电提示音教程
- 苹果iphone手机直接设置铃声教程(ios怎么直接设置铃声)
- iPhone苹果16自定义来电铃声攻略(ios自定义铃声怎么设置)
- 未越狱iPhone用户自定义来电铃声和短信铃声的教程
- 苹果手机怎么设置闹钟铃声?更改为歌曲铃声,亲测有效
- 标签列表
-
- 如何绘制折线图 (52)
- javaabstract (48)
- 新浪微博头像 (53)
- grub4dos (66)
- s扫描器 (51)
- httpfile dll (48)
- ps实例教程 (55)
- taskmgr (51)
- s spline (61)
- vnc远程控制 (47)
- 数据丢失 (47)
- wbem (57)
- flac文件 (72)
- 网页制作基础教程 (53)
- 镜像文件刻录 (61)
- ug5 0软件免费下载 (78)
- debian下载 (53)
- ubuntu10 04 (60)
- web qq登录 (59)
- 笔记本变成无线路由 (52)
- flash player 11 4 (50)
- 右键菜单清理 (78)
- cuteftp 注册码 (57)
- ospf协议 (53)
- ms17 010 下载 (60)