很多副厂 mac 输入法忘记给 IMKInputController 实作那种没有参数的建构子。

361 天前
 ShikiSuen

如果切到输入法的时候、输入法正在运行、且有对当前 IMKTextInput Client 创建控制会话副本、且该副本正常的话,执行的会是 这个副本的 setValue(),传入的参数是当前输入法安装副本的 identifier (有些输入法会有多个安装副本,对应不同的输入模式,比如系统内建简体中文输入法就有全拼或双拼模式、内建繁体中文输入法会有注音或仓颉模式,等)。setValue 在被呼叫的时候,如果发现这个控制会话副本没有被正确 activateServer 的话,可能会重新 activateServer() 一遍。

如果切到输入法的时候、输入法尚未运行(或尚未对当前 IMKTextInput Client 创建控制会话副本)的话,会启动副本。但此时因为副本是直接 init() 的,没有跑 activateServer(),所以无法视为正常启动,输入法自然也就无法正常工作(输入法选单也会「啥也不显示、仅显示省略号」)、需要你重新切一下。

怎么说呢,activateServer() 实际上是 IMKInputController 的两个建构子( Constructor )之一。很多 macOS 副厂输入法开发者不知道这玩意另有这么一个不需要任何参数的建构子。

很多 mac 平台的输入法都需要一个扩充设计、来应对这个情况。我开发的威注音输入法对此的应对策略是这样的:

一、给 IMKInputController 另写一个共用的建构子:

  /// 所有建構子都會執行的共用部分,在 super.init() 之後執行。
  private func construct(client theClient: (IMKTextInput & NSObjectProtocol)? = nil) {
    DispatchQueue.main.async { [self] in
      // 威注音限定:设定 inputHandler 以及同步核心词库偏好设定。
      inputHandler = InputHandler(
        lm: LMMgr.currentLM, uom: LMMgr.currentUOM, pref: PrefMgr.shared
      )
      inputHandler?.delegate = self
      syncBaseLMPrefs() // 同步核心词库偏好设定。

      // 所有输入法共用:下述兩行很有必要,否則輸入法會在手動重啟之後無法立刻生效。
      let maybeClient = theClient ?? client()  // 注意:这样过后仍旧是 nullable IMKTextInput 类型。
      // 这里 client 是不是 nil 都无所谓,activateServer() 自己会处理:只要是 null ,就可以啥也不做(或者仅载入词库)。
      activateServer(maybeClient)

      // 威注音限定:GCD 會觸發 inputMode 的 didSet ,所以不用擔心。
      inputMode = .init(rawValue: PrefMgr.shared.mostRecentInputMode) ?? .imeModeNULL
    }
  }

二、给 IMKInputController 实作 .init() 这个无参数的建构子。

  /// 對用以設定委任物件的控制器型別進行初期化處理。
  override public init() {
    super.init()
    construct(client: client())
  }

三、让 IMKInputController 的 .init(server:delegate:client:) 这个有参数的建构子也利用 construct()。

这样区分的原因是:有参数的建构子会接收系统传入的变数;而无参数的建构子会主动读取 client()。

  /// 對用以設定委任物件的控制器型別進行初期化處理。
  ///
  /// inputClient 參數是客體應用側存在的用以藉由 IMKServer 伺服器向輸入法傳訊的物件。該物件始終遵守 IMKTextInput 協定。
  /// - Remark: 所有由委任物件實裝的「被協定要求實裝的方法」都會有一個用來接受客體物件的參數。在 IMKInputController 內部的型別不需要接受這個參數,因為已經有「 client()」這個參數存在了。
  /// - Parameters:
  ///   - server: IMKServer
  ///   - delegate: 客體物件
  ///   - inputClient: 用以接受輸入的客體應用物件
  override public init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) {
    super.init(server: server, delegate: delegate, client: inputClient)
    let theClient = inputClient as? (IMKTextInput & NSObjectProtocol)
    construct(client: theClient)
  }

思路大致如此。


补充:为什么说 activateServer 这玩意是建构子呢?因为他就是建构子。

RemObjects 的开发套装将 IMKInputController 的 API 解析成 C# 格式时,里面没有 activateServer() ,但有 public static this withServer(InputMethodKit.IMKServer server) @delegate(id @delegate) client(id inputClient); 这一行建构子。这就足以说明一切。

至于那个没有参数的建构子哪里来的?是 NSObject 就都会有这东西。

    class InputMethodKit.IMKInputController : NSObject, InputMethodKit.IIMKStateSetting, InputMethodKit.IIMKMouseHandling
    {
        private id _private;
        [NonSwiftOnly]
        public id initWithServer(InputMethodKit.IMKServer server) @delegate(id @delegate) client(id inputClient);
        [InitFromClassFactoryMethod]
        [Alias]
        [SwiftOnly]
        public static this withServer(InputMethodKit.IMKServer server) @delegate(id @delegate) client(id inputClient);
        public void updateComposition();
        public void cancelComposition();
        [NonSwiftOnly]
        public NSMutableDictionary<id,id> compositionAttributesAtRange(NSRange range);
        [Alias]
        [SwiftOnly]
        public NSMutableDictionary<id,id> compositionAttributes(NSRange range);
        public NSRange selectionRange();
        public NSRange replacementRange();
        [NonSwiftOnly]
        public NSDictionary<id,id> markForStyle(NSInteger style) atRange(NSRange range);
        [Alias]
        [SwiftOnly]
        public NSDictionary<id,id> mark(NSInteger style) at(NSRange range);
        [NonSwiftOnly]
        public void doCommandBySelector(SEL aSelector) commandDictionary(NSDictionary<id,id> infoDictionary);
        [Alias]
        [SwiftOnly]
        public void doCommand(SEL aSelector) commandDictionary(NSDictionary<id,id> infoDictionary);
        public void hidePalettes();
        public NSMenu menu();
        public InputMethodKit.IMKServer server();
        public UNKNON_CONSTRAINED_TYPE<id,IIMKTextInput,INSObject> client();
        public void inputControllerWillClose();
        public void annotationSelected(NSAttributedString annotationString) forCandidate(NSAttributedString candidateString);
        public void candidateSelectionChanged(NSAttributedString candidateString);
        public void candidateSelected(NSAttributedString candidateString);
        public id @delegate { get; set; }
    }
562 次点击
所在节点    Apple
1 条回复
ShikiSuen
361 天前
V2EX 的贴文无法更新,所以我在稀土掘金也放了一份。
https://juejin.cn/post/7236196894988369957/

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/942237

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX