百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

图形编辑器开发:快捷键的管理(图形编辑器怎么用)

cac55 2024-09-20 12:50 46 浏览 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 是原生事件的一层简单的封装,简化了写法并提高了可读性。

如果你的图形编辑器并不复杂,用一些易用性不错的快捷键库是不错的选择。

快捷键高级能力

原生事件和一些常见的快捷键库可以处理一些简单的场景,但图形编辑器的场景往往更复杂。

图形编辑器还需要的快捷键高级能力有:

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

快捷键管理类

考虑上面这些功能点,我们来实现这个快捷键管理类 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 的开头。

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

短路匹配逻辑

然后就是快捷键的匹配逻辑:

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

一些优化点

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

结尾

我是前端西瓜哥,欢迎关注我,学习更多图形编辑器知识。

相关推荐

如何屏蔽色情网站?_怎么能屏蔽网站

一、基础防御:全网DNS劫持阻断1.修改全网DNS服务器推荐DNS:安全DNS:CleanBrowsing(成人内容过滤):185.228.168.168/185.228.169.168Open...

容器、Pod、虚拟机与宿主机网络通信全解:看这一篇就够了

在日常开发与部署过程中,很多人一开始都会有这样的疑惑:容器之间是怎么通信的?容器怎么访问宿主机?宿主机又如何访问容器?Kubernetes中Pod的网络和Docker容器一样吗?容器跨机器是...

Win11专业版找不到共享打印机的问题

有很多深度官网的用户,都是在办公室上班的。而上班就需要使用打印机,但更新win11系统后,却出现同一个办公室里面的打印机都找不到的问题,这该如何处理呢?其实,可能是由于我们并没有打开共享打印机而造成的...

常用电脑快捷键大全,摆脱鼠标依赖,建议收藏

Ctrl+C复制Ctrl+X剪切Ctrl+V粘贴Ctrl+Z撤销Ctrl+Y重做Ctrl+B加粗Ctrl+A全选所有文件Ctrl+S保存Ctrl+N新建Ctrl+O打开Ctrl+E...

Win11实现自动追剧Jellyfin硬解,免NAS复杂操作

大家好,欢迎来到思赞数码。本期将详细介绍如何通过安装和配置Sonarr、Radarr、Prowlarr、qBittorrent和Jellyfin,打造一套自动化的影视管理系统。很多人认为,要实现自动追...

微软Win11安卓子系统WSA 2308.40000.3.0更新推送下载

IT之家9月21日消息,微软官方博客今日宣布,已面向所有WindowsInsider用户推送了Windows11安卓子系统的2308.40000.3.0版本更新。本次更新和之前...

路由器总掉线 一个命令就能猜出八九分

明明网络强度满格或有线图标正常,但视频卡成PPT、网页刷不开、游戏动不了,闲心这些问题很多小伙伴都碰到过。每次都要开关路由、宽带/光猫、插拔网线……一通忙。有没有啥办法能快速确定故障到底在哪儿,方便处...

windows电脑如何修改hosts文件?_windows怎么修改hosts

先来简单说下电脑host的作用hosts文件的作用:hosts文件是一个用于储存计算机网络中各节点信息的计算机文件;作用是将一些常用的网址域名与其对应的IP地址建立一个关联“数据库”,当用户在浏览器中...

win10广告弹窗ShellExperienceHost.exe

win10右下角老是弹出广告弹窗,排查为以下程序引起,但是这个是系统菜单的程序不能动:C:\Windows\SystemApps\ShellExperienceHost_cw5n1h2txyewy\S...

Win10 Mobile预览版10512/10166越狱解锁部署已被黑客攻破

看起来统一的WindowsPhone和Windows越加吸引人们的关注,特别是黑客们的好奇心。XDA论坛宣称,在Win10Mobile预览版10512/10166上,已取得越狱/解锁部署突破,比如可...

6款冷门小众软件,都是宝藏,建议收藏

真的很不错(。-ω-)zzzBearhttps://bear.app/cn/Bear是一个漂亮,灵活的Markdown的写作工具。它一样只支持苹果家的全平台。它一出现就惊艳四方,就被AppSto...

如何让不符合条件的设备升级Windows 11

如果你是最近(6月24日之后)加入WindowsInsider项目并且你的设备并不符合升级条件,那么当你在尝试升级Windows11的时候可能会看到以下错误:你的PC不符合Wi...

windows host文件怎么恢复?局域网访问全靠这些!

windowshost文件怎么恢复?windowshost文件是常用网址域名及其相应IP地址建立一个关联文件,通过这个host文件配置域名和IP的映射关系,以提高域名解析的速度,方便局域网用户使用...

Mac Hosts管理工具---SwitchHosts

switchhosts!formac是一款帮助用户快速切换hosts文件的工具,switchhosts!formac能够帮助你快速方便的打造个人专用的网络环境,支持本地和在线两种方式,并且支持...

「浅谈趣说网络知识」 第十二弹 老而不死的Hosts,它还很有用

【浅谈趣说网络知识】第十二弹老而不死的Hosts,它还很有用什么时候才觉得自己真的老了,不是35岁以上的数字,不是头上的点点白发,而是不知觉中的怀旧。风口上的IT界讲的就是"长江后浪推前浪...

取消回复欢迎 发表评论: