当前位置:首页 > 科技  > 软件

图形编辑器开发:快捷键的管理

来源: 责编: 时间:2023-10-08 09:59:56 224观看
导读大家好,我是前端西瓜哥。快捷键操作在图形编辑器中是很高频的操作,能让用户快速高效地执行特定命令。那么今天就来学习图形编辑器是如何做快捷键的管理的。编辑器 github 地址:https://github.com/F-star/suika线上体验:h

B5H28资讯网——每日最新资讯28at.com

大家好,我是前端西瓜哥。B5H28资讯网——每日最新资讯28at.com

快捷键操作在图形编辑器中是很高频的操作,能让用户快速高效地执行特定命令。B5H28资讯网——每日最新资讯28at.com

那么今天就来学习图形编辑器是如何做快捷键的管理的。B5H28资讯网——每日最新资讯28at.com

图片B5H28资讯网——每日最新资讯28at.com

编辑器 github 地址:B5H28资讯网——每日最新资讯28at.com

https://github.com/F-star/suikaB5H28资讯网——每日最新资讯28at.com

线上体验:B5H28资讯网——每日最新资讯28at.com

https://blog.fstars.wang/app/suika/B5H28资讯网——每日最新资讯28at.com

简单的快捷键绑定

我们先看看原生的键盘事件能否满足需求。B5H28资讯网——每日最新资讯28at.com

假设我们需要判断用户是否按下了 Ctrl + C(需要精准匹配),如果按下了就执行 copy 方法。B5H28资讯网——每日最新资讯28at.com

用原生事件,我们要这样写:B5H28资讯网——每日最新资讯28at.com

window.addEventListener('keydown', (e) => {  const { ctrlKey, shiftKey, altKey, metaKey } = e;  if (ctrlKey && !shiftKey && !altKey && !metaKey && e.code === 'KeyC') {    copy();  }})

写法有点繁琐。我们希望能简化一下写法。B5H28资讯网——每日最新资讯28at.com

一开始我并不太在意快捷键绑定的管理,因为复杂度还没起来,就找了一个轮子 hotkeys-js。B5H28资讯网——每日最新资讯28at.com

import hotkeys from 'hotkeys-js';hotkeys('ctrl+c', copy);

hotkeys-js 是原生事件的一层简单的封装,简化了写法并提高了可读性。B5H28资讯网——每日最新资讯28at.com

如果你的图形编辑器并不复杂,用一些易用性不错的快捷键库是不错的选择。B5H28资讯网——每日最新资讯28at.com

快捷键高级能力

原生事件和一些常见的快捷键库可以处理一些简单的场景,但图形编辑器的场景往往更复杂。B5H28资讯网——每日最新资讯28at.com

图形编辑器还需要的快捷键高级能力有:B5H28资讯网——每日最新资讯28at.com

  • 给一个行为设置多个不同快捷键,比如 Delete 或 Backspace 都可以删除选中元素(这个大多第三方快捷键轮子是支持的);
  • 可以根据不同操作系统绑定不同的快捷键,比如复制,我希望在 Windows 系统为 Ctrl+C,在 MacOS 系统则是 Command+C。
  • 提供环境上下文,绑定的函数可以通过它决定是否被调用,比如我希望移动图形的时候不能执行 Delete 对应删除操作。
  • 支持短路匹配,只执行第一个匹配条件。这是为了防止快捷键冲突,一个快捷键执行了多个行为。当然如果你就是希望一个快捷键要执行多个行为,那可以考虑补充一个 next 方法。
  • 某个快捷键绑定可以设置为高优先级,比如激活某个工具时,要注册一些快捷键,需要高优先级,以便覆盖掉和其他的同名快捷键。

快捷键管理类

考虑上面这些功能点,我们来实现这个快捷键管理类 KeyBindingManager。B5H28资讯网——每日最新资讯28at.com

