[Phase L5-1] 항목별 전용 핫키 기능 구현
HotkeyAssignment 모델 추가 (AppSettings.Models.cs): - Hotkey, Target, Label, Type 필드 (app/url/folder/command) - AppSettings.CustomHotkeys (List<HotkeyAssignment>) 추가 InputListener.cs 확장 (Core/): - _customHotkeys 목록 (스레드 안전 lock) - UpdateCustomHotkeys() 메서드 — 설정 저장 후 즉시 갱신 - CustomHotkeyTriggered 이벤트 (CustomHotkeyEventArgs: Target, Type) - WH_KEYBOARD_LL 콜백에 전용 핫키 감지 분기 추가 HotkeyHandler.cs 신규 생성 (Handlers/, 140줄): - prefix="hotkey" — 등록 목록 표시 / 설정 열기 - ExecuteHotkeyTarget() — app/url/folder/command 타입별 실행 App.xaml.cs + App.Settings.cs: - HotkeyHandler 등록 (Phase L5 주석) - OnCustomHotkeyTriggered 이벤트 핸들러 연결 - 설정 저장 시 UpdateCustomHotkeys() 호출 SettingsViewModel 3파일 업데이트: - HotkeyRowModel (Properties.cs): Hotkey/Target/Label/Type + TypeSymbol - CustomHotkeys ObservableCollection + New* 필드 (Properties.cs) - AddCustomHotkey() / RemoveCustomHotkey() 메서드 (Methods.cs) - HotkeyParser 형식 검증, 중복 핫키 방지 - Load/Save에 CustomHotkeys 매핑 (SettingsViewModel.cs, Methods.cs) SettingsWindow.xaml + .xaml.cs: - "전용 핫키" 탭 신규 추가 (배치 명령 탭 다음) - 안내 배너, 입력 폼 (핫키 레코더 + 표시이름 + 타입 + 대상) - 핫키 목록 (배지 + 타입아이콘 + 이름/경로 + 삭제 버튼) - HotkeyRecord_Click: 클릭 후 키 입력 → 자동 핫키 감지 (PreviewKeyDown) - AddHotkey_Click, RemoveHotkey_Click, BrowseHotkeyTarget_Click 핸들러 docs/LAUNCHER_ROADMAP.md: - L5-1 ✅ 완료 표시, 구현 내용 업데이트 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 세션이름` → 등록된 앱 목록을 각 레이아웃에 배치 | 중간 |
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>전용 핫키 발동 시 발생합니다. EventArgs에 Target과 Type이 포함됩니다.</summary>
|
||||
public event EventHandler<CustomHotkeyEventArgs>? CustomHotkeyTriggered;
|
||||
|
||||
public event EventHandler? HookFailed;
|
||||
|
||||
/// <summary>
|
||||
@@ -74,6 +81,25 @@ public class InputListener : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 항목별 전용 핫키 목록을 업데이트합니다.
|
||||
/// </summary>
|
||||
public void UpdateCustomHotkeys(IEnumerable<Models.HotkeyAssignment> 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}개 등록");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 글로벌 캡처 단축키를 설정합니다.
|
||||
/// </summary>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>항목별 전용 핫키 발동 이벤트 인수.</summary>
|
||||
public class CustomHotkeyEventArgs : EventArgs
|
||||
{
|
||||
public string Target { get; }
|
||||
public string Type { get; }
|
||||
|
||||
public CustomHotkeyEventArgs(string target, string type)
|
||||
{
|
||||
Target = target;
|
||||
Type = type;
|
||||
}
|
||||
}
|
||||
|
||||
155
src/AxCopilot/Handlers/HotkeyHandler.cs
Normal file
155
src/AxCopilot/Handlers/HotkeyHandler.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using AxCopilot.Core;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L5-1: 전용 핫키 관리 핸들러.
|
||||
/// 예: hotkey → 등록된 전용 핫키 목록 표시
|
||||
/// hotkey 1doc → 라벨 또는 대상에 "1doc" 포함 항목 필터
|
||||
/// Enter 시 해당 핫키 항목의 대상을 실행합니다.
|
||||
/// </summary>
|
||||
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var items = new List<LauncherItem>();
|
||||
var hotkeys = _settings?.Settings.CustomHotkeys ?? new List<Models.HotkeyAssignment>();
|
||||
|
||||
if (hotkeys.Count == 0)
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
"등록된 전용 핫키 없음",
|
||||
"설정 → 전용 핫키 탭에서 항목별 글로벌 단축키를 등록하세요",
|
||||
null,
|
||||
"__open_settings__",
|
||||
Symbol: "\uE713"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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;
|
||||
}
|
||||
|
||||
/// <summary>전용 핫키 대상을 타입에 따라 실행합니다.</summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,6 +240,28 @@ public class ScreenCaptureSettings
|
||||
public int ScrollDelayMs { get; set; } = 120;
|
||||
}
|
||||
|
||||
// ─── 전용 핫키 ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 항목별 글로벌 전용 핫키 할당.
|
||||
/// Hotkey: "Ctrl+Alt+1" 형식, Target: 실행 대상 경로/URL/명령어, Label: 표시 이름.
|
||||
/// </summary>
|
||||
public class HotkeyAssignment
|
||||
{
|
||||
[JsonPropertyName("hotkey")]
|
||||
public string Hotkey { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("target")]
|
||||
public string Target { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("label")]
|
||||
public string Label { get; set; } = "";
|
||||
|
||||
/// <summary>실행 타입. app | url | folder | command. 기본 app.</summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "app";
|
||||
}
|
||||
|
||||
// ─── 잠금 해제 알림 설정 ───────────────────────────────────────────────────────
|
||||
|
||||
public class ReminderSettings
|
||||
|
||||
@@ -106,6 +106,12 @@ public class AppSettings
|
||||
[JsonPropertyName("reminder")]
|
||||
public ReminderSettings Reminder { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 항목별 글로벌 전용 핫키 목록. 어떤 앱이 포커스를 가져도 해당 핫키로 즉시 실행됩니다.
|
||||
/// </summary>
|
||||
[JsonPropertyName("customHotkeys")]
|
||||
public List<HotkeyAssignment> CustomHotkeys { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("llm")]
|
||||
public LlmSettings Llm { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -290,6 +290,19 @@ public partial class SettingsViewModel
|
||||
// ─── 인덱스 확장자 ──────────────────────────────────────────────────
|
||||
public ObservableCollection<string> IndexExtensions { get; } = new();
|
||||
|
||||
// ─── 전용 핫키 설정 ─────────────────────────────────────────────────────────
|
||||
public ObservableCollection<HotkeyRowModel> 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<SnippetRowModel> Snippets { get; } = new();
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2791,6 +2791,190 @@
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
전용 핫키 탭 — 어떤 앱에서든 항목별 글로벌 단축키 즉시 실행
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
<TabItem Header="전용 핫키" Tag="" Style="{StaticResource SideNavItem}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0">
|
||||
<TextBlock Text="항목별 전용 핫키" Style="{StaticResource SectionHeader}"/>
|
||||
|
||||
<!-- 안내 배너 -->
|
||||
<Border Background="White" CornerRadius="10" Padding="14,11" Margin="0,0,0,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" Width="36" Height="36" CornerRadius="9"
|
||||
Background="#4B5EFC" Margin="0,0,12,0" VerticalAlignment="Center">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="17"
|
||||
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1" FontSize="12" Foreground="#444466"
|
||||
TextWrapping="Wrap" VerticalAlignment="Center">
|
||||
어떤 앱이 포커스를 가져도 등록된 <Run FontWeight="SemiBold" Foreground="#4B5EFC">전용 핫키</Run>로 파일·폴더·앱·URL을 즉시 실행합니다.
|
||||
지원 형식: <Run FontWeight="SemiBold">Ctrl+Alt+1</Run>, <Run FontWeight="SemiBold">Ctrl+Shift+F2</Run> 등
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 입력 폼 -->
|
||||
<Border Background="White" CornerRadius="10" Padding="16,14" Margin="0,0,0,10"
|
||||
BorderBrush="#E0E0EC" BorderThickness="1.5">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="150"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 행 1: 핫키 + 표시 이름 + 타입 -->
|
||||
<Border Grid.Row="0" Grid.Column="0" CornerRadius="6" Padding="8,6"
|
||||
Background="#F5F5FA" BorderBrush="#D0D0E0" BorderThickness="1"
|
||||
Cursor="IBeam" x:Name="HotkeyRecordBorder"
|
||||
MouseLeftButtonUp="HotkeyRecord_Click"
|
||||
ToolTip="클릭 후 키를 누르면 자동 입력됩니다">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock x:Name="HotkeyRecordText" Grid.Column="0"
|
||||
Text="{Binding NewHotkeyStr, FallbackValue='핫키 클릭 후 입력'}"
|
||||
FontSize="13" FontWeight="SemiBold" Foreground="#4B5EFC"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12" Foreground="#8888AA" Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBox Grid.Row="0" Grid.Column="2"
|
||||
Style="{StaticResource ModernTextBox}"
|
||||
Text="{Binding NewHotkeyLabel, UpdateSourceTrigger=PropertyChanged}"
|
||||
Tag="표시 이름 (예: 내 문서 폴더)"
|
||||
ToolTip="AX Commander 결과에 표시될 이름 (비워두면 파일명 자동 사용)"/>
|
||||
|
||||
<!-- 타입 드롭다운 -->
|
||||
<ComboBox Grid.Row="0" Grid.Column="4"
|
||||
SelectedValue="{Binding NewHotkeyType}"
|
||||
SelectedValuePath="Tag"
|
||||
Style="{StaticResource ModernComboBox}">
|
||||
<ComboBoxItem Tag="app">앱 (exe)</ComboBoxItem>
|
||||
<ComboBoxItem Tag="folder">폴더</ComboBoxItem>
|
||||
<ComboBoxItem Tag="url">URL</ComboBoxItem>
|
||||
<ComboBoxItem Tag="command">명령어</ComboBoxItem>
|
||||
</ComboBox>
|
||||
|
||||
<!-- 행 2: 대상 경로 + 찾아보기 + 추가 -->
|
||||
<Grid Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0"
|
||||
Style="{StaticResource ModernTextBox}"
|
||||
Text="{Binding NewHotkeyTarget, UpdateSourceTrigger=PropertyChanged}"
|
||||
Tag="대상 경로 또는 URL"
|
||||
ToolTip="앱 실행 파일 경로, 폴더 경로, 또는 URL"/>
|
||||
<Button Grid.Column="2" Style="{StaticResource IconButton}"
|
||||
Click="BrowseHotkeyTarget_Click" ToolTip="파일/폴더 선택">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13" Foreground="#555566"/>
|
||||
</Button>
|
||||
<Button Grid.Column="4" Style="{StaticResource PrimaryButton}"
|
||||
Click="AddHotkey_Click" ToolTip="전용 핫키 추가">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="12" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="추가"/>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 핫키 목록 -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl x:Name="HotkeyList"
|
||||
ItemsSource="{Binding CustomHotkeys}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Background="White" CornerRadius="8" Padding="12,10"
|
||||
Margin="0,0,0,6" BorderBrush="#E8E8F0" BorderThickness="1">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="8"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="12"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 타입 아이콘 -->
|
||||
<Border Grid.Column="0" Width="32" Height="32" CornerRadius="8"
|
||||
Background="#EEF0FF">
|
||||
<TextBlock Text="{Binding TypeSymbol}"
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="14"
|
||||
Foreground="#4B5EFC"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<!-- 핫키 배지 -->
|
||||
<Border Grid.Column="2" CornerRadius="5" Padding="7,3"
|
||||
Background="#F3F0FF">
|
||||
<TextBlock Text="{Binding Hotkey}"
|
||||
FontSize="12" FontWeight="Bold"
|
||||
Foreground="#4B5EFC"
|
||||
FontFamily="Consolas"/>
|
||||
</Border>
|
||||
|
||||
<!-- 이름 + 경로 -->
|
||||
<StackPanel Grid.Column="4" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Label}" FontSize="13"
|
||||
FontWeight="SemiBold" Foreground="#222244"/>
|
||||
<TextBlock Text="{Binding Target}" FontSize="11"
|
||||
Foreground="#8888AA"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 삭제 버튼 -->
|
||||
<Border Grid.Column="5" Width="28" Height="28" CornerRadius="6"
|
||||
Background="Transparent" Cursor="Hand"
|
||||
MouseLeftButtonUp="RemoveHotkey_Click"
|
||||
Tag="{Binding}"
|
||||
ToolTip="핫키 제거">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="13" Foreground="#CC3333"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<!-- ══════════════════════════════════════════════════════════════
|
||||
기능 탭 — 단축키로 활성/비활성화 가능한 기능들의 ON/OFF 설정
|
||||
══════════════════════════════════════════════════════════════ -->
|
||||
|
||||
@@ -132,6 +132,95 @@ public partial class SettingsWindow : Window
|
||||
_vm.RemoveShortcut(shortcut);
|
||||
}
|
||||
|
||||
// ─── 전용 핫키 이벤트 핸들러 ───────────────────────────────────────────────
|
||||
|
||||
/// <summary>핫키 입력 영역 클릭 → PreviewKeyDown 캡처 모드 활성화</summary>
|
||||
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<string>();
|
||||
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())
|
||||
|
||||
Reference in New Issue
Block a user