Диктовка: нативный подчёркнутый черновик
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
import AppKit
|
||||
import Carbon
|
||||
import InputMethodKit
|
||||
import OSLog
|
||||
|
||||
private let inputLog = Logger(
|
||||
subsystem: "com.romanv.inputmethod.WhisperKey",
|
||||
category: "composition"
|
||||
)
|
||||
|
||||
private enum CompositionMessage {
|
||||
static let ping = Notification.Name("com.romanv.WhisperKeyInput.ping")
|
||||
static let ready = Notification.Name("com.romanv.WhisperKeyInput.ready")
|
||||
static let draft = Notification.Name("com.romanv.WhisperKeyInput.draft")
|
||||
static let commit = Notification.Name("com.romanv.WhisperKeyInput.commit")
|
||||
static let cancel = Notification.Name("com.romanv.WhisperKeyInput.cancel")
|
||||
static let textKey = "text"
|
||||
static let activeKey = "active"
|
||||
}
|
||||
|
||||
private final class CompositionCoordinator: @unchecked Sendable {
|
||||
static let shared = CompositionCoordinator()
|
||||
|
||||
weak var activeController: WhisperKeyInputController? {
|
||||
didSet { announceReady() }
|
||||
}
|
||||
|
||||
private init() {
|
||||
let center = DistributedNotificationCenter.default()
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(handlePing),
|
||||
name: CompositionMessage.ping,
|
||||
object: nil
|
||||
)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(handleDraft),
|
||||
name: CompositionMessage.draft,
|
||||
object: nil
|
||||
)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(handleCommit),
|
||||
name: CompositionMessage.commit,
|
||||
object: nil
|
||||
)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(handleCancel),
|
||||
name: CompositionMessage.cancel,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func handlePing() {
|
||||
NSLog("WhisperKeyInput: ping")
|
||||
announceReady()
|
||||
}
|
||||
|
||||
@objc private func handleDraft(_ notification: Notification) {
|
||||
guard let text = notification.userInfo?[CompositionMessage.textKey] as? String else { return }
|
||||
NSLog("WhisperKeyInput: draft %ld", (text as NSString).length)
|
||||
activeController?.setDraft(text)
|
||||
}
|
||||
|
||||
@objc private func handleCommit(_ notification: Notification) {
|
||||
guard let text = notification.userInfo?[CompositionMessage.textKey] as? String else { return }
|
||||
NSLog("WhisperKeyInput: commit %ld", (text as NSString).length)
|
||||
activeController?.commit(text)
|
||||
}
|
||||
|
||||
@objc private func handleCancel() {
|
||||
NSLog("WhisperKeyInput: cancel")
|
||||
activeController?.cancelDraft()
|
||||
}
|
||||
|
||||
private func announceReady() {
|
||||
DistributedNotificationCenter.default().post(
|
||||
name: CompositionMessage.ready,
|
||||
object: nil,
|
||||
userInfo: [CompositionMessage.activeKey: activeController != nil]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(WhisperKeyInputController)
|
||||
final class WhisperKeyInputController: IMKInputController {
|
||||
private var draft = ""
|
||||
|
||||
override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) {
|
||||
super.init(server: server, delegate: delegate, client: inputClient)
|
||||
_ = CompositionCoordinator.shared
|
||||
NSLog("WhisperKeyInput: controller initialized")
|
||||
}
|
||||
|
||||
override func activateServer(_ sender: Any!) {
|
||||
super.activateServer(sender)
|
||||
inputLog.notice(
|
||||
"controller activated for \(self.client().bundleIdentifier(), privacy: .public)"
|
||||
)
|
||||
CompositionCoordinator.shared.activeController = self
|
||||
client().overrideKeyboard(withKeyboardNamed: "com.apple.keylayout.Russian")
|
||||
}
|
||||
|
||||
override func deactivateServer(_ sender: Any!) {
|
||||
cancelDraft()
|
||||
if CompositionCoordinator.shared.activeController === self {
|
||||
CompositionCoordinator.shared.activeController = nil
|
||||
}
|
||||
super.deactivateServer(sender)
|
||||
}
|
||||
|
||||
override func inputControllerWillClose() {
|
||||
if CompositionCoordinator.shared.activeController === self {
|
||||
CompositionCoordinator.shared.activeController = nil
|
||||
}
|
||||
super.inputControllerWillClose()
|
||||
}
|
||||
|
||||
func setDraft(_ text: String) {
|
||||
inputLog.notice(
|
||||
"setting marked text for \(self.client().bundleIdentifier(), privacy: .public)"
|
||||
)
|
||||
draft = text
|
||||
let length = (text as NSString).length
|
||||
client().setMarkedText(
|
||||
text,
|
||||
selectionRange: NSRange(location: length, length: 0),
|
||||
replacementRange: activeReplacementRange()
|
||||
)
|
||||
}
|
||||
|
||||
func commit(_ text: String) {
|
||||
inputLog.notice(
|
||||
"inserting final text for \(self.client().bundleIdentifier(), privacy: .public)"
|
||||
)
|
||||
client().insertText(text, replacementRange: activeReplacementRange())
|
||||
draft = ""
|
||||
}
|
||||
|
||||
func cancelDraft() {
|
||||
guard !draft.isEmpty else { return }
|
||||
client().insertText("", replacementRange: activeReplacementRange())
|
||||
draft = ""
|
||||
}
|
||||
|
||||
private func activeReplacementRange() -> NSRange {
|
||||
let marked = client().markedRange()
|
||||
return marked.location == NSNotFound
|
||||
? NSRange(location: NSNotFound, length: 0)
|
||||
: marked
|
||||
}
|
||||
}
|
||||
|
||||
let app = NSApplication.shared
|
||||
app.setActivationPolicy(.prohibited)
|
||||
|
||||
let connectionName =
|
||||
Bundle.main.object(forInfoDictionaryKey: "InputMethodConnectionName") as? String
|
||||
?? "com.romanv.inputmethod.WhisperKey_Connection"
|
||||
let bundleIdentifier = Bundle.main.bundleIdentifier ?? "com.romanv.inputmethod.WhisperKey"
|
||||
guard let inputServer = IMKServer(
|
||||
name: connectionName,
|
||||
bundleIdentifier: bundleIdentifier
|
||||
) else {
|
||||
fatalError("Не удалось запустить WhisperKey Input Method.")
|
||||
}
|
||||
_ = CompositionCoordinator.shared
|
||||
NSLog("WhisperKeyInput: server started")
|
||||
app.run()
|
||||
withExtendedLifetime(inputServer) {}
|
||||
Reference in New Issue
Block a user