怎样让一款副厂 macOS 输入法使用与系统内建的拼音/注音

本文仅对 macOS 10.14 开始有效。macOS 10.13 为止的系统虽然不需要像本文一样用 bridging-header 桥接报头来强制曝露 InputMethodKit (简称 IMK) 内部的 API,但 macOS 10.13 为止的系统内建的展页阵列选字窗是不支持选字键的、也不支持根据内容长度对每一行的候选字词数量做出动态调整,所以不推荐使用。一言以蔽之:如果你想给 macOS 10.13 为止的系统使用 IMK 选字窗的话,你只有「横向单列」「纵向单列」这两个选择。

威注音输入法在启用 IMK 展页阵列选字窗之后的效果

很多人都对 macOS 10.9 Mavericks 开始的系统内建的拼音/注音输入法的展页阵列选字窗垂涎欲滴,但因为某些原因(比如系统内建的注音输入法的词库有太多的智障问题拖了十几年没解决)导致很多人不得不选择副厂输入法:

  • 五笔输入法:业火输入法、清歌输入法;
  • 注音输入法:奇摩輸入法、超注音、威注音;
  • 行列輸入法:OpenVanilla;
  • 拼音输入法:鼠须管、搜狗、微信键盘;
    • 副厂输入法当中,实现了与系统内建的拼音/注音输入法的展页阵列选字窗几乎一致的体验的,也就只有微信键盘。然而,一旦单个视窗内的候选字词数量变多,整个选字窗的操作就会有越来越严重的操作迟滞感。
  • ………

总之呢,这十年以来,因为官方的开发手册资料的缺乏,导致 IMK 选字窗几乎没有被副厂输入法所采用、但副厂输入法用户往往又都想用上这样的选字窗。本文就来讲解使用方法,对 macOS 10.14 至 macOS 14 有效。对于在此之后的系统而言,则需要另行测试可用性。

IMK 内建的选字窗与 macOS 内建的注音/拼音输入法的选字窗并非同一套,而更像是两个孪生兄弟。后者具备的一些功能,在前者当中要么有残疾、要么就是空白实作。但 IMK 团队现在被下了封口令、对与这些内容有关的提问一律对外缄默。

第一步:强制曝露相关的 API。

笔者的一个不愿具名的朋友对 macOS 10.15 的 IMK 做了逆向工程、然后笔者逐个测试可用性,才找出这四个 API。威注音输入法很早就用上了这四个 API,只是一直以来都觉得这算是野路子……直到 WWDC 2023 的 Lab 与 Apple 的工程师(不是 IMK 团队的人)对接过之后、被告知说「不妨先用着这四个 API」,笔者才敢放心公开推荐出来。

  • 然而,IMK 团队目前在忙的事情全都是不宜对外公开的事项,本人亦无知情权。
  • macOS 11 开始的系统内建模组无法被逆向工程。

先将下述内容放到 bridging header 桥接报头档案内:

@interface IMKCandidates(PROJECT_TARGET_NAME) {}

- (unsigned long long)windowLevel API_AVAILABLE(macosx(10.14));
- (void)setWindowLevel:(unsigned long long)level API_AVAILABLE(macosx(10.14));
- (BOOL)handleKeyboardEvent:(NSEvent *)event API_AVAILABLE(macosx(10.14));
- (void)setFontSize:(double)fontSize API_AVAILABLE(macosx(10.14));

@end
  • 第一个与第二个 API 是「选字窗视窗层次高度」的 { get set }
  • 第三个是用来响应按键输入的,但对 Home / End 键好像没有响应。
  • 第四个是用来设定选字窗内的文字大小尺寸的函式。虽然该 API 对 macOS 10.14 开放,但似乎仅对 macOS 10.15 开始的系统才真正有效果。

第二步:每次叫出选字窗的时候,都设定其视窗层次高度。

macOS 10.14 开始,IMK 内建的选字窗在预设的情况下都会被 Spotlight 与 NSMenu 挡住,需要在每次呼叫显示的时候都设定视窗层高。而这个视窗层高不能设得太高,否则会导致诸如英雄联盟这样的电玩在叫出输入法选字窗的时候、输入法崩溃掉。经笔者实机测试后发现 UInt64(CGShieldingWindowLevel() + x) 是个安全值(x 可以是 1 或 2)。参见下述范例:

