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

怎样让一款副厂 macOS 输入法使用与系统内建的拼音/注音输入法一样的展页阵列选字窗?

很多人都对 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)目前在这方面做得也不是很好,还得需求再调整一下。下图是下一个版本的威注音的调整方案:只需当时是选字窗状态,你敢私行关一次,我就给你再开一次。

怎样让一款副厂 macOS 输入法使用与系统内建的拼音/注音输入法一样的展页阵列选字窗?

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

$ EOF.