class KeyBindingManager {  // 传入一个入口类对象 Editor,之后需要用到它的变量  constructor(private editor: Editor) {}}

keyBinding 对象

一份快捷键绑定(keyBinding)由下面几个部分组成:B5H28资讯网——每日最新资讯28at.com

key,快捷键描述。理论上应该用 "Ctrl+C" 这种字符串来描述,但它实现起来比较麻烦,要解析,要转换(比如 / 要转成 Slash 去匹配 event.code)。B5H28资讯网——每日最新资讯28at.com

所以我换成了一个对象:{ CtrlKey: true, keyCode: 'KeyC' }。不用解析,不用转换,直接和 event 的属性对比即可。这个是 精准 匹配,即不能有多余的修饰键。B5H28资讯网——每日最新资讯28at.com

此外,key 也支持传入数组,这种情况比较少,对应一个行为有多个快捷键的情况。比如删除操作,我们可以传入 [{ keyCode: 'Delete' }, { keyCode: 'Backspace' }]。B5H28资讯网——每日最新资讯28at.com

winKey,快捷键描述(Windows 特供版)。这个参数是可选的,如果不提供,所有系统都会使用 key 参数。如果提供,且用户操作系统为 Windows,会使用  winKey,忽略 key。B5H28资讯网——每日最新资讯28at.com

when,是否满足上下文。也是可选的。when 是一个方法,可以通过它拿到一些上下文参数,通过这些参数决定返回的布尔值。如果为 true,表示匹配到了,并执行对应的响应行为;如果为 false,没匹配到,继续找下一个。when 可不提供,表示永远满足条件。B5H28资讯网——每日最新资讯28at.com

action,快捷键匹配后要执行的方法。B5H28资讯网——每日最新资讯28at.com

TypeScript 类型签名为:B5H28资讯网——每日最新资讯28at.com

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 数据结构,它是一种有序数据结构。B5H28资讯网——每日最新资讯28at.com

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。B5H28资讯网——每日最新资讯28at.com

事件的解绑方式有 3 种,这里选择的是类似 setTimeout 返回一个订阅 id 的风格。B5H28资讯网——每日最新资讯28at.com

《事件订阅的几种实现风格》B5H28资讯网——每日最新资讯28at.com

实际上 3 种写法都没啥差别,都是要把绑定事件方法返回的结果保存下来,在合适的时机调用解绑方法。B5H28资讯网——每日最新资讯28at.com

哦对了,还有注册高优先级快捷键的方法:B5H28资讯网——每日最新资讯28at.com

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 的开头。B5H28资讯网——每日最新资讯28at.com

如果你需要更细的粒度,比如低优先级、中优先级、高优先级,那你可以考虑传多一个优先级枚举值或一个数值,然后在正确的位置插入。感觉并没有太多需要用到这种粒度的场景。B5H28资讯网——每日最新资讯28at.com

短路匹配逻辑

然后就是快捷键的匹配逻辑:B5H28资讯网——每日最新资讯28at.com

  • 匹配顺序根据注册顺序(有特例,就是前面说的高优先级快捷键绑定,会插队,插到队伍开头)。
  • 使用精准匹配(key 或 winKey),以及 when 方法是否为 true,都为 true 时执行 action。
  • 使用短路逻辑,即只执行第一个匹配的(后面可能也有其他匹配的,但不执行)。这个其实是设计模式的责任链模式,像是 express 或 koa 的路由匹配机制也是责任链模式。

实现如下:B5H28资讯网——每日最新资讯28at.com

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) {          if (keyBinding.winKey && 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    );  }}

用法举例

类写好了,看看用法。B5H28资讯网——每日最新资讯28at.com

删除快捷键的写法:B5H28资讯网——每日最新资讯28at.com

