diff --git a/apps/cmd-ime-swift/Sources/CmdIMESwift/InputSourceSwitcher.swift b/apps/cmd-ime-swift/Sources/CmdIMESwift/InputSourceSwitcher.swift index 5de80b5..8e7cf13 100644 --- a/apps/cmd-ime-swift/Sources/CmdIMESwift/InputSourceSwitcher.swift +++ b/apps/cmd-ime-swift/Sources/CmdIMESwift/InputSourceSwitcher.swift @@ -71,33 +71,52 @@ enum InputSourceSwitcher { } } - /// Finds an enabled input source whose languages include "ja", preferring - /// the standard Hiragana input mode (`com.apple.inputmethod.Japanese`) - /// when more than one Japanese source is enabled. - private static func japaneseInputSource() -> TISInputSource? { - let conditions = [kTISPropertyInputSourceIsEnabled as String: true] as CFDictionary + /// Finds an enabled keyboard input source whose languages include "ja", + /// preferring the standard Hiragana input mode + /// (`com.apple.inputmethod.Japanese`) when more than one is enabled. + /// + /// Internal (not private) so the regression test can assert the picked + /// source is a keyboard input source, never a palette. + static func japaneseInputSource() -> TISInputSource? { + // Restrict to the keyboard category: language "ja" alone also matches + // palette input sources (e.g. the 50-on Kana Palette), and selecting + // one of those opens a floating palette window instead of switching + // the IME (v2.4.7 regression). + let conditions = [ + kTISPropertyInputSourceIsEnabled as String: true, + kTISPropertyInputSourceCategory as String: kTISCategoryKeyboardInputSource as String + ] as CFDictionary guard let cfList = TISCreateInputSourceList(conditions, false)?.takeRetainedValue() else { return nil } let sources = (cfList as NSArray) as? [TISInputSource] ?? [] let japaneseSources = sources.filter { source in - guard let languages = inputSourceLanguages(source) else { return false } + guard isSelectCapable(source), let languages = inputSourceLanguages(source) else { return false } return languages.contains("ja") } - if let hiragana = japaneseSources.first(where: { inputSourceID($0) == "com.apple.inputmethod.Japanese" }) { + // Prefer the Hiragana input mode; its *mode* ID is + // "com.apple.inputmethod.Japanese" while the input source ID carries + // an IME-specific prefix (e.g. "…Kotoeri.RomajiTyping.Japanese"), so + // match on the mode ID. + if let hiragana = japaneseSources.first(where: { inputModeID($0) == "com.apple.inputmethod.Japanese" }) { return hiragana } return japaneseSources.first } - private static func inputSourceID(_ source: TISInputSource) -> String? { - guard let ptr = TISGetInputSourceProperty(source, kTISPropertyInputSourceID) else { return nil } + private static func inputModeID(_ source: TISInputSource) -> String? { + guard let ptr = TISGetInputSourceProperty(source, kTISPropertyInputModeID) else { return nil } return Unmanaged.fromOpaque(ptr).takeUnretainedValue() as String } + private static func isSelectCapable(_ source: TISInputSource) -> Bool { + guard let ptr = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsSelectCapable) else { return false } + return CFBooleanGetValue(Unmanaged.fromOpaque(ptr).takeUnretainedValue()) + } + private static func inputSourceLanguages(_ source: TISInputSource) -> [String]? { guard let ptr = TISGetInputSourceProperty(source, kTISPropertyInputSourceLanguages) else { return nil } return Unmanaged.fromOpaque(ptr).takeUnretainedValue() as NSArray as? [String] diff --git a/apps/cmd-ime-swift/Tests/CmdIMESwiftTests/InputSourceSwitcherTests.swift b/apps/cmd-ime-swift/Tests/CmdIMESwiftTests/InputSourceSwitcherTests.swift index aebb5c2..96e60ac 100644 --- a/apps/cmd-ime-swift/Tests/CmdIMESwiftTests/InputSourceSwitcherTests.swift +++ b/apps/cmd-ime-swift/Tests/CmdIMESwiftTests/InputSourceSwitcherTests.swift @@ -1,4 +1,5 @@ import XCTest +import Carbon.HIToolbox @testable import CmdIMESwift final class InputSourceSwitcherTests: XCTestCase { @@ -17,4 +18,25 @@ final class InputSourceSwitcherTests: XCTestCase { XCTAssertFalse(InputSourceSwitcher.shouldSwitchViaTIS(outputKeyCode: 103)) XCTAssertFalse(InputSourceSwitcher.shouldSwitchViaTIS(outputKeyCode: 105)) } + + /// Regression test for the v2.4.7 bug where the kana switch selected the + /// 50-on Kana Palette (a palette-category input source with language + /// "ja"), opening a floating palette window. The picked source must be a + /// keyboard input source; when a Japanese IME is enabled, it must be the + /// Hiragana input mode. + func testJapaneseInputSource_NeverPicksPalette() throws { + guard let source = InputSourceSwitcher.japaneseInputSource() else { + throw XCTSkip("No enabled Japanese keyboard input source on this machine.") + } + + XCTAssertEqual(property(source, kTISPropertyInputSourceCategory), + kTISCategoryKeyboardInputSource as String) + XCTAssertEqual(property(source, kTISPropertyInputModeID), + "com.apple.inputmethod.Japanese") + } + + private func property(_ source: TISInputSource, _ key: CFString) -> String? { + guard let ptr = TISGetInputSourceProperty(source, key) else { return nil } + return Unmanaged.fromOpaque(ptr).takeUnretainedValue() as String + } } diff --git a/manifest.toml b/manifest.toml index 0086642..a281f66 100644 --- a/manifest.toml +++ b/manifest.toml @@ -3,7 +3,7 @@ [project] id = "cmd-ime" name = "⌘IME" -version = "2.4.7" +version = "2.4.8" description = "Lightweight macOS app for switching between alphanumeric and kana input using Command keys" authors = ["Kazuki "] license = "MIT"