-
-
Notifications
You must be signed in to change notification settings - Fork 67
feat: ピン留めプロンプトへのキーボードショートカット割り当て #293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
68051c2
9c199b6
1cdd4b2
8b00f03
3f7ecc7
f8f6262
218e002
b7f8439
1c25800
aaa6c8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| 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 { | ||
|
ensan-hcl marked this conversation as resolved.
Outdated
|
||
| 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 | ||
| } | ||
| } | ||
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import Foundation | ||
|
|
||
| protocol KeyboardShortcutConfigItem: ConfigItem<KeyboardShortcut> { | ||
| static var `default`: KeyboardShortcut { get } | ||
| } | ||
|
|
||
| extension KeyboardShortcutConfigItem { | ||
| public var value: KeyboardShortcut { | ||
| get { | ||
| guard let data = UserDefaults.standard.data(forKey: Self.key) else { | ||
| return Self.default | ||
| } | ||
| do { | ||
| let decoded = try JSONDecoder().decode(KeyboardShortcut.self, from: data) | ||
| return decoded | ||
| } catch { | ||
| return Self.default | ||
| } | ||
| } | ||
| nonmutating set { | ||
| do { | ||
| let encoded = try JSONEncoder().encode(newValue) | ||
| UserDefaults.standard.set(encoded, forKey: Self.key) | ||
| } catch { | ||
| // エンコード失敗時は何もしない | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension Config { | ||
| /// いい感じ変換のキーボードショートカット | ||
| public struct TransformShortcut: KeyboardShortcutConfigItem { | ||
| public init() {} | ||
|
|
||
| public static let `default`: KeyboardShortcut = .defaultTransformShortcut | ||
| public static let key: String = "dev.ensan.inputmethod.azooKeyMac.preference.transform_shortcut" | ||
| } | ||
| } |
|
ensan-hcl marked this conversation as resolved.
Outdated
ensan-hcl marked this conversation as resolved.
Outdated
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import Cocoa | ||
| import Core | ||
|
|
||
| /// NSEvent.ModifierFlagsとの相互変換 | ||
| extension EventModifierFlags { | ||
| public init(from nsModifiers: NSEvent.ModifierFlags) { | ||
| // サポートするモディファイア(control, option, shift, command)のみを抽出 | ||
| let supportedMask: NSEvent.ModifierFlags = [.control, .option, .shift, .command] | ||
| self.init(rawValue: nsModifiers.intersection(supportedMask).rawValue) | ||
| } | ||
|
|
||
| public var nsModifierFlags: NSEvent.ModifierFlags { | ||
| NSEvent.ModifierFlags(rawValue: rawValue) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,9 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s | |
| private static let doubleTapInterval: TimeInterval = 0.5 | ||
| private static let candidateWindowInitialSize = CGSize(width: 400, height: 1000) | ||
|
|
||
| // ピン留めプロンプトのキャッシュ(パフォーマンス向上のため) | ||
| private var pinnedPromptsCache: [PromptHistoryItem] = [] | ||
|
|
||
| private static func makeCandidateWindow(contentViewController: NSViewController, inputClient: IMKTextInput?) -> NSWindow { | ||
| let window = NSWindow(contentViewController: contentViewController) | ||
| window.styleMask = [.borderless] | ||
|
|
@@ -58,6 +61,49 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s | |
| return isDouble | ||
| } | ||
|
|
||
| /// ピン留めプロンプトのキャッシュを更新 | ||
| func reloadPinnedPromptsCache() { | ||
| guard let data = UserDefaults.standard.data(forKey: "dev.ensan.inputmethod.azooKeyMac.preference.PromptHistory"), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ここでUserDefaultsからキーをベタ書きして読み出すのはちょっと良くない気がする。そもそもわざわざデコードをmanualで書く必要って何があるんだっけ
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. StringConfigItemはString型なので、一旦manuaでPromptHistoryItemの配列としてデコードする必要があったけどConfig.PromptHistory.key でキー文字列だけ共通化すれば別にいらないなあ。
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. じゃあキー文字列だけ共通化してほしい |
||
| let history = try? JSONDecoder().decode([PromptHistoryItem].self, from: data) else { | ||
| self.pinnedPromptsCache = [] | ||
| return | ||
| } | ||
| self.pinnedPromptsCache = history.filter { $0.isPinned } | ||
| } | ||
|
ensan-hcl marked this conversation as resolved.
|
||
|
|
||
| // MARK: - カスタムプロンプトショートカット検出 | ||
| private func checkCustomPromptShortcut(event: NSEvent) -> String? { | ||
| guard let characters = event.charactersIgnoringModifiers, | ||
| !characters.isEmpty else { | ||
| return nil | ||
| } | ||
|
|
||
| let key = characters.lowercased() | ||
|
|
||
| // 必要な修飾キーのみをマスクして取得 | ||
| let relevantModifiers: NSEvent.ModifierFlags = [.control, .option, .shift, .command] | ||
| let eventModifiers = event.modifierFlags.intersection(relevantModifiers) | ||
|
|
||
| // 修飾キーがない場合は早期リターン(通常の入力) | ||
| if eventModifiers.isEmpty { | ||
| return nil | ||
| } | ||
|
|
||
| // キャッシュからショートカット付きのピン留めプロンプトを検索 | ||
| let pinnedWithShortcuts = self.pinnedPromptsCache.filter { $0.shortcut != nil } | ||
| if let matched = pinnedWithShortcuts.first(where: { item in | ||
| guard let itemShortcut = item.shortcut else { | ||
| return false | ||
| } | ||
| let shortcutModifiers = itemShortcut.modifiers.nsModifierFlags.intersection(relevantModifiers) | ||
| return itemShortcut.key == key && eventModifiers == shortcutModifiers | ||
| }) { | ||
| return matched.prompt | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
ensan-hcl marked this conversation as resolved.
|
||
|
|
||
| override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) { | ||
| let applicationDirectoryURL = if #available(macOS 13, *) { | ||
| URL.applicationSupportDirectory | ||
|
|
@@ -124,6 +170,8 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s | |
| CustomInputTableStore.registerIfExists() | ||
| self.updateLiveConversionToggleMenuItem(newValue: self.liveConversionEnabled) | ||
| self.updateTransformSelectedTextMenuItemEnabledState() | ||
| // ピン留めプロンプトのキャッシュを更新 | ||
| self.reloadPinnedPromptsCache() | ||
| self.segmentsManager.activate() | ||
|
|
||
| if let client = sender as? IMKTextInput { | ||
|
|
@@ -229,6 +277,21 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s | |
| return false | ||
| } | ||
|
|
||
| // カスタムプロンプトショートカットのチェック | ||
| if let matchedPrompt = checkCustomPromptShortcut(event: event) { | ||
| let aiBackendEnabled = Config.AIBackendPreference().value != .off | ||
| if aiBackendEnabled && !self.isPromptWindowVisible { | ||
| let selectedRange = client.selectedRange() | ||
| if selectedRange.length > 0 { | ||
| if self.triggerAiTranslation(initialPrompt: matchedPrompt) { | ||
| return true | ||
| } | ||
|
ensan-hcl marked this conversation as resolved.
|
||
| } | ||
| } | ||
| // ショートカットがマッチした場合はイベントを消費して他のハンドラに渡さない | ||
| return true | ||
| } | ||
|
|
||
| let userAction = UserAction.getUserAction(eventCore: event.keyEventCore, inputLanguage: inputLanguage) | ||
|
|
||
| // 英数キー(keyCode 102)の処理 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,8 +9,14 @@ extension azooKeyMacInputController { | |
| self.appMenu.autoenablesItems = true | ||
| self.liveConversionToggleMenuItem = NSMenuItem(title: "ライブ変換", action: #selector(self.toggleLiveConversion(_:)), keyEquivalent: "") | ||
| self.appMenu.addItem(self.liveConversionToggleMenuItem) | ||
| self.transformSelectedTextMenuItem = NSMenuItem(title: TransformMenuTitle.normal, action: #selector(self.performTransformSelectedText(_:)), keyEquivalent: "s") | ||
| self.transformSelectedTextMenuItem.keyEquivalentModifierMask = [.control] | ||
| // ショートカット設定を読み込み | ||
| let shortcut = Config.TransformShortcut().value | ||
| self.transformSelectedTextMenuItem = NSMenuItem( | ||
| title: TransformMenuTitle.normal, | ||
| action: #selector(self.performTransformSelectedText(_:)), | ||
| keyEquivalent: shortcut.key | ||
| ) | ||
| self.transformSelectedTextMenuItem.keyEquivalentModifierMask = shortcut.modifiers.nsModifierFlags | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. これって何が発生することを期待してる変更?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. menuitemがどこから来るのかわからない
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ユーザーがいい感じ変換のショートカットをカスタマイズした場合に、メニューバーの表示も連動して変わるようにするようにしたかった。現状はいい感じ変換自体(ctrl + s)自体の変更自体はまだできないので、それを将来的に入れるための変更として追加している。
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. なるほど。うーん、ちょっと文脈が混ざって嫌なので一旦この差分はなしにしたいかも
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK |
||
| self.transformSelectedTextMenuItem.target = self | ||
| self.appMenu.addItem(self.transformSelectedTextMenuItem) | ||
| self.appMenu.addItem(NSMenuItem.separator()) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.