const deleteAction = () => {  // 删除选中元素};editor.keybindingManager.register({  // Backspace 或 Delete 都可以删除  key: [{ keyCode: 'Backspace' }, { keyCode: 'Delete' }],  // 只能在没有发生拖拽的情况下下删除(比如移动图形时不能删除)  when: (ctx) => !ctx.isToolDragging,  action: deleteAction,});

复制快捷键的写法:B5H28资讯网——每日最新资讯28at.com

const copyHandler = () => {  // 复制}editor.keybindingManager.register({  key: { metaKey: true, keyCode: 'KeyC' },  // Windows 环境下的快捷键  winKey: { ctrlKey: true, keyCode: 'KeyC' },  action: copyHandler,});

一些优化点

  • 如果你考虑一些非美式键盘,比如法语键盘,因为按键布局位置发生了变化,需要做键位的重映射,确保物理位置不变,确保用户的肌肉记忆有效。
  • 简化快捷键描述的写法,使用类似 Ctrl+/ 的更简洁写法。如果你需要类似 VSCode 一样提供 JSON 文件给支持用户自己设置快捷键,这个还是要实现的。

本文链接:http://www.28at.com/showinfo-26-12425-0.html图形编辑器开发:快捷键的管理

声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com

上一篇: HTTP协议揭秘:探寻互联网的背后密码、探秘数据传输的奥秘

下一篇: 一个 println 竟然比 volatile 还好使?

标签:
  • 热门焦点
  • 红魔电竞平板评测:大屏幕硬实力

    红魔电竞平板评测:大屏幕硬实力

    前言:三年的疫情因为要上网课的原因激活了平板市场,如今网课的时代已经过去,大家的生活都恢复到了正轨,这也就意味着,真正考验平板电脑生存的环境来了。也就是面对着这种残酷的
  • 中兴AX5400Pro+上手体验:再升级 双2.5G网口+USB 3.0这次全都有

    中兴AX5400Pro+上手体验:再升级 双2.5G网口+USB 3.0这次全都有

    2021年11月的时候,中兴先后发布了两款路由器产品,中兴AX5400和中兴AX5400 Pro,从产品命名上就不难看出这是隶属于同一系列的,但在外观设计上这两款产品可以说是完全没一点关系
  • JavaScript 混淆及反混淆代码工具

    JavaScript 混淆及反混淆代码工具

    介绍在我们开始学习反混淆之前,我们首先要了解一下代码混淆。如果不了解代码是如何混淆的,我们可能无法成功对代码进行反混淆,尤其是使用自定义混淆器对其进行混淆时。什么是混
  • Raft算法:保障分布式系统共识的稳健之道

    Raft算法:保障分布式系统共识的稳健之道

    1. 什么是Raft算法?Raft 是英文”Reliable、Replicated、Redundant、And Fault-Tolerant”(“可靠、可复制、可冗余、可容错”)的首字母缩写。Raft算法是一种用于在分布式系统
  • 摸鱼心法第一章——和配置文件说拜拜

    摸鱼心法第一章——和配置文件说拜拜

    为了能摸鱼我们团队做了容器化,但是带来的问题是服务配置文件很麻烦,然后大家在群里进行了“亲切友好”的沟通图片图片图片图片对比就对比,简单对比下独立配置中心和k8s作为配
  • 十个简单但很有用的Python装饰器

    十个简单但很有用的Python装饰器

    装饰器(Decorators)是Python中一种强大而灵活的功能,用于修改或增强函数或类的行为。装饰器本质上是一个函数,它接受另一个函数或类作为参数,并返回一个新的函数或类。它们通常用
  • 一篇文章带你了解 CSS 属性选择器

    一篇文章带你了解 CSS 属性选择器

    属性选择器对带有指定属性的 HTML 元素设置样式。可以为拥有指定属性的 HTML 元素设置样式,而不仅限于 class 和 id 属性。一、了解属性选择器CSS属性选择器提供了一种简单而
  • 使用Webdriver-manager解决浏览器与驱动不匹配所带来自动化无法执行的问题

    使用Webdriver-manager解决浏览器与驱动不匹配所带来自动化无法执行的问题

    1、前言在我们使用 Selenium 进行 UI 自动化测试时,常常会因为浏览器驱动与浏览器版本不匹配,而导致自动化测试无法执行,需要手动去下载对应的驱动版本,并替换原有的驱动,可能还
  • 上海举办人工智能大会活动,建设人工智能新高地

    上海举办人工智能大会活动,建设人工智能新高地

    人工智能大会在上海浦江两岸隆重拉开帷幕,人工智能新技术、新产品、新应用、新理念集中亮相。8月30日晚,作为大会的特色活动之一的上海人工智能发展盛典人工
Top