diff --git a/docs/LAUNCHER_ROADMAP.md b/docs/LAUNCHER_ROADMAP.md
index d60e034..6d9f87b 100644
--- a/docs/LAUNCHER_ROADMAP.md
+++ b/docs/LAUNCHER_ROADMAP.md
@@ -129,7 +129,7 @@
| # | 기능 | 설명 | 우선순위 |
|---|------|------|----------|
-| L5-1 | **항목별 전용 핫키** | 앱·URL·폴더에 `Ctrl+Alt+숫자` 등 글로벌 단축키 직접 할당. `hotkey` 프리픽스로 관리. GlobalHotkeyService에 동적 등록 | 높음 |
+| L5-1 | **항목별 전용 핫키** ✅ | 앱·URL·폴더에 `Ctrl+Alt+숫자` 등 글로벌 단축키 직접 할당. `hotkey` 프리픽스로 관리. `HotkeyAssignment` 모델 + `InputListener` 확장 + 설정창 "전용 핫키" 탭 | 높음 |
| L5-2 | **OCR 화면 텍스트 추출** | `ocr` 명령 또는 캡처에서 F4 → 화면 영역 드래그 → Windows OCR(로컬) 추출 → 클립보드 복사 / 런처 입력 | 높음 |
| L5-3 | **QuickLook 인라인 편집** | F3 미리보기에서 텍스트·마크다운 파일 직접 편집 + Ctrl+S 저장. 변경 감지(수정 표시 `●`), Esc 취소 | 중간 |
| L5-4 | **앱 세션 스냅** | 여러 앱을 지정 레이아웃으로 한번에 열기. `snap 세션이름` → 등록된 앱 목록을 각 레이아웃에 배치 | 중간 |
diff --git a/src/AxCopilot/App.Settings.cs b/src/AxCopilot/App.Settings.cs
index 652aa89..20d6757 100644
--- a/src/AxCopilot/App.Settings.cs
+++ b/src/AxCopilot/App.Settings.cs
@@ -307,6 +307,7 @@ public partial class App
_inputListener.UpdateCaptureHotkey(
_settings.Settings.ScreenCapture.GlobalHotkey,
_settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
+ _inputListener.UpdateCustomHotkeys(_settings.Settings.CustomHotkeys);
}
_worktimeReminder?.RestartTimer();
};
diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs
index 6ce6018..5bb1e58 100644
--- a/src/AxCopilot/App.xaml.cs
+++ b/src/AxCopilot/App.xaml.cs
@@ -177,6 +177,10 @@ public partial class App : System.Windows.Application
// Phase L4-1: 인라인 파일 탐색기 (Prefix=null, 경로 패턴 감지)
commandResolver.RegisterHandler(new FileBrowserHandler());
+ // ─── Phase L5 핸들러 ──────────────────────────────────────────────────
+ // Phase L5-1: 전용 핫키 목록 관리 (prefix=hotkey)
+ commandResolver.RegisterHandler(new HotkeyHandler(settings));
+
// ─── 플러그인 로드 ────────────────────────────────────────────────────
var pluginHost = new PluginHost(settings, commandResolver);
pluginHost.LoadAll();
@@ -207,6 +211,7 @@ public partial class App : System.Windows.Application
_inputListener = new InputListener();
_inputListener.HotkeyTriggered += OnHotkeyTriggered;
_inputListener.CaptureHotkeyTriggered += OnCaptureHotkeyTriggered;
+ _inputListener.CustomHotkeyTriggered += OnCustomHotkeyTriggered;
_inputListener.HookFailed += OnHookFailed;
// 설정에 저장된 핫키로 초기화 (기본: Alt+Space)
@@ -217,6 +222,9 @@ public partial class App : System.Windows.Application
settings.Settings.ScreenCapture.GlobalHotkey,
settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
+ // 항목별 전용 핫키 초기화
+ _inputListener.UpdateCustomHotkeys(settings.Settings.CustomHotkeys);
+
var snippetExpander = new SnippetExpander(settings);
_inputListener.KeyFilter = snippetExpander.HandleKey;
@@ -450,6 +458,12 @@ public partial class App : System.Windows.Application
});
}
+ private void OnCustomHotkeyTriggered(object? sender, Core.CustomHotkeyEventArgs e)
+ {
+ // 전용 핫키 발동: 런처를 열지 않고 대상을 직접 실행
+ Handlers.HotkeyHandler.ExecuteHotkeyTarget(e.Target, e.Type);
+ }
+
private void OnHookFailed(object? sender, EventArgs e)
{
Dispatcher.Invoke(() =>
diff --git a/src/AxCopilot/Core/InputListener.cs b/src/AxCopilot/Core/InputListener.cs
index 8e30caf..b2ea51f 100644
--- a/src/AxCopilot/Core/InputListener.cs
+++ b/src/AxCopilot/Core/InputListener.cs
@@ -42,8 +42,15 @@ public class InputListener : IDisposable
private HotkeyDefinition _captureHotkey;
private bool _captureHotkeyEnabled;
+ // 항목별 전용 핫키 목록
+ private readonly List<(HotkeyDefinition Def, string Target, string Type)> _customHotkeys = new();
+
public event EventHandler? HotkeyTriggered;
public event EventHandler? CaptureHotkeyTriggered;
+
+ /// 전용 핫키 발동 시 발생합니다. EventArgs에 Target과 Type이 포함됩니다.
+ public event EventHandler? CustomHotkeyTriggered;
+
public event EventHandler? HookFailed;
///
@@ -74,6 +81,25 @@ public class InputListener : IDisposable
}
}
+ ///
+ /// 항목별 전용 핫키 목록을 업데이트합니다.
+ ///
+ public void UpdateCustomHotkeys(IEnumerable assignments)
+ {
+ lock (_customHotkeys)
+ {
+ _customHotkeys.Clear();
+ foreach (var a in assignments)
+ {
+ if (HotkeyParser.TryParse(a.Hotkey, out var def))
+ _customHotkeys.Add((def, a.Target, a.Type));
+ else
+ LogService.Warn($"전용 핫키 파싱 실패: '{a.Hotkey}' (대상: {a.Target})");
+ }
+ LogService.Info($"전용 핫키 {_customHotkeys.Count}개 등록");
+ }
+ }
+
///
/// 글로벌 캡처 단축키를 설정합니다.
///
@@ -220,6 +246,33 @@ public class InputListener : IDisposable
}
}
+ // ─── 항목별 전용 핫키 감지 ───────────────────────────────────────────
+ if (!SuspendHotkey && _customHotkeys.Count > 0)
+ {
+ List<(HotkeyDefinition Def, string Target, string Type)> snapshot;
+ lock (_customHotkeys) { snapshot = new List<(HotkeyDefinition, string, string)>(_customHotkeys); }
+
+ foreach (var (def, target, type) in snapshot)
+ {
+ if (vkCode != def.VkCode) continue;
+
+ bool ctrlOk = !def.Ctrl || (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
+ bool altOk = !def.Alt || (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
+ bool shiftOk = !def.Shift || (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
+ bool winOk = !def.Win || (GetAsyncKeyState(VK_LWIN) & 0x8000) != 0
+ || (GetAsyncKeyState(VK_RWIN) & 0x8000) != 0;
+
+ if (ctrlOk && altOk && shiftOk && winOk)
+ {
+ CustomHotkeyTriggered?.Invoke(this, new CustomHotkeyEventArgs(target, type));
+ _suppressNextKeyUp = true;
+ _suppressKeyUpVk = vkCode;
+ if (def.Alt) _suppressNextAltUp = true;
+ return (IntPtr)1;
+ }
+ }
+ }
+
// ─── 스니펫 키 필터 ─────────────────────────────────────────────────
if (KeyFilter?.Invoke(vkCode) == true)
return (IntPtr)1;
@@ -263,3 +316,16 @@ public class InputListener : IDisposable
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
}
+
+/// 항목별 전용 핫키 발동 이벤트 인수.
+public class CustomHotkeyEventArgs : EventArgs
+{
+ public string Target { get; }
+ public string Type { get; }
+
+ public CustomHotkeyEventArgs(string target, string type)
+ {
+ Target = target;
+ Type = type;
+ }
+}
diff --git a/src/AxCopilot/Handlers/HotkeyHandler.cs b/src/AxCopilot/Handlers/HotkeyHandler.cs
new file mode 100644
index 0000000..62a34bd
--- /dev/null
+++ b/src/AxCopilot/Handlers/HotkeyHandler.cs
@@ -0,0 +1,155 @@
+using AxCopilot.Core;
+using AxCopilot.SDK;
+using AxCopilot.Services;
+using AxCopilot.Themes;
+
+namespace AxCopilot.Handlers;
+
+///
+/// L5-1: 전용 핫키 관리 핸들러.
+/// 예: hotkey → 등록된 전용 핫키 목록 표시
+/// hotkey 1doc → 라벨 또는 대상에 "1doc" 포함 항목 필터
+/// Enter 시 해당 핫키 항목의 대상을 실행합니다.
+///
+public class HotkeyHandler : IActionHandler
+{
+ private readonly SettingsService? _settings;
+
+ public HotkeyHandler(SettingsService? settings = null)
+ {
+ _settings = settings;
+ }
+
+ public string? Prefix => "hotkey";
+
+ public PluginMetadata Metadata => new(
+ "HotkeyManager",
+ "전용 핫키 목록 관리",
+ "1.0",
+ "AX");
+
+ public Task> GetItemsAsync(string query, CancellationToken ct)
+ {
+ var items = new List();
+ var hotkeys = _settings?.Settings.CustomHotkeys ?? new List();
+
+ if (hotkeys.Count == 0)
+ {
+ items.Add(new LauncherItem(
+ "등록된 전용 핫키 없음",
+ "설정 → 전용 핫키 탭에서 항목별 글로벌 단축키를 등록하세요",
+ null,
+ "__open_settings__",
+ Symbol: "\uE713"));
+ return Task.FromResult>(items);
+ }
+
+ var filter = query.Trim().ToLowerInvariant();
+
+ foreach (var h in hotkeys)
+ {
+ if (!string.IsNullOrEmpty(filter) &&
+ !h.Label.Contains(filter, StringComparison.OrdinalIgnoreCase) &&
+ !h.Hotkey.Contains(filter, StringComparison.OrdinalIgnoreCase) &&
+ !h.Target.Contains(filter, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ var typeSymbol = h.Type switch
+ {
+ "url" => Symbols.Globe,
+ "folder" => Symbols.Folder,
+ "command" => "\uE756",
+ _ => Symbols.App
+ };
+
+ var label = string.IsNullOrWhiteSpace(h.Label)
+ ? System.IO.Path.GetFileNameWithoutExtension(h.Target)
+ : h.Label;
+
+ items.Add(new LauncherItem(
+ $"[{h.Hotkey}] {label}",
+ h.Target,
+ null,
+ h,
+ Symbol: typeSymbol));
+ }
+
+ // 설정 단축키 안내 항목
+ items.Add(new LauncherItem(
+ "전용 핫키 설정 열기",
+ "설정 → 전용 핫키 탭에서 핫키를 추가하거나 제거합니다",
+ null,
+ "__open_settings__",
+ Symbol: "\uE713"));
+
+ return Task.FromResult>(items);
+ }
+
+ public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
+ {
+ if (item.Data is string s && s == "__open_settings__")
+ {
+ System.Windows.Application.Current.Dispatcher.Invoke(() =>
+ {
+ var app = System.Windows.Application.Current as AxCopilot.App;
+ app?.OpenSettingsFromChat();
+ });
+ return Task.CompletedTask;
+ }
+
+ if (item.Data is Models.HotkeyAssignment ha)
+ {
+ ExecuteHotkeyTarget(ha.Target, ha.Type);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ /// 전용 핫키 대상을 타입에 따라 실행합니다.
+ internal static void ExecuteHotkeyTarget(string target, string type)
+ {
+ try
+ {
+ switch (type)
+ {
+ case "url":
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = target,
+ UseShellExecute = true
+ });
+ break;
+
+ case "folder":
+ System.Diagnostics.Process.Start("explorer.exe", target);
+ break;
+
+ case "command":
+ var parts = target.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
+ var cmdFile = parts[0];
+ var cmdArgs = parts.Length > 1 ? parts[1] : "";
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = cmdFile,
+ Arguments = cmdArgs,
+ UseShellExecute = true
+ });
+ break;
+
+ default: // app
+ System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = target,
+ UseShellExecute = true
+ });
+ break;
+ }
+
+ Services.LogService.Info($"전용 핫키 실행: {target} ({type})");
+ }
+ catch (Exception ex)
+ {
+ Services.LogService.Error($"전용 핫키 실행 오류: {target} — {ex.Message}");
+ }
+ }
+}
diff --git a/src/AxCopilot/Models/AppSettings.Models.cs b/src/AxCopilot/Models/AppSettings.Models.cs
index 71e204f..657e51b 100644
--- a/src/AxCopilot/Models/AppSettings.Models.cs
+++ b/src/AxCopilot/Models/AppSettings.Models.cs
@@ -240,6 +240,28 @@ public class ScreenCaptureSettings
public int ScrollDelayMs { get; set; } = 120;
}
+// ─── 전용 핫키 ────────────────────────────────────────────────────────────────
+
+///
+/// 항목별 글로벌 전용 핫키 할당.
+/// Hotkey: "Ctrl+Alt+1" 형식, Target: 실행 대상 경로/URL/명령어, Label: 표시 이름.
+///
+public class HotkeyAssignment
+{
+ [JsonPropertyName("hotkey")]
+ public string Hotkey { get; set; } = "";
+
+ [JsonPropertyName("target")]
+ public string Target { get; set; } = "";
+
+ [JsonPropertyName("label")]
+ public string Label { get; set; } = "";
+
+ /// 실행 타입. app | url | folder | command. 기본 app.
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = "app";
+}
+
// ─── 잠금 해제 알림 설정 ───────────────────────────────────────────────────────
public class ReminderSettings
diff --git a/src/AxCopilot/Models/AppSettings.cs b/src/AxCopilot/Models/AppSettings.cs
index 1b4d53a..4fc54f2 100644
--- a/src/AxCopilot/Models/AppSettings.cs
+++ b/src/AxCopilot/Models/AppSettings.cs
@@ -106,6 +106,12 @@ public class AppSettings
[JsonPropertyName("reminder")]
public ReminderSettings Reminder { get; set; } = new();
+ ///
+ /// 항목별 글로벌 전용 핫키 목록. 어떤 앱이 포커스를 가져도 해당 핫키로 즉시 실행됩니다.
+ ///
+ [JsonPropertyName("customHotkeys")]
+ public List CustomHotkeys { get; set; } = new();
+
[JsonPropertyName("llm")]
public LlmSettings Llm { get; set; } = new();
}
diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.Methods.cs b/src/AxCopilot/ViewModels/SettingsViewModel.Methods.cs
index 6f6cab2..a72d151 100644
--- a/src/AxCopilot/ViewModels/SettingsViewModel.Methods.cs
+++ b/src/AxCopilot/ViewModels/SettingsViewModel.Methods.cs
@@ -342,6 +342,15 @@ public partial class SettingsViewModel
}))
.ToList();
+ // 전용 핫키 저장
+ s.CustomHotkeys = CustomHotkeys.Select(h => new Models.HotkeyAssignment
+ {
+ Hotkey = h.Hotkey,
+ Target = h.Target,
+ Label = h.Label,
+ Type = h.Type
+ }).ToList();
+
// 스니펫 저장
s.Snippets = Snippets.Select(sn => new SnippetEntry
{
@@ -416,6 +425,50 @@ public partial class SettingsViewModel
public void RemoveBatchCommand(BatchCommandModel cmd) => BatchCommands.Remove(cmd);
+ // ─── 전용 핫키 메서드 ─────────────────────────────────────────────────────
+
+ public bool AddCustomHotkey()
+ {
+ if (string.IsNullOrWhiteSpace(_newHotkeyStr) || string.IsNullOrWhiteSpace(_newHotkeyTarget))
+ return false;
+
+ var hotkey = _newHotkeyStr.Trim();
+
+ // 핫키 형식 검증
+ if (!Core.HotkeyParser.TryParse(hotkey, out _))
+ {
+ CustomMessageBox.Show(
+ $"핫키 형식이 올바르지 않습니다: '{hotkey}'\n예) Ctrl+Alt+1, Ctrl+Shift+F2",
+ "AX Copilot — 핫키 오류",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Warning);
+ return false;
+ }
+
+ // 중복 검사
+ if (CustomHotkeys.Any(h => h.Hotkey.Equals(hotkey, StringComparison.OrdinalIgnoreCase)))
+ {
+ CustomMessageBox.Show(
+ $"이미 등록된 핫키입니다: '{hotkey}'",
+ "AX Copilot — 중복 핫키",
+ System.Windows.MessageBoxButton.OK,
+ System.Windows.MessageBoxImage.Warning);
+ return false;
+ }
+
+ CustomHotkeys.Add(new HotkeyRowModel
+ {
+ Hotkey = hotkey,
+ Target = _newHotkeyTarget.Trim(),
+ Label = string.IsNullOrWhiteSpace(_newHotkeyLabel) ? System.IO.Path.GetFileNameWithoutExtension(_newHotkeyTarget.Trim()) : _newHotkeyLabel.Trim(),
+ Type = _newHotkeyType
+ });
+ NewHotkeyStr = ""; NewHotkeyTarget = ""; NewHotkeyLabel = ""; NewHotkeyType = "app";
+ return true;
+ }
+
+ public void RemoveCustomHotkey(HotkeyRowModel row) => CustomHotkeys.Remove(row);
+
// ─── 스니펫 메서드 ──────────────────────────────────────────────────────
public bool AddSnippet()
{
diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.Properties.cs b/src/AxCopilot/ViewModels/SettingsViewModel.Properties.cs
index b1e5066..e22835d 100644
--- a/src/AxCopilot/ViewModels/SettingsViewModel.Properties.cs
+++ b/src/AxCopilot/ViewModels/SettingsViewModel.Properties.cs
@@ -290,6 +290,19 @@ public partial class SettingsViewModel
// ─── 인덱스 확장자 ──────────────────────────────────────────────────
public ObservableCollection IndexExtensions { get; } = new();
+ // ─── 전용 핫키 설정 ─────────────────────────────────────────────────────────
+ public ObservableCollection CustomHotkeys { get; } = new();
+
+ private string _newHotkeyStr = "";
+ private string _newHotkeyTarget = "";
+ private string _newHotkeyLabel = "";
+ private string _newHotkeyType = "app";
+
+ public string NewHotkeyStr { get => _newHotkeyStr; set { _newHotkeyStr = value; OnPropertyChanged(); } }
+ public string NewHotkeyTarget { get => _newHotkeyTarget; set { _newHotkeyTarget = value; OnPropertyChanged(); } }
+ public string NewHotkeyLabel { get => _newHotkeyLabel; set { _newHotkeyLabel = value; OnPropertyChanged(); } }
+ public string NewHotkeyType { get => _newHotkeyType; set { _newHotkeyType = value; OnPropertyChanged(); } }
+
// ─── 스니펫 설정 ──────────────────────────────────────────────────────────
public ObservableCollection Snippets { get; } = new();
diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.cs b/src/AxCopilot/ViewModels/SettingsViewModel.cs
index 9b19b52..7ff6eb3 100644
--- a/src/AxCopilot/ViewModels/SettingsViewModel.cs
+++ b/src/AxCopilot/ViewModels/SettingsViewModel.cs
@@ -268,6 +268,10 @@ public partial class SettingsViewModel : INotifyPropertyChanged
foreach (var ext in s.IndexExtensions)
IndexExtensions.Add(ext);
+ // 전용 핫키 로드
+ foreach (var h in s.CustomHotkeys)
+ CustomHotkeys.Add(new HotkeyRowModel { Hotkey = h.Hotkey, Target = h.Target, Label = h.Label, Type = h.Type });
+
// 스니펫 로드
foreach (var sn in s.Snippets)
Snippets.Add(new SnippetRowModel { Key = sn.Key, Name = sn.Name, Content = sn.Content });
diff --git a/src/AxCopilot/ViewModels/SettingsViewModelModels.cs b/src/AxCopilot/ViewModels/SettingsViewModelModels.cs
index 97a7834..7a6ce60 100644
--- a/src/AxCopilot/ViewModels/SettingsViewModelModels.cs
+++ b/src/AxCopilot/ViewModels/SettingsViewModelModels.cs
@@ -243,6 +243,33 @@ public class RegisteredModelRow : INotifyPropertyChanged
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
}
+// ─── 전용 핫키 행 모델 ────────────────────────────────────────────────────────────
+
+public class HotkeyRowModel : INotifyPropertyChanged
+{
+ private string _hotkey = "";
+ private string _target = "";
+ private string _label = "";
+ private string _type = "app";
+
+ public string Hotkey { get => _hotkey; set { _hotkey = value; OnPropertyChanged(); } }
+ public string Target { get => _target; set { _target = value; OnPropertyChanged(); } }
+ public string Label { get => _label; set { _label = value; OnPropertyChanged(); } }
+ public string Type { get => _type; set { _type = value; OnPropertyChanged(); OnPropertyChanged(nameof(TypeSymbol)); } }
+
+ public string TypeSymbol => Type switch
+ {
+ "url" => Symbols.Globe,
+ "folder" => Symbols.Folder,
+ "command" => "\uE756", // Terminal
+ _ => Symbols.App
+ };
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected void OnPropertyChanged([CallerMemberName] string? n = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
+}
+
// ─── 스니펫 행 모델 ────────────────────────────────────────────────────────────
public class SnippetRowModel : INotifyPropertyChanged
diff --git a/src/AxCopilot/Views/SettingsWindow.xaml b/src/AxCopilot/Views/SettingsWindow.xaml
index cacf29a..e28f1fa 100644
--- a/src/AxCopilot/Views/SettingsWindow.xaml
+++ b/src/AxCopilot/Views/SettingsWindow.xaml
@@ -2791,6 +2791,190 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 어떤 앱이 포커스를 가져도 등록된 전용 핫키로 파일·폴더·앱·URL을 즉시 실행합니다.
+ 지원 형식: Ctrl+Alt+1, Ctrl+Shift+F2 등
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 앱 (exe)
+ 폴더
+ URL
+ 명령어
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AxCopilot/Views/SettingsWindow.xaml.cs b/src/AxCopilot/Views/SettingsWindow.xaml.cs
index 9956c25..ff080a6 100644
--- a/src/AxCopilot/Views/SettingsWindow.xaml.cs
+++ b/src/AxCopilot/Views/SettingsWindow.xaml.cs
@@ -132,6 +132,95 @@ public partial class SettingsWindow : Window
_vm.RemoveShortcut(shortcut);
}
+ // ─── 전용 핫키 이벤트 핸들러 ───────────────────────────────────────────────
+
+ /// 핫키 입력 영역 클릭 → PreviewKeyDown 캡처 모드 활성화
+ private bool _hotkeyRecording;
+
+ private void HotkeyRecord_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ _hotkeyRecording = true;
+ SuspendHotkeyCallback?.Invoke(true);
+ _vm.NewHotkeyStr = "키를 누르세요…";
+ // 전역 PreviewKeyDown 캡처
+ this.PreviewKeyDown -= HotkeyGlobalCapture_PreviewKeyDown;
+ this.PreviewKeyDown += HotkeyGlobalCapture_PreviewKeyDown;
+ }
+
+ private void HotkeyGlobalCapture_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
+ {
+ if (!_hotkeyRecording) return;
+ e.Handled = true;
+
+ var key = e.Key == System.Windows.Input.Key.System ? e.SystemKey : e.Key;
+
+ // 수정자 단독 → 무시
+ if (key is System.Windows.Input.Key.LeftCtrl or System.Windows.Input.Key.RightCtrl
+ or System.Windows.Input.Key.LeftAlt or System.Windows.Input.Key.RightAlt
+ or System.Windows.Input.Key.LeftShift or System.Windows.Input.Key.RightShift
+ or System.Windows.Input.Key.LWin or System.Windows.Input.Key.RWin)
+ return;
+
+ if (key == System.Windows.Input.Key.Escape)
+ {
+ _vm.NewHotkeyStr = "";
+ _hotkeyRecording = false;
+ SuspendHotkeyCallback?.Invoke(false);
+ this.PreviewKeyDown -= HotkeyGlobalCapture_PreviewKeyDown;
+ return;
+ }
+
+ var parts = new List();
+ if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Control)) parts.Add("Ctrl");
+ if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Alt)) parts.Add("Alt");
+ if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Shift)) parts.Add("Shift");
+ if (System.Windows.Input.Keyboard.Modifiers.HasFlag(System.Windows.Input.ModifierKeys.Windows)) parts.Add("Win");
+
+ var keyName = key switch
+ {
+ System.Windows.Input.Key.Snapshot => "PrintScreen",
+ System.Windows.Input.Key.Scroll => "ScrollLock",
+ System.Windows.Input.Key.Pause => "Pause",
+ _ => key.ToString()
+ };
+ parts.Add(keyName);
+
+ var hotkey = string.Join("+", parts);
+ if (Core.HotkeyParser.TryParse(hotkey, out _))
+ _vm.NewHotkeyStr = hotkey;
+
+ _hotkeyRecording = false;
+ SuspendHotkeyCallback?.Invoke(false);
+ this.PreviewKeyDown -= HotkeyGlobalCapture_PreviewKeyDown;
+ }
+
+ private void AddHotkey_Click(object sender, RoutedEventArgs e)
+ {
+ _vm.AddCustomHotkey();
+ }
+
+ private void RemoveHotkey_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (sender is FrameworkElement fe && fe.Tag is ViewModels.HotkeyRowModel row)
+ _vm.RemoveCustomHotkey(row);
+ }
+
+ private void BrowseHotkeyTarget_Click(object sender, RoutedEventArgs e)
+ {
+ var dlg = new Microsoft.Win32.OpenFileDialog
+ {
+ Title = "실행 파일 선택",
+ Filter = "실행 파일|*.exe;*.bat;*.cmd;*.ps1;*.lnk|모든 파일|*.*"
+ };
+ if (dlg.ShowDialog(this) == true)
+ {
+ _vm.NewHotkeyTarget = dlg.FileName;
+ _vm.NewHotkeyType = "app";
+ if (string.IsNullOrWhiteSpace(_vm.NewHotkeyLabel))
+ _vm.NewHotkeyLabel = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName);
+ }
+ }
+
private void AddBatchCommand_Click(object sender, RoutedEventArgs e)
{
if (!_vm.AddBatchCommand())