diff --git a/Core/Sources/Core/Configs/BoolConfigItem.swift b/Core/Sources/Core/Configs/BoolConfigItem.swift index 5f32629..3c0b7b2 100644 --- a/Core/Sources/Core/Configs/BoolConfigItem.swift +++ b/Core/Sources/Core/Configs/BoolConfigItem.swift @@ -56,6 +56,12 @@ extension Config { static let `default` = false public static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.typeHalfSpace" } + /// Optionキー押下時に直接全角英数を入力する設定 + public struct OptionDirectFullWidthInput: BoolConfigItem { + public init() {} + static let `default` = false + public static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.optionDirectFullWidthInput" + } /// AI変換時にコンテキストを含めるかどうか public struct IncludeContextInAITransform: BoolConfigItem { public init() {} diff --git a/Core/Sources/Core/InputUtils/OptionDirectInputResolver.swift b/Core/Sources/Core/InputUtils/OptionDirectInputResolver.swift new file mode 100644 index 0000000..0041c20 --- /dev/null +++ b/Core/Sources/Core/InputUtils/OptionDirectInputResolver.swift @@ -0,0 +1,43 @@ +import Foundation + +public enum OptionDirectInputResolver { + public static func resolve( + characters: String?, + modifierFlags: KeyEventCore.ModifierFlag, + inputLanguage: InputLanguage, + inputState: InputState, + typeBackSlash: Bool + ) -> String? { + guard inputLanguage == .japanese, inputState == .none else { + return nil + } + guard modifierFlags == [.option] || modifierFlags == [.option, .shift] else { + return nil + } + guard let characters, + !characters.isEmpty, + isPrintable(characters) + else { + return nil + } + let normalized = normalize(characters, typeBackSlash: typeBackSlash) + return normalized.applyingTransform(.fullwidthToHalfwidth, reverse: true) + } + + private static func isPrintable(_ text: String) -> Bool { + let printable: CharacterSet = [.alphanumerics, .symbols, .punctuationCharacters] + .reduce(into: CharacterSet()) { + $0.formUnion($1) + } + return CharacterSet(text.unicodeScalars).isSubset(of: printable) + } + + private static func normalize(_ text: String, typeBackSlash: Bool) -> String { + switch text { + case "¥", "\\": + typeBackSlash ? "\\" : "¥" + default: + text + } + } +} diff --git a/Core/Tests/CoreTests/InputUtilsTests/OptionDirectInputResolverTests.swift b/Core/Tests/CoreTests/InputUtilsTests/OptionDirectInputResolverTests.swift new file mode 100644 index 0000000..97243f7 --- /dev/null +++ b/Core/Tests/CoreTests/InputUtilsTests/OptionDirectInputResolverTests.swift @@ -0,0 +1,83 @@ +import Core +import Testing + +@Test func testOptionDirectInputResolverReturnsFullWidthTextForJapaneseNoneState() async throws { + let option: KeyEventCore.ModifierFlag = [.option] + let shiftOption: KeyEventCore.ModifierFlag = [.option, .shift] + + #expect(OptionDirectInputResolver.resolve( + characters: "a", + modifierFlags: option, + inputLanguage: .japanese, + inputState: .none, + typeBackSlash: false + ) == "a") + #expect(OptionDirectInputResolver.resolve( + characters: "A", + modifierFlags: shiftOption, + inputLanguage: .japanese, + inputState: .none, + typeBackSlash: false + ) == "A") + #expect(OptionDirectInputResolver.resolve( + characters: "-", + modifierFlags: option, + inputLanguage: .japanese, + inputState: .none, + typeBackSlash: false + ) == "-") + #expect(OptionDirectInputResolver.resolve( + characters: "/", + modifierFlags: shiftOption, + inputLanguage: .japanese, + inputState: .none, + typeBackSlash: false + ) == "/") + #expect(OptionDirectInputResolver.resolve( + characters: "¥", + modifierFlags: option, + inputLanguage: .japanese, + inputState: .none, + typeBackSlash: false + ) == "¥") + #expect(OptionDirectInputResolver.resolve( + characters: "¥", + modifierFlags: option, + inputLanguage: .japanese, + inputState: .none, + typeBackSlash: true + ) == "\") +} + +@Test func testOptionDirectInputResolverRejectsUnsupportedContext() async throws { + let option: KeyEventCore.ModifierFlag = [.option] + + #expect(OptionDirectInputResolver.resolve( + characters: "a", + modifierFlags: [], + inputLanguage: .japanese, + inputState: .none, + typeBackSlash: false + ) == nil) + #expect(OptionDirectInputResolver.resolve( + characters: "a", + modifierFlags: option, + inputLanguage: .english, + inputState: .none, + typeBackSlash: false + ) == nil) + #expect(OptionDirectInputResolver.resolve( + characters: "a", + modifierFlags: option, + inputLanguage: .japanese, + inputState: .composing, + typeBackSlash: false + ) == nil) + #expect(OptionDirectInputResolver.resolve( + characters: "\r", + modifierFlags: option, + inputLanguage: .japanese, + inputState: .none, + typeBackSlash: false + ) == nil) +} diff --git a/azooKeyMac/InputController/azooKeyMacInputController.swift b/azooKeyMac/InputController/azooKeyMacInputController.swift index c8cd83f..9f9bb91 100644 --- a/azooKeyMac/InputController/azooKeyMacInputController.swift +++ b/azooKeyMac/InputController/azooKeyMacInputController.swift @@ -255,14 +255,6 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s self.appMenu } - private func isPrintable(_ text: String) -> Bool { - let printable: CharacterSet = [.alphanumerics, .symbols, .punctuationCharacters] - .reduce(into: CharacterSet()) { - $0.formUnion($1) - } - return CharacterSet(text.unicodeScalars).isSubset(of: printable) - } - // swiftlint:disable:next cyclomatic_complexity @MainActor override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { guard let event, let client = sender as? IMKTextInput else { @@ -287,6 +279,20 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s return true } + let eventModifiers = KeyEventCore.ModifierFlag(from: event.modifierFlags) + let charactersForOptionDirectInput = event.characters(byApplyingModifiers: event.modifierFlags.subtracting(.option)) + if Config.OptionDirectFullWidthInput().value, + let text = OptionDirectInputResolver.resolve( + characters: charactersForOptionDirectInput, + modifierFlags: eventModifiers, + inputLanguage: inputLanguage, + inputState: inputState, + typeBackSlash: Config.TypeBackSlash().value + ) { + client.insertText(text, replacementRange: NSRange(location: NSNotFound, length: 0)) + return true + } + let userAction = UserAction.getUserAction(eventCore: event.keyEventCore, inputLanguage: inputLanguage) // 英数キー(keyCode 102)の処理 diff --git a/azooKeyMac/Windows/ConfigWindow.swift b/azooKeyMac/Windows/ConfigWindow.swift index d5546b1..d9bcd4a 100644 --- a/azooKeyMac/Windows/ConfigWindow.swift +++ b/azooKeyMac/Windows/ConfigWindow.swift @@ -8,6 +8,7 @@ struct ConfigWindow: View { @ConfigState private var typeBackSlash = Config.TypeBackSlash() @ConfigState private var punctuationStyle = Config.PunctuationStyle() @ConfigState private var typeHalfSpace = Config.TypeHalfSpace() + @ConfigState private var optionDirectFullWidthInput = Config.OptionDirectFullWidthInput() @ConfigState private var zenzaiProfile = Config.ZenzaiProfile() @ConfigState private var zenzaiPersonalizationLevel = Config.ZenzaiPersonalizationLevel() @ConfigState private var openAiApiKey = Config.OpenAiApiKey() @@ -473,6 +474,7 @@ struct ConfigWindow: View { Section { Toggle("円記号の代わりにバックスラッシュを入力", isOn: $typeBackSlash) Toggle("スペースは常に半角を入力", isOn: $typeHalfSpace) + Toggle("Optionキーで直接全角英数を入力", isOn: $optionDirectFullWidthInput) Picker("句読点の種類", selection: $punctuationStyle) { Text("、と。").tag(Config.PunctuationStyle.Value.`kutenAndToten`) Text("、と.").tag(Config.PunctuationStyle.Value.periodAndToten)