if #available(macOS 10.14, *) {
  // Spotlight 視窗會擋住 IMK 選字窗,所以需要特殊處理。
  if let ctlCandidateCurrent = candidateUI as? CtlCandidateIMK {
    PrefMgr.shared.failureFlagForIMKCandidates = true
    ctlCandidateCurrent.setWindowLevel(UInt64(CGShieldingWindowLevel() + 2))
    PrefMgr.shared.failureFlagForIMKCandidates = false
  }
}
  1. CtlCandidateIMK 是我自行对 IMKCandidates 制作的 subclass,以符合威注音输入法内部的对选字窗的共用介面协定。
  2. failureFlagForIMKCandidates 是个保险开关:在使用这些非公开的 API 之前打开,成功执行之后再关闭。这样一来,如果执行 API 时出现了输入法崩溃的情况的话,输入法下次执行时可以在 applicationDidFinishLaunching() 的时候检查这个开关是否有开启:一旦发现该开关处于开启状态,则自动停用 IMK 选字窗,且叫出系统通知来让用户明白「IMK 选字窗崩溃了」。
  3. CGShieldingWindowLevel() 每次呼叫出来的结果数值可能都不同,一定要趁热呼叫使用、而不是事先取值重复使用。

其余的注意点

之后就可以按照 Apple 的开发说明手册资料来实作了。但有几点是手册当中没提到的、需要解释下:

1. 善用 DispatchQueue.main.async {},也就是 GCD。

比如说 candidateSelectionChanged() 里面「只要存取了 IMKCandidates 副本,就会出现记忆体位址存取冲突、崩溃掉」,但可以用 GCD 躲开这个冲突:

  /// IMK 選字窗限定函式,只要選字窗內的高亮內容選擇出現變化了、就會呼叫這個函式。
  /// - Parameter currentSelection: 已經高亮選中的候選字詞內容。
  override func candidateSelectionChanged(_ currentSelection: NSAttributedString!) {
    guard let candidateString = currentSelection?.string, !candidateString.isEmpty else { return }
    // Handle candidatePairHighlightChanged().
    var indexDeducted = 0
    fixIndexForIMKCandidates(&indexDeducted, source: candidateString)
    if state.type == .ofCandidates {
      candidatePairHighlightChanged(at: indexDeducted)
    }
    let realCandidateString = state.candidates[indexDeducted].value
    // Handle IMK Annotation... We just use this to tell Apple that this never works in IMKCandidates.
    DispatchQueue.main.async { [self] in
      let annotation = reverseLookup(for: candidateString).joined(separator: "\n")
      guard !annotation.isEmpty else { return }
      vCLog("Current Annotation: \(annotation)")
      guard let imkCandidates = candidateUI as? CtlCandidateIMK else { return }
      annotationSelected(.init(string: annotation), forCandidate: .init(string: realCandidateString))
      imkCandidates.showAnnotation(.init(string: annotation))
    }
  }

candidateSelectionChanged() 这个函式主要完成这两点任务:

  1. 更新 annotation 来显示反查结果或其他与当前候选字词有关的资讯;
  2. 趁机让输入法的内文组字区实时更新显示「选择了这个候选字之后,当前的组字区会是什么样子」,也就是「内文组字区实时预览」。
2. 一个 IMK 选字窗副本的记忆体位址在利用上可能需要注意。

IMK 选字窗的纵横排版布局设定是在 IMK 选字窗副本初期化的时候决定的,每次修改完设定之后、得重新初期化一个副本才可以。一款输入法在不重启的情况下,针对同一个记忆体指针位置初期化多次 IMK 选字窗虽然可行,但不可以针对该记忆体位址初期化另一个「持相同协定的非 IMK 选字窗」,不然你就必须得先重启输入法,否则就会在初期化的时候崩掉输入法。

笔者推测:这个设计可能是威注音输入法无法在 macOS 10.9 - 10.12 系统内顺利使用 IMK 选字窗的原因之一。

但是,哪怕你把 IMKCandidates 针对不同的 IMKInputController 会话开了不同的 IMK 选字窗副本,这些 IMK 选字窗副本会共用一个 NSWindow 来显示选字窗。这一点得特别注意:不要在 IMKInputController.deactivateServer() 的时候对选字窗的开关显示采取任何操作,除非你有黑科技可以阻止上一个 IMKInputController 会话副本在 deactivateServer() 时的乱来:前一个会话副本的 deactivateServer() 往往会在当前的新的会话副本成功 activateServer() 之后。你没准可以在新副本 activateServer() 的时候设法关掉前一个副本所开启了的选字窗。这或许可以解释为什么 IMK 预设情况下让所有的 IMKCandidates 副本共用一个 NSWindow。

仔细想了想之后,窃以为威注音输入法(v3.4.9)目前在这方面做得也不是很好,还得需要再调整一下。下图是下一个版本的威注音的调整方案:只要当前是选字窗状态,你敢擅自关一次,我就给你再开一次。

image.png

总之就先讲这么些。如果今后有必要的话,本文还会有追加更新的内容。

$ EOF.

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务