Conversation
ピン留めされたプロンプトにカスタムキーボードショートカットを設定できる機能を追加。 テキスト選択時にショートカットキーで直接プロンプトを実行可能に。 - KeyboardShortcut構造体とEventModifierFlags(Core module) - KeyboardShortcutConfigItem(TransformShortcut設定) - KeyboardShortcutRecorder(NSViewRepresentable) - PromptHistoryItemにIdentifiable/id/shortcutを追加(後方互換デコーダー付き) - PromptInputViewにショートカット編集UI(ShortcutEditorSheet) - InputControllerにピン留めキャッシュとショートカット検出 - メニュー項目のショートカットを設定から動的に取得 - PromptInputWindow.close()でフォーカス復帰 - CustomTextFieldからonCancelを削除(ESCはWindow側で処理) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- PromptInputWindow: showPromptInput時にpreviousAppを保存しclose()で復元 - KeyboardShortcutRecorder: typealiasを削除しCore.KeyboardShortcutを直接使用 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This pull request adds the ability to assign custom keyboard shortcuts to pinned prompts in the azooKey input method. When a user has text selected and presses a configured shortcut, the corresponding prompt is automatically applied via Magic Conversion without opening the prompt input window.
Changes:
- Introduced
KeyboardShortcutandEventModifierFlagsstructures in the Core module to represent keyboard shortcuts in a platform-independent, Codable format - Added keyboard shortcut recording UI (
KeyboardShortcutRecorder) and shortcut editor sheet to the prompt input interface - Extended
PromptHistoryItemwithidandshortcutfields, including backward-compatible decoder for existing user data - Implemented shortcut detection logic in
azooKeyMacInputControllerwith caching for performance and conflict resolution - Improved focus restoration by capturing and restoring the previous application when the prompt window closes
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| Core/Sources/Core/Configs/KeyboardShortcut.swift | Defines KeyboardShortcut and EventModifierFlags structs with Codable/Sendable conformance for cross-platform keyboard shortcut representation |
| Core/Sources/Core/Configs/KeyboardShortcutConfigItem.swift | Implements ConfigItem protocol for KeyboardShortcut, including TransformShortcut config |
| azooKeyMac/Configs/KeyboardShortcut.swift | Provides NSEvent.ModifierFlags conversion extensions for macOS integration |
| azooKeyMac/Windows/KeyboardShortcutRecorder.swift | Custom NSView-based keyboard shortcut recorder UI component |
| azooKeyMac/Windows/PromptInput/PromptHistoryItem.swift | Adds id and shortcut fields with backward-compatible decoding |
| azooKeyMac/Windows/PromptInput/PromptInputView.swift | Adds shortcut editor UI, conflict detection, and shortcut management logic |
| azooKeyMac/Windows/PromptInput/PromptInputWindow.swift | Captures and restores focus to previous application on window close |
| azooKeyMac/Windows/PromptInput/CustomTextField.swift | Removes Escape key handling (now handled at window level) |
| azooKeyMac/InputController/azooKeyMacInputController.swift | Implements shortcut detection, caching, and trigger logic for custom prompts |
| azooKeyMac/InputController/azooKeyMacInputControllerHelper.swift | Updates menu item to use dynamic shortcut from config instead of hardcoded values |
| azooKeyMac/InputController/azooKeyMacInputController+SelectedTextTransform.swift | Reloads shortcut cache when prompt window closes to sync configuration changes |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| self.applyCallback = onApply | ||
| self.completion = completion | ||
| self.initialPrompt = initialPrompt | ||
| self.previousApp = NSWorkspace.shared.frontmostApplication |
There was a problem hiding this comment.
Storing previousApp may capture the azooKey IME itself if it's the frontmost application when showPromptInput is called. This could prevent proper focus restoration to the actual text editing application. Consider checking if frontmostApplication is the current application before storing it, or use a different approach to capture the client application context.
| self.previousApp = NSWorkspace.shared.frontmostApplication | |
| let frontmostApp = NSWorkspace.shared.frontmostApplication | |
| let currentApp = NSRunningApplication.current | |
| if let frontmostApp, frontmostApp.processIdentifier != currentApp.processIdentifier { | |
| self.previousApp = frontmostApp | |
| } else { | |
| self.previousApp = nil | |
| } |
| import Foundation | ||
|
|
||
| /// キーボードショートカットを表す構造体 | ||
| public struct KeyboardShortcut: Codable, Equatable, Hashable, Sendable { | ||
| public var key: String | ||
| public var modifiers: EventModifierFlags | ||
|
|
||
| public init(key: String, modifiers: EventModifierFlags) { | ||
| self.key = key | ||
| self.modifiers = modifiers | ||
| } | ||
|
|
||
| /// デフォルトのショートカット(Control+S) | ||
| public static let defaultTransformShortcut = KeyboardShortcut( | ||
| key: "s", | ||
| modifiers: .control | ||
| ) | ||
|
|
||
| /// 表示用の文字列(例: "⌃S") | ||
| public var displayString: String { | ||
| var result = "" | ||
|
|
||
| if modifiers.contains(.control) { | ||
| result += "⌃" | ||
| } | ||
| if modifiers.contains(.option) { | ||
| result += "⌥" | ||
| } | ||
| if modifiers.contains(.shift) { | ||
| result += "⇧" | ||
| } | ||
| if modifiers.contains(.command) { | ||
| result += "⌘" | ||
| } | ||
|
|
||
| result += key.uppercased() | ||
| return result | ||
| } | ||
| } | ||
|
|
||
| /// ModifierFlagsをCodable/Sendableにするためのラッパー(rawValueベース) | ||
| public struct EventModifierFlags: Codable, Equatable, Hashable, Sendable { | ||
| public var rawValue: UInt | ||
|
|
||
| public init(rawValue: UInt) { | ||
| self.rawValue = rawValue | ||
| } | ||
|
|
||
| public static let control = EventModifierFlags(rawValue: 1 << 18) // NSEvent.ModifierFlags.control.rawValue | ||
| public static let option = EventModifierFlags(rawValue: 1 << 19) // NSEvent.ModifierFlags.option.rawValue | ||
| public static let shift = EventModifierFlags(rawValue: 1 << 17) // NSEvent.ModifierFlags.shift.rawValue | ||
| public static let command = EventModifierFlags(rawValue: 1 << 20) // NSEvent.ModifierFlags.command.rawValue | ||
|
|
||
| public func contains(_ other: EventModifierFlags) -> Bool { | ||
| (rawValue & other.rawValue) == other.rawValue | ||
| } | ||
| } |
There was a problem hiding this comment.
The new KeyboardShortcut and EventModifierFlags structs in the Core module lack test coverage. Given that the Core module has comprehensive test coverage (as seen in InputUtilsTests, KeyMapTests, WindowsTests, etc.), new functionality should include tests. Consider adding tests for: (1) KeyboardShortcut equality and hashing, (2) EventModifierFlags bitwise operations (especially the contains method), (3) displayString formatting for various modifier combinations, and (4) Codable conformance (encoding/decoding).
| // deviceIndependentFlagsMaskを適用してデバイス固有のフラグを除去し、 | ||
| // サポートするモディファイア(control, option, shift, command)のみを抽出 | ||
| let deviceIndependent = nsModifiers.intersection(.deviceIndependentFlagsMask) | ||
| let supportedMask: NSEvent.ModifierFlags = [.control, .option, .shift, .command] | ||
| self.init(rawValue: deviceIndependent.intersection(supportedMask).rawValue) |
There was a problem hiding this comment.
The EventModifierFlags init(from:) conversion may not correctly handle all NSEvent.ModifierFlags values. The deviceIndependentFlagsMask intersection is applied first, but then a second intersection with supportedMask is performed. This double intersection might inadvertently filter out valid modifier flags. Consider testing edge cases where multiple modifiers are combined, and verify that the conversion is bidirectional (nsModifierFlags property can reconstruct the original flags).
| // deviceIndependentFlagsMaskを適用してデバイス固有のフラグを除去し、 | |
| // サポートするモディファイア(control, option, shift, command)のみを抽出 | |
| let deviceIndependent = nsModifiers.intersection(.deviceIndependentFlagsMask) | |
| let supportedMask: NSEvent.ModifierFlags = [.control, .option, .shift, .command] | |
| self.init(rawValue: deviceIndependent.intersection(supportedMask).rawValue) | |
| // サポートするモディファイア(control, option, shift, command)のみを抽出 | |
| let supportedMask: NSEvent.ModifierFlags = [.control, .option, .shift, .command] | |
| let filtered = nsModifiers.intersection(supportedMask) | |
| self.init(rawValue: filtered.rawValue) |
- ショートカットマッチ時にイベントを確実に消費するよう修正 - previousAppが自アプリをキャプチャしないようガード追加 - ModifierFlags変換の冗長な二重intersectionを簡素化 - KeyboardShortcutRecorderでplaceholderプロパティを使用 - reservedShortcutsをcomputed propertyに変更 - 警告メッセージの文法修正 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…のEscapeキー処理を復元 - EventModifierFlags を削除し、既存の KeyEventCore.ModifierFlag を使用するよう統一 - KeyEventCore.ModifierFlag に Codable 準拠を追加 - PR #277 で追加された CustomTextField の onCancel/Escape キー処理を復元 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
azooKeyMac/InputController/azooKeyMacInputController+SelectedTextTransform.swift
Show resolved
Hide resolved
…集約、キャンセル時フォーカス復元を復元 - KeyboardShortcut構造体をKeyboardShortcutConfigItem.swiftに統合しファイル削除 - azooKeyMac/Configs/KeyboardShortcut.swiftを削除し、NSEvent.swiftにModifierFlag変換を集約 - PR #277のキャンセル時フォーカス復元ロジックを復元 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| // ショートカット設定を読み込み | ||
| let shortcut = Config.TransformShortcut().value | ||
| self.transformSelectedTextMenuItem = NSMenuItem( | ||
| title: TransformMenuTitle.normal, | ||
| action: #selector(self.performTransformSelectedText(_:)), | ||
| keyEquivalent: shortcut.key | ||
| ) | ||
| self.transformSelectedTextMenuItem.keyEquivalentModifierMask = shortcut.modifiers.nsModifierFlags |
There was a problem hiding this comment.
ユーザーがいい感じ変換のショートカットをカスタマイズした場合に、メニューバーの表示も連動して変わるようにするようにしたかった。現状はいい感じ変換自体(ctrl + s)自体の変更自体はまだできないので、それを将来的に入れるための変更として追加している。
There was a problem hiding this comment.
なるほど。うーん、ちょっと文脈が混ざって嫌なので一旦この差分はなしにしたいかも
|
|
||
| /// ピン留めプロンプトのキャッシュを更新 | ||
| func reloadPinnedPromptsCache() { | ||
| guard let data = UserDefaults.standard.data(forKey: "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory"), |
There was a problem hiding this comment.
ここでUserDefaultsからキーをベタ書きして読み出すのはちょっと良くない気がする。そもそもわざわざデコードをmanualで書く必要って何があるんだっけ
There was a problem hiding this comment.
StringConfigItemはString型なので、一旦manuaでPromptHistoryItemの配列としてデコードする必要があったけどConfig.PromptHistory.key でキー文字列だけ共通化すれば別にいらないなあ。
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Changes
Core.KeyboardShortcut/EventModifierFlags構造体を追加(Coreモジュール)KeyboardShortcutRecorder(NSViewRepresentable) を追加PromptHistoryItemにid/shortcutフィールドを追加(後方互換デコーダー付き)PromptInputViewにショートカット設定UI(ShortcutEditorSheet)を追加azooKeyMacInputControllerにピン留めプロンプトキャッシュとショートカット検出ロジックを追加PromptInputWindow.close()でフォーカスを元アプリに復元するように改善Config.TransformShortcutから動的に取得Test plan
cd Core && swift test- 13テスト全パス./install.sh --ignore-lint- ビルド成功