diff --git a/Core/Sources/ConverterServer/ConverterServer+Settings.swift b/Core/Sources/ConverterServer/ConverterServer+Settings.swift index e5229effc..ad1b0c16a 100644 --- a/Core/Sources/ConverterServer/ConverterServer+Settings.swift +++ b/Core/Sources/ConverterServer/ConverterServer+Settings.swift @@ -109,7 +109,9 @@ extension ConverterServer { .init(title: "British", value: .string(Config.KeyboardLayout.Value.british.rawValue)), .init(title: "Colemak", value: .string(Config.KeyboardLayout.Value.colemak.rawValue)), .init(title: "Dvorak", value: .string(Config.KeyboardLayout.Value.dvorak.rawValue)), - .init(title: "Dvorak - QWERTY Command", value: .string(Config.KeyboardLayout.Value.dvorakQwertyCommand.rawValue)) + .init(title: "Dvorak - QWERTY Command", value: .string(Config.KeyboardLayout.Value.dvorakQwertyCommand.rawValue)), + .init(title: "ProgrammerDvorak_JIS - QWERTY Command", value: .string(Config.KeyboardLayout.Value.programmerDvorakJIS.rawValue)), + .init(title: "ProgrammerDvorak_US - QWERTY Command", value: .string(Config.KeyboardLayout.Value.programmerDvorakUS.rawValue)) ]), value: .string(Config.KeyboardLayout().value.rawValue) ), diff --git a/Core/Sources/Core/Configs/CustomCodableConfigItem.swift b/Core/Sources/Core/Configs/CustomCodableConfigItem.swift index a9332c8bb..5c2473ce6 100644 --- a/Core/Sources/Core/Configs/CustomCodableConfigItem.swift +++ b/Core/Sources/Core/Configs/CustomCodableConfigItem.swift @@ -182,8 +182,22 @@ extension Config { case colemak case dvorak case dvorakQwertyCommand + case programmerDvorakJIS + case programmerDvorakUS public var layoutIdentifier: String { + /// 【独自の keyRemapTable を持つ配列を追加する際の注意】 + /// 1. 対象キーボードに応じた識別子を返却し、それを基準に辞書を作成すること + /// OSはここで指定された識別子に従って論理キーを生成し、azooKeyに渡します。 + /// JISキーボード用として追加する場合は「com.apple.keylayout.Romaji」を、 + /// USキーボード用として追加する場合は「com.apple.keylayout.US」を必ず返却してください。 + /// その上で、追加する配列の変換辞書(keyRemapTable)は、 + /// この戻り値によってOSから送られてくる論理文字を前提として記述する必要があります。 + /// 2. 必ずJIS用とUS用の2つの独立した case を用意し、1つに統合しないこと + /// 独自の変換マップ(keyRemapTable)を持つ配列は、記号出力をOSに委ねる標準配列とは異なり、 + /// ベースとなるJIS/USの前提が一致しないと出力が完全に破綻します。 + /// OSによるキーボードのJIS/US誤認識が発生した際、ユーザーが手動で正しい配列を + /// 選択して回避できる手段を残しておく必要があるため、内部での自動判定は避けてください。 switch self { case .qwerty: return "com.apple.keylayout.US" @@ -197,6 +211,114 @@ extension Config { return "com.apple.keylayout.Dvorak" case .dvorakQwertyCommand: return "com.apple.keylayout.DVORAK-QWERTYCMD" + case .programmerDvorakJIS: + return "com.apple.keylayout.Romaji" + case .programmerDvorakUS: + return "com.apple.keylayout.US" + } + } + + public var hasDistinctBackSlashAndYen: Bool { + /// 【独自の keyRemapTable を追加した配列専用の設定】 + /// 「¥」と「\」が両方存在する場合にtrueを返却する処理を追加すること + switch self { + case .programmerDvorakJIS: + return true + default: + return false + } + } + + public func jisZeroKeyOutput(isShiftPressed: Bool) -> String? { + /// 【独自の keyRemapTable を追加した配列専用の設定】 + /// JISに於いて「0」はShift押下有無にかかわらず常に "0" が送られる。 + /// Shiftの有無を区別して変換できないため、isShiftPressedに応じた文字返却をして下さい。 + switch self { + case .programmerDvorakJIS: + if isShiftPressed { + return "6" + } else { + return "]" + } + default: + // QWERTYなど対象外の配列は nil を返し、元の処理(default: での辞書翻訳など)へ流す + return nil + } + } + + public func jisUnderscoreKeyOutput(isShiftPressed: Bool, isOptionPressed: Bool, typeBackSlash: Bool) -> String? { + /// 【独自の keyRemapTable を追加した配列専用の設定】 + /// JISに於いて「_」はShift押下有無にかかわらず常に "_" が送られる。 + /// Shiftの有無を区別して変換できないため、isShiftPressedに応じた文字返却をして下さい。 + switch self { + case .programmerDvorakJIS: + if isShiftPressed { + return "|" + } else if isOptionPressed { + // Option押下時は標準仕様との整合性を取るため、通常時と出力を逆転させる + if typeBackSlash { + // 設定ONの時、通常時は「¥」だが、Option時は本来の「\」を出力 + return "\\" + } else { + // 設定OFFの時、通常時は「\」だが、Option時は「¥」を出力 + return "¥" + } + } else { + // 通常時 (Unshifted) + if typeBackSlash { + // 「¥」と「\」の専用キーが両方存在するため、 + // 設定がONの場合は、本来の文字「\」ではなく「¥」を出力 + return "¥" + } else { + // 設定がOFFの場合は、本来の文字「\」を出力 + return "\\" + } + } + default: + // QWERTYなど対象外の配列は nil を返し、元の処理(default: での辞書翻訳など)へ流す + return nil + } + } + + public var keyRemapTable: [Character: Character]? { + /// 【独自配列用の論理キー変換辞書】 + /// 論理キーを別の文字に変換するマッピング辞書です。 + /// OSから送られてくる論理文字をキー(Key)とし、配列特有の変換後の文字を値(Value)として定義してください。 + /// ※ JISの「_」や「0」など、OS仕様でShiftの区別ができないキーはここでは変換できません。専用関数で処理してください。 + switch self { + case .programmerDvorakJIS: + return [ + // JIS配列 Unshifted + "1":"&", "2":"[", "3":"{", "4":"}", "5":"(", "6":"=", "7":"*", "8":")", "9":"+", "0":"]", + "q":";", "w":",", "e":".", "r":"p", "t":"y", "y":"f", "u":"g", "i":"c", "o":"r", "p":"l", "@":"/", "[":"@", + "a":"a", "s":"o", "d":"e", "f":"u", "g":"i", "h":"d", "j":"h", "k":"t", "l":"n", ";":"s", ":":"-", "]":"$", + "z":"'", "x":"q", "c":"j", "v":"k", "b":"x", "n":"b", "m":"m", ",":"w", ".":"v", "/":"z", "\\":"\\", + "-":"!", "^":"#", "¥":"¥", + + // JIS配列 Shifted + "!":"%", "\"":"7", "#":"5", "$":"3", "%":"1", "&":"9", "'":"0", "(":"2", ")":"4", "=":"8", "~":"`", "|":"|", + "Q":":", "W":"<", "E":">", "R":"P", "T":"Y", "Y":"F", "U":"G", "I":"C", "O":"R", "P":"L", "`":"?", "{":"^", + "A":"A", "S":"O", "D":"E", "F":"U", "G":"I", "H":"D", "J":"H", "K":"T", "L":"N", "+":"S", "*":"_", "}":"~", + "Z":"\"", "X":"Q", "C":"J", "V":"K", "B":"X", "N":"B", "M":"M", "<":"W", ">":"V", "?":"Z" + ] + case .programmerDvorakUS: + return [ + // US配列 Unshifted + "1":"&", "2":"[", "3":"{", "4":"}", "5":"(", "6":"=", "7":"*", "8":")", "9":"+", "0":"]", + "q":";", "w":",", "e":".", "r":"p", "t":"y", "y":"f", "u":"g", "i":"c", "o":"r", "p":"l", "[":"/", "]":"@", + "a":"a", "s":"o", "d":"e", "f":"u", "g":"i", "h":"d", "j":"h", "k":"t", "l":"n", ";":"s", "'":"-", + "z":"'", "x":"q", "c":"j", "v":"k", "b":"x", "n":"b", "m":"m", ",":"w", ".":"v", "/":"z", "\\":"\\", + "-":"!", "=":"#", + + // US配列 Shifted + "!":"%", "@":"7", "#":"5", "$":"3", "%":"1", "^":"9", "&":"0", "*":"2", "(":"4", ")":"6", "_":"8", "+":"`", + "Q":":", "W":"<", "E":">", "R":"P", "T":"Y", "Y":"F", "U":"G", "I":"C", "O":"R", "P":"L", "{":"?", "}":"^", + "A":"A", "S":"O", "D":"E", "F":"U", "G":"I", "H":"D", "J":"H", "K":"T", "L":"N", ":":"S", "\"":"_", + "Z":"\"", "X":"Q", "C":"J", "V":"K", "B":"X", "N":"B", "M":"M", "<":"W", ">":"V", "?":"Z", "|":"|" + ] + default: + // QWERTYなどの標準配列 + return nil } } } diff --git a/Core/Sources/Core/InputUtils/Actions/UserAction.swift b/Core/Sources/Core/InputUtils/Actions/UserAction.swift index 58230ef21..d7c8ebe03 100644 --- a/Core/Sources/Core/InputUtils/Actions/UserAction.swift +++ b/Core/Sources/Core/InputUtils/Actions/UserAction.swift @@ -47,35 +47,68 @@ public enum UserAction { } } + private var currentLayout: Config.KeyboardLayout.Value { + return Config.KeyboardLayout().value + } + public var inputPiece: InputPiece { - switch self { - case .one: .character("1") - case .two: .character("2") - case .three: .character("3") - case .four: .character("4") - case .five: .character("5") - case .six: .character("6") - case .seven: .character("7") - case .eight: .character("8") - case .nine: .character("9") - case .zero: .character("0") - case .shiftZero: .key(intention: "0", input: "0", modifiers: [.shift]) + switch currentLayout { + case .programmerDvorakJIS, .programmerDvorakUS: + switch self { + case .one: return .character("&") + case .two: return .character("[") + case .three: return .character("{") + case .four: return .character("}") + case .five: return .character("(") + case .six: return .character("=") + case .seven: return .character("*") + case .eight: return .character(")") + case .nine: return .character("+") + case .zero: return .character("]") + case .shiftZero: return .key(intention: "6", input: "6", modifiers: [.shift]) + } + default: + switch self { + case .one: return .character("1"); case .two: return .character("2") + case .three: return .character("3"); case .four: return .character("4") + case .five: return .character("5"); case .six: return .character("6") + case .seven: return .character("7"); case .eight: return .character("8") + case .nine: return .character("9"); case .zero: return .character("0") + case .shiftZero: return .key(intention: "0", input: "0", modifiers: [.shift]) + } } } public var inputString: String { - switch self { - case .one: "1" - case .two: "2" - case .three: "3" - case .four: "4" - case .five: "5" - case .six: "6" - case .seven: "7" - case .eight: "8" - case .nine: "9" - case .zero: "0" - case .shiftZero: "0" + switch currentLayout { + case .programmerDvorakJIS, .programmerDvorakUS: + switch self { + case .one: "&" + case .two: "[" + case .three: "{" + case .four: "}" + case .five: "(" + case .six: "=" + case .seven: "*" + case .eight: ")" + case .nine: "+" + case .zero: "]" + case .shiftZero: "6" + } + default: + switch self { + case .one: "1" + case .two: "2" + case .three: "3" + case .four: "4" + case .five: "5" + case .six: "6" + case .seven: "7" + case .eight: "8" + case .nine: "9" + case .zero: "0" + case .shiftZero: "0" + } } } } @@ -104,7 +137,6 @@ public enum UserAction { return KeyMap.h2zMap(c) } } - // この種のコードは複雑にしかならないので、lintを無効にする // swiftlint:disable:next cyclomatic_complexity public static func getUserAction( @@ -114,6 +146,7 @@ public enum UserAction { ) -> UserAction { let typeBackSlash = typeBackSlash ?? Config.TypeBackSlash().value // see: https://developer.mozilla.org/ja/docs/Web/API/UI_Events/Keyboard_event_code_values#mac_%E3%81%A7%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89%E5%80%A4 + let currentLayout = Config.KeyboardLayout().value func keyMap(_ string: String, invertPunctuation: Bool = false) -> [InputPiece] { switch inputLanguage { case .english: @@ -126,10 +159,16 @@ public enum UserAction { } // Resolve action based on logical key character (ignoring modifiers) - if let logicalKey = eventCore.charactersIgnoringModifiers?.lowercased() { + if let originalLogicalKey = eventCore.charactersIgnoringModifiers?.lowercased() { + let logicalKey: String + if let firstChar = originalLogicalKey.first, let mappedChar = currentLayout.keyRemapTable?[firstChar] { + logicalKey = String(mappedChar).lowercased() + } else { + logicalKey = originalLogicalKey + } switch (logicalKey, eventCore.modifierFlags) { case (let key, [.option]) - where DiacriticAttacher.deadKeyList.contains(key) && inputLanguage == .english: + where DiacriticAttacher.deadKeyList.contains(key) && inputLanguage == .english: return .deadKey(key) case ("h", [.control]): // Control + h @@ -164,18 +203,28 @@ public enum UserAction { return .startUnicodeInput case ("¥", [.shift, .option]), ("¥", [.shift]), ("\\", [.shift, .option]), ("\\", [.shift]): + if currentLayout.keyRemapTable != nil { break } return .input(keyMap("|")) case ("¥", []), ("\\", []): - return if typeBackSlash { - .input(keyMap("\\")) + if currentLayout.hasDistinctBackSlashAndYen { + // 「¥」と「\」が両方存在する場合は、 + // typeBackSlash設定がONの時のみ「¥」「\」のキーを入れ替える + let isYen = (logicalKey == "¥") + return typeBackSlash ? .input(keyMap(isYen ? "\\" : "¥")) : .input(keyMap(isYen ? "¥" : "\\")) } else { - .input(keyMap("¥")) + // typeBackSlash設定がONの場合は「\」 OFFの場合は「¥」 + return typeBackSlash ? .input(keyMap("\\")) : .input(keyMap("¥")) } + case ("¥", [.option]), ("\\", [.option]): - return if typeBackSlash { - .input(keyMap("¥")) + if currentLayout.hasDistinctBackSlashAndYen { + // Option押下時は標準の挙動に合わせてUnshifted時と出力を逆転させる。 + // typeBackSlash設定がOFFの時のみ「¥」「\」のキーを入れ替える + let isYen = (logicalKey == "¥") + return typeBackSlash ? .input(keyMap(isYen ? "¥" : "\\")) : .input(keyMap(isYen ? "\\" : "¥")) } else { - .input(keyMap("\\")) + // typeBackSlash設定がONの場合は「¥」 OFFの場合は「\」 + return typeBackSlash ? .input(keyMap("¥")) : .input(keyMap("\\")) } case ("/", [.shift, .option]) where inputLanguage == .japanese: @@ -208,6 +257,26 @@ public enum UserAction { return .unknown } + if eventCore.keyCode == 29, let text = currentLayout.jisZeroKeyOutput(isShiftPressed:eventCore.modifierFlags.contains(.shift)) { + // 29番キー(JIS ゼロ)のオーバーライド処理 + // OS仕様(シフト有無によらず常に「0」)を回避し、対象配列のみ出力を上書きする。 + // switch内の fallthrough (case 18...29) との干渉を避け、 + // 対象外の配列を安全にdefaultへ流すためswitch手前でフックする。 + return .input(keyMap(text)) + } + + if eventCore.keyCode == 94, let text = currentLayout.jisUnderscoreKeyOutput( + isShiftPressed: eventCore.modifierFlags.contains(.shift), + isOptionPressed: eventCore.modifierFlags.contains(.option), + typeBackSlash: typeBackSlash + ) { + // 94番キー(JIS アンダースコア)のオーバーライド処理 + // OS仕様(シフト有無によらず常に「_」)を回避し、対象配列のみ出力を上書きする。 + // switch内の fallthrough (case 18...29) との干渉を避け、 + // 対象外の配列を安全にdefaultへ流すためswitch手前でフックする。 + return .input(keyMap(text)) + } + // Resolve action based on physical key code switch eventCore.keyCode { case 0x24, 0x4C: // Enter (0x24) and Numpad Enter (0x4C) @@ -287,7 +356,13 @@ public enum UserAction { } default: if let text = eventCore.characters, isPrintable(text) { - return .input(keyMap(text)) + let mappedText: String + if let remapTable = currentLayout.keyRemapTable { + mappedText = String(text.map { remapTable[$0] ?? $0 }) + } else { + mappedText = text + } + return .input(keyMap(mappedText)) } else { return .unknown }