Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions apps/cmd-ime-swift/Sources/CmdIMESwift/InputSourceSwitcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<CFString>.fromOpaque(ptr).takeUnretainedValue() as String
}

private static func isSelectCapable(_ source: TISInputSource) -> Bool {
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyInputSourceIsSelectCapable) else { return false }
return CFBooleanGetValue(Unmanaged<CFBoolean>.fromOpaque(ptr).takeUnretainedValue())
}

private static func inputSourceLanguages(_ source: TISInputSource) -> [String]? {
guard let ptr = TISGetInputSourceProperty(source, kTISPropertyInputSourceLanguages) else { return nil }
return Unmanaged<CFArray>.fromOpaque(ptr).takeUnretainedValue() as NSArray as? [String]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import XCTest
import Carbon.HIToolbox
@testable import CmdIMESwift

final class InputSourceSwitcherTests: XCTestCase {
Expand All @@ -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<CFString>.fromOpaque(ptr).takeUnretainedValue() as String
}
}
2 changes: 1 addition & 1 deletion manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kazuki@agiletec.jp>"]
license = "MIT"
Expand Down
Loading