威注音输入法技术白皮书 (v2.8.5)
威注音输入法是我最近一年在开发的面向 macOS 平台的输入法,在平均输入速度及其稳定性方面碾压小鹤等双拼输入方案。左手声母、右手韵母的大千声韵并击布局也非常易于记忆和学习。
其敲字效率演示:
该输入法可以在 Gitee 下载到:
注:本文提到的 SessionCtl 是 IMKInputController 的派生型别、在小麦注音当中被称为「InputMethodController」。此前,该型别在威注音当中曾一度被命名为「ctlInputMethod」。自威注音 2.7.5 版开始,ctlInputMethod (ctlIME) 被更名为 SessionCtl,意为「会话控制器」。引文
有人可能对威注音「自何时起从小麦注音 fork 出来」感兴趣。虽然仓库显示的时间可能比较早,但实际上(拿 1.3.0 版来讲)是 fork 自 2022 年 2 月初版本的小麦注音。1.2.x 版只能说是草稿版,自 2021 年底开始(到 2022 年 2 月中旬 1.3.0 版为止)一直在尝试手动同步来自上游的一些修改,且在此同时试图实现威注音自己的产品设计意图。
自 2022 年三月底开始,威注音尝试继续之前上游的 Swift 化计划所未完成的内容:用 Swift 将整个专案的 ObjC(++) 与 C++ 的部分用 Swift 彻底重写。先是重写了按键调度模组,之后是组字引擎「Megrez」,最后威注音又有了自己原创的功能更丰富的声韵并击引擎「(齐)铁恨 Tekkon」。此外,威注音当时尝试用 C# 语言重写这些模组,但因为 Windows「没有与 InputMethodKit 一样方便的输入法开发套装体验」而只能作罢,相关仓库荒废至今。在此之后,Megrez 引擎做过一轮技术升级。
然后就是到了八月初,威注音实现了 IMK 矩阵选字窗的支援。由于上游有人在八月底对于威注音输入法的 Shift 按键判定行为有关的不实言论,威注音专案启动了「D 计划」,在一个月内将输入法本体的所有「直接从上游继承来的 Swift 模组」全部淘汰,且用 SwiftUI 完成了威注音自家的全新的田所选字窗:这套选字窗同时实现了(类似 IMK 选字窗的)横排矩阵布局与(类似微软新注音的)纵排矩阵布局。而上游那位某人还在那里止步于一月底「有种冲动想把小麦注音里头的 UI 都换成 SwiftUI」那种冲动而已。
然而,仍旧会有诸如 BrLi 之流拼尽一己全力混淆视听。于是,就有了笔者这篇技术白皮书。
总括概述
基础大模组与处理单元
差异项小麦注音 (2.4.1)威注音 (2.8.8)补记按键调度模组Objective-C++
(KeyHandler)以 Swift 重写
(InputHandler)按键讯号承载单位KeyHandlerInputNSEvent及其私有功能拓展威注音利用 NSEvent 的 characters ignoring modifiers 参数塞入状态上下文情境描述用中继资料组字引擎Gramambular 2 (C++)
by Lukhnos LiuMegrez (Swift)
采用与 Gramambular 2 相同的顶点爬轨算法Megrez 新增了很多功能扩展,包括对就地加词功能要用到的标记游标的管理,等注音并击引擎OVMandarin (C++)
by Lukhnos LiuTekkon (Swift)
支援更多注音排列与拼音种类词库管理模组Objective-C++ & C++
ParselessLM
采 TXT 格式的原厂辞典Swift
采 Plist 格式的原厂辞典
使用者辞典采 txt 格式威注音在这方面有着记忆体占用率方面的劣势,记忆体占用率约为 150MB 以内。态械引擎多型别状态策略设计模式无专有内部资料型别单结构状态策略设计模式有专有内部资料型别威注音的设计更易于维护选字窗
差异项小麦注音 (2.4.1)威注音 (2.8.5)补记IMK 选字窗支援无有,依赖状态管理+ bridging header 强制曝露内部 API仅横排选字窗在 macOS 10.14 开始支援卷动矩阵布局输入法内建选字窗Voltaire MK2 (Swift Cocoa)Tadokoro 田所 (SwiftUI)以取代 Voltaire MK3田所选字窗仅支援 macOS 10.15 开始的系统版本内建选字窗是否有支援矩阵布局否横排矩阵(类似 IMK)
纵排矩阵(类似新注音)田所选字窗出于介面绘制效能的考量,移除了原本要加入的选字窗内容卷动支援。内建选字窗是否有专有内部资料型别否是威注音的设计更易于维护页码支援否否(Voltaire MK3 有支援过)田所选字窗另支援对当前选中的候选字的总索引编号显示针对简繁体输入模式以对应的系统介面字型显示候选字无仅限田所选字窗(需在偏好设定内的开发道场当中手动启用)田所选字窗对此要求至少 macOS 12针对简繁体输入模式以不同的高亮候选字背景色否仅限田所选字窗(Voltaire MK3 有支援过)文字输入方式
差异项小麦注音 (2.4.1)威注音 (2.8.5)补记支援注音排列种类大千传统、倚天传统、倚天26、许氏、IBM大千传统、倚天传统、IBM、神通、(伪)精业、倚天26、酷音大千 26 、许氏、星光下述排列为动态注音排列:倚天26、酷音大千 26 、许氏、星光支援拼音输入种类仅一种模式,缺乏介绍资料汉语拼音、国音二式、华罗拼音、耶鲁拼音、通用拼音拼音并击模式无有就是在敲注音的时候在组字区内显示拼音字母输入Shift+字母键可定义两种输入方式,仅允许对大写字母直接地教Shift+字母键可定义三种输入方式,其中允许对小写字母的直接递交全形数字输入尚无直接支援Alt+Shift+数字键Alt+数字键输入半形数字大键盘数字键区支援无法正确处理,且蛮横地拒绝任何针对该问题的修正 PR。直接忽略输入,或者用作选字键威注音有针对 JIS 的数字小键盘特有的按键做过针对处理Emoji 模式仅原厂 Emoji,无开关自订 Emoji 到使用者辞典内的话,会与汉字词抢频有专门的使用者 Emoji 辞典档案,有开关Alt 切换「热键专用键盘布局」有支援(需使用终端机)故意放弃支援与该功能有关的 Alt 键操作会因为 NSMenu 的按键拦截干扰、而无法保证使用体验用 Shift + BackSpace 析构游标后方的汉字的读音不支援支援用声调键复写游标后方的汉字的声调不支援支援在用选字窗选字时,先巩固操作范围上下文不支援支援类似 macOS 内建注音输入法的体验。但是,威注音只会在借由选字窗选字时巩固上下文。就地加词删词
差异项小麦注音 (2.4.1)威注音 (2.8.5)补记就地加词删词功能仅就地加词就地加词/删词/控频就地加词操作方式Shift+前后方向键选取范围,再敲 Enter 操作Shift+前后方向键选取范围,再敲 Enter 加词 / 升频、敲 BkSp / Del 就地删词、摁 Shift + Command + Enter 降频该功能与该操作方法的搭配乃微软新注音 2003 首创、且汉音从未支援过这种操作方法。对字词数与读音数不等的情况的支援无有加词删词范围运算管理仅位于态械内,以 UTF16 为范围长度单位,需 NSStringUtils。态械、组字引擎、按键调度引擎同时协作,以 UTF8 为范围长度单位,不需要 NSStringUtils。威注音的做法更精确,也只有这样才能实现对字词数与读音数不等的情况的支援资安
差异项小麦注音 (2.4.1)威注音 (2.8.5)补记Sandboxing 沙箱无有允许设定在就地加词之后自动执行指定脚本允许,且脚本位址存于 UserDefaults Plist 当中不允许一两道 Defaults write 指令就可以让小麦注音在每次加词时自动执行有害脚本。更甚者,小麦注音没有 Sandbox 设计、无法控制这种情况下的被害目录范围。内文组字区防泄密无有(需要在客体管理器内针对具体的客体 App 启用浮动组字窗)会有 App 或网站提前获取内文组字区的内容给远端伺服器、来远端预判使用者的下一步操作。读取网路资料仅在新版软体检查时才会联网,且该功能自动启用、会在安装完毕后首次执行时主动联网仅在新版软体检查时才会联网,需使用者手动启用新版软体检查之功能将本地资料上传无无威注音另有与此有关的 Sandbox 约束汉字模式支援与审音支援
差异项小麦注音 (2.4.1)威注音 (2.8.5)补记简体中文输入支援转换原生简体中文词库+通用规范汉字表支援,与原生繁体中文词库分离。繁转简也存在失真,比如「着->着」。简体中文使用者语汇支援(跑题了)简繁模式下就地加词时会做交叉转换、确保在某一个模式下加词时也会让另一个模式受益。没有别的好办法。汉字转换引擎OpenCC (支援词组转换)VXHanConvert (仅支援逐字转换)步天歌 Hotenka(支援词组转换)步天歌引擎不适合用来做大篇幅的简繁转换全字库支援有(2011 版)有(2022 版)宜每年更新一次。其他汉字支援无繁体转 JIS 汉字繁体转康熙转换准确度有限组字区与选字窗内的汉字反应当前的简繁模式或 JIS / 康熙转换模式否是审音仅台澎金马民间读音两岸审音与台澎金马常用读音均兼顾金融数字转换模式无有银行等场合会用到其他功能
差异项小麦注音 (2.4.1)威注音 (2.8.5)补记Steam 支援无有(需要在客体管理器内针对具体的客体 App 启用浮动组字窗)对其他「不认真遵守 IMKTextInput 协定」的软体也适用使用者辞典格式主动整理无,只会在就地加词时套用档案 EOF 检查修正有,会主动整理使用者辞典目录状况监测有:FSEventStream有:Dispatch Source File System Object (DSFSO)DSFSO 无法监测 App 自身对目录内的档案的修改偏好设定视窗仅 XIBXIB 给旧版系统用,SwiftUI 给新版系统用敲字读音错误提示仅系统蜂鸣生效有专门的SFX威注音的廉耻模式的开关会影响 SFXㄅ半模式支援单独的输入法模式副本输入法功能选单内切换威注音的联想词模式可自订词库自订语汇长度限制六个字十个字符号选单类似汉音的符号选单类似汉音的符号选单+类似新酷音的分层符号选单,但内容更丰富后者可借由自己撰写 symbols.dat 的方式自订使用者语汇置换及过滤有有双方功能行为一致半衰记忆模组资料的可持续利用暂无有 (JSON)威注音可在输入法选单内清空该资料工具提示视窗的纵排显示支援无有通知飘窗对于 GCD Async 的支援无(用了就会 Crash)有W3C Ruby 注音读音标记mac 版无,linux 版有;不支援教科书声调写法有,且支援教科书声调写法威注音也可输出汉语拼音的 Ruby 标记介面语言种类仅繁体中文与英文简体中文/繁体中文/日文/英文是否容忍横跨游标的候选字的出现是否无论汉音还是微软新注音都不会容忍横跨游标的候选字的出现轮替候选字仅 (Shift+)TabShift(+Alt)+Space
Alt+上下
(Shift+)Tab纵排输入时为 Alt+左右以组字区内的字词节点为单位移动游标无有:Alt+前后方向键允许自己卸除自己不允许,必须要求使用者手动操作需摁 Alt 点输入法选单,才可以看到该功能的入口使用者语汇编辑器不具备;会依赖外部编辑器内建一套语汇编辑器GraphViz 输出支援不支援(以前曾经支援过)支援(得先开启侦错模式)日期时间便捷输入不支援支援Emacs Key在按键调度模组内部处理在水源入口处理:发现 Emacs Key 就直接将原始 NSEvent 换成翻译后的 NSEvent个别详细介绍
田所选字窗
迄威注音 2.7.5 版为止,这个备选的选字窗都是自上游继承过来的 Voltaire MK2 (Swift) 选字窗修改来的。威注音在此之上做了介面美工,加上页码显示,变成 MK3 版:
迄 2.7.5 版为止的 Voltaire MK3 选字窗威注音自 2.8.0 版起,对 macOS 11 及之前的系统,仅提供 IMK 选字窗可用。本来威注音是不用再另起炉灶的。然而,IMK 选字窗在未来可能会因为改了某个内部 API 而导致新的故障出现,所以必须得有一个非 IMK 的选字窗作为备选。但如果要把 Voltaire 选字窗重写成类似 macOS 内建输入法那种卷动矩阵布局的话,只用 Cocoa 技术实现的话,技术难度太大。
幸好,SwiftUI 让事情变得简单了一些。虽然很多要用到的 SwiftUI API 要求至少 macOS 12 才可以用得上。田所选字窗的工作原理与笔者下文当中提到的 IMEState 差不多:有一个专门处理资料变化的内部型别,且与负责介面显示的型别彻底分离。这样一来,负责介面显示的型别(其实是 SwiftUI 结构)在程式维护方面就会轻松不少。
先看田所选字窗的横排矩阵布局:
田所选字窗(横排矩阵布局)威注音不满足于对横排矩阵选字窗的支援,于是又完成了纵排矩阵选字窗、提供了类似微软新注音 2003 的体验:
田所选字窗(纵排矩阵布局)一开始的田所选字窗是有做得跟 macOS 内建的选字窗几乎雷同的。但是,SwiftUI 的绘制效能并不好,使得选字窗的操作流畅度欠佳、严重影响了ㄅ半输入模式的体验。于是,田所选字窗的滑鼠滚动检视特性就被移除了、且仅显示三列矩阵。
通知飘窗
威注音输入法用到 2.7.0 为止的通知飘窗,是由小麦注音的通知飘窗修改了 NSWindow 的配色属性之后而成的。而论及威注音 2.7.5 版引入的新款通知飘窗,则拥有如下新特性:
新通知始终显示在萤幕右上角、且旧通知会自动变淡+位置下移。原因:使用了更好的飘窗通知副本管理方法。飘窗文本排版及动画动画效果受到了 Call of Duty: MWII 2022 的影响,但也兼顾了 Cocoa 应用的显示风格、使之不太出戏。对 DispatchQueue Async 的相容。之前上游的通知飘窗在 DispatchQueue Async 内呼叫时会让输入法直接崩溃掉。此外,考虑到有使用者在滥用这个功能的情况下出现的记忆体泄漏、CPU 高占用率的问题,威注音输入法在 2.8.0 SP3 当中对此做了专门的修正:最多只会残留四条近期通知。
通知飘窗的风格演进工具提示视窗
小麦注音与奇摩输入法的实现只适合横排输入时的显示。一旦纵排输入,整个文字提示视窗就还是横排显示的。威注音 2.5.0 版对该模组做了重写,引入了 NSAttributedTextView 这个可以纵排显示文字的 NSView (在 Fuziki 的同款功能的 UIView 的基础上改来,且得到他本人的准许)。但是呢,因为系统字型的 vert 特性在显示注音、汉语拼音、英文、阿拉伯数字时的效果非常糟糕,使得笔者决定做出如下处置:
在使用英文介面时,始终使用横排显示的工具提示。在使用中文或日文介面时,允许使用者在偏好设定内启用「始终使用横排显示的工具提示」。纵排工具提示视窗内的按键名称都使用符号来显示。纵排工具提示视窗内以全形空格来分隔每个汉字的读音。纵排工具提示视窗内仅显示注音、而不显示汉语拼音。另外,新版工具提示视窗在任何场合都不会遮住使用者正在输入的文字。
纵排工具提示按键讯号载体单位 & 对 EmacsKey 的支援
小麦注音的 KeyHanderInput 是把 NSEvent 读取且翻译成这么一个新的按键讯号载体单位类型。该组件在威注音当中被更名为 InputSignal,但与 InputSignalProtocol 是两码事情:前者遵循后者这个协议所规定的规范,而 KeyHandler 按键调度模组当中的函式可以处理任何符合该规范的物件。
威注音在 v2.3.1 当中淘汰了该模组,转而直接对 NSEvent 做了符合 InputSignalProtocol 的扩展。主要原因在于「IMK 选字窗只能处理 NSEvent」这个客观事实。更何况,KeyHanderInput 当中的很多实现,对目前的威注音而言并不经济。
对 NSEvent 做了这种扩展处理之后,就明显减少了 NSEvent 按键讯号流从 SessionCtl 流往 IMKCandidates 的过程当中的任何不必要的类型转换。在此之上,威注音 v2.4.0 又引入了「让 NSEvent 迅速改掉自身的其中任意一处属性」的全新重构函式(Reconstructor)、进一步减少了 SessionCtl 与 ctlCandidateIMK 的篇幅。威注音还借由 charactersIgnoringModifiers 这个参数,实现了「往 NSEvent 里面塞入诸如『当前是否是纵排输入』等资料」这种特性需求。
不过呢,威注音从业火五笔输入法承袭来的「用来判定 Shift 键是否有被敲过」的特殊按键判定函式要求传入的 NSEvent 必须是 SessionCtl.handle(event:) 吃到的 raw event 且做过 guard-let 处理、然后直接交给特殊按键判定函式。不然的话,判定会失效。这与该判定函式本身所使用的方法有关(涉及到对 NX_DEVICELSHIFTKEYMASK 等 NX Mask 的判定处理),且 NSEvent 不是 Struct 而是 Class、在 Swift 当中就容易出现这方面的闹鬼情况。
上文有提到威注音利用 NSEvent 作为基础按键讯号单位、且引入了自我重构函式的事情,那么对 EmacsKey 的判定就简单了:直接在接收讯号的第一道关口将符合条件的 Emacs Key NSEvent 重构为对应讯号的 NSEvent 即可,省去了之后的一切判断处理步骤。
InputState 有限态械系统+NSStringUtils+就地增删词
先提一下:威注音相比小麦注音而言,新增了就地删词、就地降频的功能。
进入正题。
小麦注音 2.x 版引入的有限态械,很像是参考自 Refactoring.guru 设计模式教学网站的 State Pattern 状态模式、且在 InputMethodController (小麦注音对 IMKInputController 的实作) 当中采用了与 Strategy Pattern 类似的方法来处理不同的状态。
但是,这个网站给出的方法(多型别有限态械),真的是唯一解吗?
答案显然是「否」,只是我意识到这一点恐怕有点晚了。
威注音在 v2.4.0 当中引入了全新的单结构态械系统「IMEState」,整个态械只用到一个 Struct、赋以针对不同 Struct 切换时的各种专有的建构函式(Constructor)。除去「每次切换状态都要重新初期化一个状态来取代旧状态」以外,这样的设计满像一个「管家」:手头的资料与既定事实都是固定的,但管家的行事模式与状态则是另一回事。
此外,IMEState 还有一个内部专用的资料型别 StateData,可以理解为管家身上带着的固定的钱包:里面有主人要花的钱、主人要用的各种证件,等。但是,管家在不同状态下,可能会在将具体的物件拿给主人之前,做一些别的事情:钱不够了,就去银行取款;在不同的场合,掏出不同的证件。
这种全新的单结构态械系统,比起之前使用的来自上游的多型别态械系统,更容易维护管理。特别是要在 IMK 的内文组字区的显示形态绘制运算当中使用的各种 NSAttributedString、以及标记模式下的高亮选取范围运算,都在新的 IMEState 当中得到了明显的简化处理。
更甚者,这次的组字引擎也做了升级、可以单独处理要在标记模式下使用的标记游标。这样一来,InputHandler 的任务轻松了不少,且威注音终于在原理上实现了在就地加词删词时对词音不等长的情形的正确应对处理。这些都使得小麦注音 2.x 的另一个模组「NSStringUtils」不再重要:每次在标记模式当中修改标记范围时,对新的标记范围的运算处理都是在组字引擎内产生的、然后被 InputHandler 翻译出来、变成更新过的标记状态当中的资料的一部分。小麦注音 mac 版受制于 Swift 与 C++ 的生殖隔离,仅依赖 Objective-C++ 对两者的桥接的话,很难做到这些。
威注音在就地加词删词时对词音不等长的情形的正确处理效果单结构态械系统也减轻了 ctlInputMethod 当中的状态策略管理函式:不再像以前那样给每种状态都制定一个专用的处理函式了,而是用一个函式就够了:这个函式内直接对传入的状态做 Switch Case 处理,且对「需要用到同种处理流程的一些状态」做了合并处理,减少了程式码的篇幅。
软体版本更新检查模组
上游的 VersionUpdateAPI 对威注音而言太复杂,于是笔者使用 NSURLSession 重写了全新的 UpdateSputnik 模组。这样一来,威注音也实现了「在检查新版本时,发现没有新版本」这个情形下的 NSAlert 弹窗提示。
档案目录异动检查模组
该模组用来监测任何发生在使用者辞典目录内的变化。上游的这套模组使用了 FSEventStream,但比较麻烦。威注音 v2.5.0 更换了新的 FolderMonitor 监视模组,利用了 DispatchSourceFileSystemObject。这个方法不会侦测由输入法本体对目录做出的修改,所以笔者又在 mgrLangModels 当中对「使用者手动加词」的情况又补上了手动重新载入使用者语汇的动作。这也解决了迄今为止 FSEventStreamHelper 与「威注音的使用者辞典格式自动整理模组」彼此的行为冲突问题。
$ EOF.