Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
57 changes: 57 additions & 0 deletions Core/Sources/Core/Configs/KeyboardShortcut.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation
Comment thread
ensan-hcl marked this conversation as resolved.
Outdated

/// キーボードショートカットを表す構造体
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 {
Comment thread
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
}
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
39 changes: 39 additions & 0 deletions Core/Sources/Core/Configs/KeyboardShortcutConfigItem.swift
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"
}
}
15 changes: 15 additions & 0 deletions azooKeyMac/Configs/KeyboardShortcut.swift
Comment thread
ensan-hcl marked this conversation as resolved.
Outdated
Comment thread
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
Expand Up @@ -163,13 +163,9 @@ extension azooKeyMacInputController {
},
completion: { [weak self] prompt in
self?.segmentsManager.appendDebugMessage("showPromptInputWindow: Window closed with prompt: \(prompt ?? "nil")")

Comment thread
ensan-hcl marked this conversation as resolved.
// Restore focus on cancel (prompt == nil) here so every closing path including window-level Esc ends up restoring focus.
if prompt == nil, let app = currentApp {
app.activate(options: [])
self?.segmentsManager.appendDebugMessage("showPromptInputWindow: Restored focus to original app on cancel")
}
self?.isPromptWindowVisible = false
// 設定変更に備えてキャッシュを更新
self?.reloadPinnedPromptsCache()
}
)
}
Expand Down
63 changes: 63 additions & 0 deletions azooKeyMac/InputController/azooKeyMacInputController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここでUserDefaultsからキーをベタ書きして読み出すのはちょっと良くない気がする。そもそもわざわざデコードをmanualで書く必要って何があるんだっけ

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

StringConfigItemはString型なので、一旦manuaでPromptHistoryItemの配列としてデコードする必要があったけどConfig.PromptHistory.key でキー文字列だけ共通化すれば別にいらないなあ。

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 }
}
Comment thread
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
}
Comment thread
ensan-hcl marked this conversation as resolved.

override init!(server: IMKServer!, delegate: Any!, client inputClient: Any!) {
let applicationDirectoryURL = if #available(macOS 13, *) {
URL.applicationSupportDirectory
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Comment thread
ensan-hcl marked this conversation as resolved.
}
}
// ショートカットがマッチした場合はイベントを消費して他のハンドラに渡さない
return true
}

let userAction = UserAction.getUserAction(eventCore: event.keyEventCore, inputLanguage: inputLanguage)

// 英数キー(keyCode 102)の処理
Expand Down
10 changes: 8 additions & 2 deletions azooKeyMac/InputController/azooKeyMacInputControllerHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これって何が発生することを期待してる変更?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

menuitemがどこから来るのかわからない

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ユーザーがいい感じ変換のショートカットをカスタマイズした場合に、メニューバーの表示も連動して変わるようにするようにしたかった。現状はいい感じ変換自体(ctrl + s)自体の変更自体はまだできないので、それを将来的に入れるための変更として追加している。

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど。うーん、ちょっと文脈が混ざって嫌なので一旦この差分はなしにしたいかも

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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())
Expand Down
Loading