diff --git a/sources/SquirrelApplicationDelegate.swift b/sources/SquirrelApplicationDelegate.swift index 840b6c9ac..27f5f02ae 100644 --- a/sources/SquirrelApplicationDelegate.swift +++ b/sources/SquirrelApplicationDelegate.swift @@ -245,6 +245,7 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta notifCenter.addObserver(forName: .init("SquirrelGetASCIIModeNotification"), object: nil, queue: nil, using: rimeGetASCIIMode) notifCenter.addObserver(forName: .init(kTISNotifySelectedKeyboardInputSourceChanged as String), object: nil, queue: .main) { [weak self] _ in self?.updateStatusItemVisibility() + self?.finalizeStrandedComposition() } } @@ -357,8 +358,23 @@ private extension SquirrelApplicationDelegate { func updateStatusItemVisibility() { guard let statusItem = statusItem else { return } - let id = SquirrelInstaller.currentInputSourceID() ?? "" - statusItem.isVisible = id.hasPrefix("im.rime.inputmethod.Squirrel") + let currentInputSourceID = SquirrelInstaller.currentInputSourceID() ?? "" + statusItem.isVisible = currentInputSourceID.hasPrefix("im.rime.inputmethod.Squirrel") + } + + // macOS 26 does not call deactivateServer when the input source is switched + // away by another process via TISSelectInputSource() (e.g. macism, Input + // Source Pro): the pending composition is stranded and the candidate panel + // is left orphaned on screen (#1140). The input-source-changed notification + // is still delivered, so finalize the composition here as a fallback. + // Switching via the menu bar calls deactivateServer first, making this a + // no-op. + func finalizeStrandedComposition() { + let currentInputSourceID = SquirrelInstaller.currentInputSourceID() ?? "" + guard !currentInputSourceID.hasPrefix("im.rime.inputmethod.Squirrel") else { return } + if let inputController = panel?.inputController { + inputController.deactivateServer(inputController.client()) + } } func applyStatusIcon(asciiMode: Bool, schemaLabel: String?) { @@ -393,7 +409,7 @@ private extension SquirrelApplicationDelegate { func rimeToggleASCIIMode(_ notification: Notification) { guard let mode = notification.object as? String else { return } let enableASCII = mode == "ascii" - + if enableASCII { NotificationCenter.default.post(name: .init("SquirrelSetASCIIModeNotification"), object: true) } else { diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift index 889443e08..75b7ebb27 100644 --- a/sources/SquirrelInputController.swift +++ b/sources/SquirrelInputController.swift @@ -197,7 +197,7 @@ final class SquirrelInputController: IMKInputController { preedit = "" // Update menu bar icon if session != 0 { - let state = rimeAPI.get_option(session, "ascii_mode"); + let state = rimeAPI.get_option(session, "ascii_mode") let label = rimeAPI.get_state_label_abbreviated(session, "ascii_mode", state, true).asString NSApp.squirrelAppDelegate.updateStatusIcon(asciiMode: state, schemaLabel: label) } @@ -208,7 +208,7 @@ final class SquirrelInputController: IMKInputController { // print("[DEBUG] initWithServer: \(server ?? .init()) delegate: \(delegate ?? "nil") client:\(client ?? "nil")") super.init(server: server, delegate: delegate, client: client) createSession() - + // Listen for ASCII mode toggle notifications NotificationCenter.default.addObserver( forName: .init("SquirrelSetASCIIModeNotification"), @@ -217,7 +217,7 @@ final class SquirrelInputController: IMKInputController { ) { [weak self] notification in self?.handleASCIIModeToggle(notification) } - + // Listen for ASCII mode status requests NotificationCenter.default.addObserver( forName: .init("SquirrelReportASCIIModeNotification"), @@ -226,8 +226,6 @@ final class SquirrelInputController: IMKInputController { ) { [weak self] notification in self?.reportASCIIMode(notification) } - - } override func deactivateServer(_ sender: Any!) { @@ -637,27 +635,26 @@ private extension SquirrelInputController { private func handleASCIIModeToggle(_ notification: Notification) { guard let enableASCII = notification.object as? Bool else { return } guard session != 0 && rimeAPI.find_session(session) else { return } - + rimeAPI.set_option(session, "ascii_mode", enableASCII) - + // Force update the UI to reflect the mode change rimeUpdate() } - + private func reportASCIIMode(_: Notification) { // Only active input controller should respond guard let client = client else { return } guard session != 0 && rimeAPI.find_session(session) else { return } - + let isASCIIMode = rimeAPI.get_option(session, "ascii_mode") let status = isASCIIMode ? "ascii" : "nascii" - + // Directly respond with the status DistributedNotificationCenter.default().postNotificationName( - .init("SquirrelASCIIModeResponse"), + .init("SquirrelASCIIModeResponse"), object: status ) } - }