Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
77 changes: 77 additions & 0 deletions Core/Sources/Core/Configs/KeyboardShortcutConfigItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Foundation

/// キーボードショートカットを表す構造体
public struct KeyboardShortcut: Codable, Equatable, Hashable, Sendable {
public var key: String
public var modifiers: KeyEventCore.ModifierFlag

public init(key: String, modifiers: KeyEventCore.ModifierFlag) {
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
}
}

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"
}
}
2 changes: 1 addition & 1 deletion Core/Sources/Core/InputUtils/KeyEventCore.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
public struct KeyEventCore: Sendable, Equatable {
public struct ModifierFlag: OptionSet, Sendable, Hashable {
public struct ModifierFlag: OptionSet, Codable, Sendable, Hashable {
public let rawValue: Int

public init(rawValue: Int) {
Expand Down
54 changes: 39 additions & 15 deletions azooKeyMac/InputController/NSEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,48 @@ import Core

extension NSEvent {
var keyEventCore: KeyEventCore {
var modifierFlags: KeyEventCore.ModifierFlag = []
if self.modifierFlags.contains(.shift) {
modifierFlags.insert(.shift)
}
if self.modifierFlags.contains(.control) {
modifierFlags.insert(.control)
}
if self.modifierFlags.contains(.command) {
modifierFlags.insert(.command)
}
if self.modifierFlags.contains(.option) {
modifierFlags.insert(.option)
}
return KeyEventCore(
modifierFlags: modifierFlags,
KeyEventCore(
modifierFlags: .init(from: self.modifierFlags),
characters: self.characters,
charactersIgnoringModifiers: self.charactersIgnoringModifiers,
keyCode: self.keyCode
)
}
}

/// NSEvent.ModifierFlagsとKeyEventCore.ModifierFlagの相互変換
extension KeyEventCore.ModifierFlag {
public init(from nsModifiers: NSEvent.ModifierFlags) {
var flags: KeyEventCore.ModifierFlag = []
if nsModifiers.contains(.control) {
flags.insert(.control)
}
if nsModifiers.contains(.option) {
flags.insert(.option)
}
if nsModifiers.contains(.shift) {
flags.insert(.shift)
}
if nsModifiers.contains(.command) {
flags.insert(.command)
}
self = flags
}

public var nsModifierFlags: NSEvent.ModifierFlags {
var flags: NSEvent.ModifierFlags = []
if contains(.control) {
flags.insert(.control)
}
if contains(.option) {
flags.insert(.option)
}
if contains(.shift) {
flags.insert(.shift)
}
if contains(.command) {
flags.insert(.command)
}
return flags
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,15 @@ extension azooKeyMacInputController {
},
completion: { [weak self] prompt in
self?.segmentsManager.appendDebugMessage("showPromptInputWindow: Window closed with prompt: \(prompt ?? "nil")")
self?.isPromptWindowVisible = false

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
58 changes: 58 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,44 @@ 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 eventModifiers = KeyEventCore.ModifierFlag(from: event.modifierFlags)

// 修飾キーがない場合は早期リターン(通常の入力)
if eventModifiers.isEmpty {
return nil
}

// キャッシュからショートカット付きのピン留めプロンプトを検索
if let matched = self.pinnedPromptsCache.first(where: { item in
guard let itemShortcut = item.shortcut else {
return false
}
return itemShortcut.key == key && itemShortcut.modifiers == eventModifiers
}) {
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 +165,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 +272,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