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>
523 lines
22 KiB
C#
523 lines
22 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.ComponentModel;
|
|
using System.Windows.Forms;
|
|
using AxCopilot.Models;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
using AxCopilot.Views;
|
|
|
|
namespace AxCopilot.ViewModels;
|
|
|
|
public partial class SettingsViewModel
|
|
{
|
|
// ─── 인덱스 경로 메서드 ──────────────────────────────────────────────
|
|
public void AddIndexPath(string path)
|
|
{
|
|
var trimmed = path.Trim();
|
|
if (string.IsNullOrWhiteSpace(trimmed)) return;
|
|
if (IndexPaths.Any(p => p.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return;
|
|
IndexPaths.Add(trimmed);
|
|
}
|
|
|
|
public void RemoveIndexPath(string path) => IndexPaths.Remove(path);
|
|
|
|
// ─── 인덱스 확장자 메서드 ──────────────────────────────────────────
|
|
public void AddExtension(string ext)
|
|
{
|
|
var trimmed = ext.Trim().ToLowerInvariant();
|
|
if (string.IsNullOrWhiteSpace(trimmed)) return;
|
|
if (!trimmed.StartsWith(".")) trimmed = "." + trimmed;
|
|
if (IndexExtensions.Any(e => e.Equals(trimmed, StringComparison.OrdinalIgnoreCase))) return;
|
|
IndexExtensions.Add(trimmed);
|
|
}
|
|
|
|
public void RemoveExtension(string ext) => IndexExtensions.Remove(ext);
|
|
|
|
public void BrowseIndexPath()
|
|
{
|
|
using var dlg = new FolderBrowserDialog
|
|
{
|
|
Description = "인덱스할 폴더 선택",
|
|
UseDescriptionForTitle = true,
|
|
ShowNewFolderButton = false
|
|
};
|
|
if (dlg.ShowDialog() != DialogResult.OK) return;
|
|
AddIndexPath(dlg.SelectedPath);
|
|
}
|
|
|
|
// ─── 빠른 실행 단축키 메서드 ──────────────────────────────────────────
|
|
public bool AddShortcut()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_newKey) || string.IsNullOrWhiteSpace(_newTarget))
|
|
return false;
|
|
|
|
// 중복 키 확인
|
|
if (AppShortcuts.Any(s => s.Key.Equals(_newKey.Trim(), StringComparison.OrdinalIgnoreCase)))
|
|
return false;
|
|
|
|
AppShortcuts.Add(new AppShortcutModel
|
|
{
|
|
Key = _newKey.Trim(),
|
|
Description = _newDescription.Trim(),
|
|
Target = _newTarget.Trim(),
|
|
Type = _newType
|
|
});
|
|
|
|
NewKey = ""; NewDescription = ""; NewTarget = ""; NewType = "app";
|
|
return true;
|
|
}
|
|
|
|
public void RemoveShortcut(AppShortcutModel shortcut) => AppShortcuts.Remove(shortcut);
|
|
|
|
/// <summary>파일 선택 대화상자. 선택 시 NewTarget에 자동 설정.</summary>
|
|
public void BrowseTarget()
|
|
{
|
|
using var dlg = new OpenFileDialog
|
|
{
|
|
Filter = "실행 파일 (*.exe)|*.exe|모든 파일 (*.*)|*.*",
|
|
Title = "앱 선택"
|
|
};
|
|
if (dlg.ShowDialog() != DialogResult.OK) return;
|
|
|
|
NewTarget = dlg.FileName;
|
|
NewType = "app";
|
|
if (string.IsNullOrWhiteSpace(NewDescription))
|
|
NewDescription = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName);
|
|
}
|
|
|
|
public void SelectTheme(string key)
|
|
{
|
|
SelectedThemeKey = key;
|
|
ThemePreviewRequested?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
public void PickColor(ColorRowModel row)
|
|
{
|
|
using var dlg = new ColorDialog { FullOpen = true };
|
|
try
|
|
{
|
|
var color = ThemeResourceHelper.HexColor(row.Hex);
|
|
dlg.Color = System.Drawing.Color.FromArgb(color.R, color.G, color.B);
|
|
}
|
|
catch (Exception) { /* 기본값 사용 */ }
|
|
|
|
if (dlg.ShowDialog() != DialogResult.OK) return;
|
|
|
|
row.Hex = $"#{dlg.Color.R:X2}{dlg.Color.G:X2}{dlg.Color.B:X2}";
|
|
if (_selectedThemeKey == "custom")
|
|
ThemePreviewRequested?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
// 시스템 예약 프리픽스 (핸들러에서 이미 사용 중인 키)
|
|
private static readonly HashSet<string> ReservedPrefixes = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"=", "?", "#", "$", ";", "@", "~", ">", "!",
|
|
"emoji", "color", "recent", "note", "uninstall", "kill", "media",
|
|
"info", "*", "json", "encode", "port", "env", "snap", "help",
|
|
"pick", "date", "svc", "pipe", "journal", "routine", "batch",
|
|
"diff", "win", "stats", "fav", "rename", "monitor", "scaffold",
|
|
};
|
|
|
|
/// <summary>설정 저장 전 프리픽스/키워드 충돌을 검사합니다. 충돌 시 메시지를 반환합니다.</summary>
|
|
public string? ValidateBeforeSave()
|
|
{
|
|
// 캡처 프리픽스 충돌 검사
|
|
var cap = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim();
|
|
if (cap != "cap" && ReservedPrefixes.Contains(cap))
|
|
return $"캡처 프리픽스 '{cap}'은(는) 이미 사용 중인 예약어입니다.";
|
|
|
|
// 빠른 실행 별칭 키 중복 검사
|
|
var aliasKeys = AppShortcuts.Select(s => s.Key.ToLowerInvariant()).ToList();
|
|
var duplicateAlias = aliasKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1);
|
|
if (duplicateAlias != null)
|
|
return $"빠른 실행 키워드 '{duplicateAlias.Key}'이(가) 중복되었습니다.";
|
|
|
|
// 빠른 실행 별칭 키 vs 예약 프리픽스 충돌
|
|
foreach (var key in aliasKeys)
|
|
{
|
|
if (ReservedPrefixes.Contains(key))
|
|
return $"빠른 실행 키워드 '{key}'은(는) 시스템 예약어와 충돌합니다.";
|
|
}
|
|
|
|
// 배치 명령 키 중복 검사
|
|
var batchKeys = BatchCommands.Select(b => b.Key.ToLowerInvariant()).ToList();
|
|
var duplicateBatch = batchKeys.GroupBy(k => k).FirstOrDefault(g => g.Count() > 1);
|
|
if (duplicateBatch != null)
|
|
return $"배치 명령 키워드 '{duplicateBatch.Key}'이(가) 중복되었습니다.";
|
|
|
|
return null; // 문제 없음
|
|
}
|
|
|
|
public void Save()
|
|
{
|
|
// 충돌 검사
|
|
var conflict = ValidateBeforeSave();
|
|
if (conflict != null)
|
|
{
|
|
CustomMessageBox.Show(
|
|
conflict,
|
|
"AX Copilot — 설정 저장 오류",
|
|
System.Windows.MessageBoxButton.OK,
|
|
System.Windows.MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
var s = _service.Settings;
|
|
s.Hotkey = _hotkey;
|
|
s.Launcher.MaxResults = _maxResults;
|
|
s.Launcher.Opacity = _opacity;
|
|
s.Launcher.Theme = _selectedThemeKey;
|
|
s.Launcher.Position = _launcherPosition;
|
|
s.Launcher.WebSearchEngine = _webSearchEngine;
|
|
s.Launcher.SnippetAutoExpand = _snippetAutoExpand;
|
|
s.Launcher.Language = _language;
|
|
L10n.SetLanguage(_language);
|
|
|
|
// 기능 토글 저장
|
|
s.Launcher.ShowNumberBadges = _showNumberBadges;
|
|
s.Launcher.EnableFavorites = _enableFavorites;
|
|
s.Launcher.EnableRecent = _enableRecent;
|
|
s.Launcher.EnableActionMode = _enableActionMode;
|
|
s.Launcher.CloseOnFocusLost = _closeOnFocusLost;
|
|
s.Launcher.ShowPrefixBadge = _showPrefixBadge;
|
|
s.Launcher.EnableIconAnimation = _enableIconAnimation;
|
|
s.Launcher.EnableRainbowGlow = _enableRainbowGlow;
|
|
s.Launcher.EnableSelectionGlow = _enableSelectionGlow;
|
|
s.Launcher.EnableRandomPlaceholder = _enableRandomPlaceholder;
|
|
s.Launcher.ShowLauncherBorder = _showLauncherBorder;
|
|
s.Launcher.ShortcutHelpUseThemeColor = _shortcutHelpUseThemeColor;
|
|
s.Launcher.EnableTextAction = _enableTextAction;
|
|
s.Launcher.EnableFileDialogIntegration = _enableFileDialogIntegration;
|
|
s.Launcher.EnableClipboardAutoCategory = _enableClipboardAutoCategory;
|
|
s.Launcher.MaxPinnedClipboardItems = _maxPinnedClipboardItems;
|
|
s.Launcher.TextActionTranslateLanguage = _textActionTranslateLanguage;
|
|
s.Llm.MaxSubAgents = _maxSubAgents;
|
|
s.Llm.PdfExportPath = _pdfExportPath;
|
|
s.Llm.TipDurationSeconds = _tipDurationSeconds;
|
|
|
|
// LLM 공통 설정 저장
|
|
s.Llm.Service = _llmService;
|
|
s.Llm.Streaming = _llmStreaming;
|
|
s.Llm.MaxContextTokens = _llmMaxContextTokens;
|
|
s.Llm.RetentionDays = _llmRetentionDays;
|
|
s.Llm.Temperature = _llmTemperature;
|
|
s.Llm.DefaultAgentPermission = _defaultAgentPermission;
|
|
s.Llm.DefaultOutputFormat = _defaultOutputFormat;
|
|
s.Llm.DefaultMood = _defaultMood;
|
|
s.Llm.AutoPreview = _autoPreview;
|
|
s.Llm.MaxAgentIterations = _maxAgentIterations;
|
|
s.Llm.MaxRetryOnError = _maxRetryOnError;
|
|
s.Llm.AgentLogLevel = _agentLogLevel;
|
|
s.Llm.AgentDecisionLevel = _agentDecisionLevel;
|
|
s.Llm.PlanMode = _planMode;
|
|
s.Llm.EnableMultiPassDocument = _enableMultiPassDocument;
|
|
s.Llm.EnableCoworkVerification = _enableCoworkVerification;
|
|
s.Llm.EnableFilePathHighlight = _enableFilePathHighlight;
|
|
s.Llm.FolderDataUsage = _folderDataUsage;
|
|
s.Llm.EnableAuditLog = _enableAuditLog;
|
|
s.Llm.EnableAgentMemory = _enableAgentMemory;
|
|
s.Llm.EnableProjectRules = _enableProjectRules;
|
|
s.Llm.MaxMemoryEntries = _maxMemoryEntries;
|
|
s.Llm.EnableImageInput = _enableImageInput;
|
|
s.Llm.MaxImageSizeKb = _maxImageSizeKb;
|
|
s.Llm.EnableToolHooks = _enableToolHooks;
|
|
s.Llm.ToolHookTimeoutMs = _toolHookTimeoutMs;
|
|
s.Llm.EnableSkillSystem = _enableSkillSystem;
|
|
s.Llm.SkillsFolderPath = _skillsFolderPath;
|
|
s.Llm.SlashPopupPageSize = _slashPopupPageSize;
|
|
s.Llm.EnableDragDropAiActions = _enableDragDropAiActions;
|
|
s.Llm.DragDropAutoSend = _dragDropAutoSend;
|
|
s.Llm.Code.EnableCodeReview = _enableCodeReview;
|
|
s.Llm.EnableAutoRouter = _enableAutoRouter;
|
|
s.Llm.AutoRouterConfidence = _autoRouterConfidence;
|
|
s.Llm.EnableChatRainbowGlow = _enableChatRainbowGlow;
|
|
s.Llm.NotifyOnComplete = _notifyOnComplete;
|
|
s.Llm.ShowTips = _showTips;
|
|
s.Llm.DevMode = _devMode;
|
|
s.Llm.DevModeStepApproval = _devModeStepApproval;
|
|
s.Llm.WorkflowVisualizer = _workflowVisualizer;
|
|
s.Llm.FreeTierMode = _freeTierMode;
|
|
s.Llm.FreeTierDelaySeconds = _freeTierDelaySeconds;
|
|
s.Llm.ShowTotalCallStats = _showTotalCallStats;
|
|
|
|
// 서비스별 독립 설정 저장
|
|
s.Llm.OllamaEndpoint = _ollamaEndpoint;
|
|
s.Llm.OllamaModel = _ollamaModel;
|
|
s.Llm.VllmEndpoint = _vllmEndpoint;
|
|
s.Llm.VllmModel = _vllmModel;
|
|
s.Llm.GeminiModel = _geminiModel;
|
|
s.Llm.ClaudeModel = _claudeModel;
|
|
s.Llm.GeminiApiKey = _geminiApiKey;
|
|
s.Llm.ClaudeApiKey = _claudeApiKey;
|
|
|
|
// 내부 서비스 API 키 저장 (암호화 분기)
|
|
if (!string.IsNullOrEmpty(_ollamaApiKey) && _ollamaApiKey != "(저장됨)")
|
|
s.Llm.OllamaApiKey = CryptoService.EncryptIfEnabled(_ollamaApiKey, s.Llm.EncryptionEnabled);
|
|
if (!string.IsNullOrEmpty(_vllmApiKey) && _vllmApiKey != "(저장됨)")
|
|
s.Llm.VllmApiKey = CryptoService.EncryptIfEnabled(_vllmApiKey, s.Llm.EncryptionEnabled);
|
|
|
|
// 활성 서비스의 설정을 기존 호환 필드에도 동기화 (LlmService.cs 호환)
|
|
switch (_llmService)
|
|
{
|
|
case "ollama":
|
|
s.Llm.Endpoint = _ollamaEndpoint;
|
|
s.Llm.Model = _ollamaModel;
|
|
s.Llm.EncryptedApiKey = s.Llm.OllamaApiKey;
|
|
break;
|
|
case "vllm":
|
|
s.Llm.Endpoint = _vllmEndpoint;
|
|
s.Llm.Model = _vllmModel;
|
|
s.Llm.EncryptedApiKey = s.Llm.VllmApiKey;
|
|
break;
|
|
case "gemini":
|
|
s.Llm.ApiKey = _geminiApiKey;
|
|
s.Llm.Model = _geminiModel;
|
|
break;
|
|
case "claude":
|
|
s.Llm.ApiKey = _claudeApiKey;
|
|
s.Llm.Model = _claudeModel;
|
|
break;
|
|
}
|
|
|
|
// 등록 모델 저장
|
|
s.Llm.RegisteredModels = RegisteredModels
|
|
.Where(rm => !string.IsNullOrWhiteSpace(rm.Alias))
|
|
.Select(rm => new RegisteredModel
|
|
{
|
|
Alias = rm.Alias,
|
|
EncryptedModelName = rm.EncryptedModelName,
|
|
Service = rm.Service,
|
|
Endpoint = rm.Endpoint,
|
|
ApiKey = rm.ApiKey,
|
|
AuthType = rm.AuthType ?? "bearer",
|
|
Cp4dUrl = rm.Cp4dUrl ?? "",
|
|
Cp4dUsername = rm.Cp4dUsername ?? "",
|
|
Cp4dPassword = rm.Cp4dPassword ?? "",
|
|
})
|
|
.ToList();
|
|
|
|
// 프롬프트 템플릿 저장
|
|
s.Llm.PromptTemplates = PromptTemplates
|
|
.Where(pt => !string.IsNullOrWhiteSpace(pt.Name))
|
|
.Select(pt => new PromptTemplate { Name = pt.Name, Content = pt.Content, Icon = pt.Icon })
|
|
.ToList();
|
|
|
|
// 인덱스 경로 + 확장자 저장
|
|
s.IndexPaths = IndexPaths.ToList();
|
|
s.IndexExtensions = IndexExtensions.ToList();
|
|
s.IndexSpeed = _indexSpeed;
|
|
|
|
// 커스텀 색상 + 모양 저장
|
|
var c = s.Launcher.CustomTheme ??= new CustomThemeColors();
|
|
foreach (var row in ColorRows)
|
|
{
|
|
var prop = typeof(CustomThemeColors).GetProperty(row.Property);
|
|
prop?.SetValue(c, row.Hex);
|
|
}
|
|
c.WindowCornerRadius = _customWindowCornerRadius;
|
|
c.ItemCornerRadius = _customItemCornerRadius;
|
|
|
|
// 빠른 실행 단축키 저장:
|
|
// batch/api/clipboard type은 그대로 유지, app/url/folder는 ViewModel 내용으로 교체
|
|
var otherAliases = s.Aliases
|
|
.Where(a => a.Type is not ("app" or "url" or "folder" or "batch"))
|
|
.ToList();
|
|
s.Aliases = otherAliases
|
|
.Concat(AppShortcuts.Select(sc => new AliasEntry
|
|
{
|
|
Key = sc.Key,
|
|
Type = sc.Type,
|
|
Target = sc.Target,
|
|
Description = string.IsNullOrWhiteSpace(sc.Description) ? null : sc.Description
|
|
}))
|
|
.Concat(BatchCommands.Select(b => new AliasEntry
|
|
{
|
|
Key = b.Key,
|
|
Type = "batch",
|
|
Target = b.Command,
|
|
ShowWindow = b.ShowWindow
|
|
}))
|
|
.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
|
|
{
|
|
Key = sn.Key,
|
|
Name = sn.Name,
|
|
Content = sn.Content
|
|
}).ToList();
|
|
|
|
// 알림 설정 저장
|
|
s.Reminder.Enabled = _reminderEnabled;
|
|
s.Reminder.Corner = _reminderCorner;
|
|
s.Reminder.IntervalMinutes = _reminderIntervalMinutes;
|
|
s.Reminder.DisplaySeconds = _reminderDisplaySeconds;
|
|
|
|
// 캡처 설정 저장
|
|
s.ScreenCapture.Prefix = string.IsNullOrWhiteSpace(_capPrefix) ? "cap" : _capPrefix.Trim();
|
|
s.ScreenCapture.GlobalHotkeyEnabled = _capGlobalHotkeyEnabled;
|
|
s.ScreenCapture.GlobalHotkey = string.IsNullOrWhiteSpace(_capGlobalHotkey) ? "PrintScreen" : _capGlobalHotkey.Trim();
|
|
s.ScreenCapture.GlobalHotkeyMode = _capGlobalMode;
|
|
s.ScreenCapture.ScrollDelayMs = Math.Max(50, _capScrollDelayMs);
|
|
|
|
// 클립보드 히스토리 설정 저장
|
|
s.ClipboardHistory.Enabled = _clipboardEnabled;
|
|
s.ClipboardHistory.MaxItems = _clipboardMaxItems;
|
|
|
|
// 시스템 명령 설정 저장
|
|
var sc = s.SystemCommands;
|
|
sc.ShowLock = _sysShowLock;
|
|
sc.ShowSleep = _sysShowSleep;
|
|
sc.ShowRestart = _sysShowRestart;
|
|
sc.ShowShutdown = _sysShowShutdown;
|
|
sc.ShowHibernate = _sysShowHibernate;
|
|
sc.ShowLogout = _sysShowLogout;
|
|
sc.ShowRecycleBin = _sysShowRecycleBin;
|
|
|
|
// 시스템 명령 별칭 저장
|
|
var cmdAliases = new Dictionary<string, List<string>>();
|
|
void SaveAlias(string key, string val)
|
|
{
|
|
var list = ParseAliases(val);
|
|
if (list.Count > 0) cmdAliases[key] = list;
|
|
}
|
|
SaveAlias("lock", _aliasLock);
|
|
SaveAlias("sleep", _aliasSleep);
|
|
SaveAlias("restart", _aliasRestart);
|
|
SaveAlias("shutdown", _aliasShutdown);
|
|
SaveAlias("hibernate", _aliasHibernate);
|
|
SaveAlias("logout", _aliasLogout);
|
|
SaveAlias("recycle", _aliasRecycle);
|
|
sc.CommandAliases = cmdAliases;
|
|
|
|
_service.Save();
|
|
SaveCompleted?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
public bool AddBatchCommand()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_newBatchKey) || string.IsNullOrWhiteSpace(_newBatchCommand))
|
|
return false;
|
|
if (BatchCommands.Any(b => b.Key.Equals(_newBatchKey.Trim(), StringComparison.OrdinalIgnoreCase)))
|
|
return false;
|
|
|
|
BatchCommands.Add(new BatchCommandModel
|
|
{
|
|
Key = _newBatchKey.Trim(),
|
|
Command = _newBatchCommand.Trim(),
|
|
ShowWindow = _newBatchShowWindow
|
|
});
|
|
NewBatchKey = ""; NewBatchCommand = ""; NewBatchShowWindow = false;
|
|
return true;
|
|
}
|
|
|
|
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()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_newSnippetKey) || string.IsNullOrWhiteSpace(_newSnippetContent))
|
|
return false;
|
|
if (Snippets.Any(sn => sn.Key.Equals(_newSnippetKey.Trim(), StringComparison.OrdinalIgnoreCase)))
|
|
return false;
|
|
|
|
Snippets.Add(new SnippetRowModel
|
|
{
|
|
Key = _newSnippetKey.Trim(),
|
|
Name = _newSnippetName.Trim(),
|
|
Content = _newSnippetContent.Trim()
|
|
});
|
|
NewSnippetKey = ""; NewSnippetName = ""; NewSnippetContent = "";
|
|
return true;
|
|
}
|
|
|
|
public void RemoveSnippet(SnippetRowModel row) => Snippets.Remove(row);
|
|
|
|
// ─── 캡처 메서드 ────────────────────────────────────────────────────────
|
|
public void ResetCapPrefix() => CapPrefix = "cap";
|
|
|
|
public void ResetCapGlobalHotkey()
|
|
{
|
|
CapGlobalHotkey = "PrintScreen";
|
|
CapGlobalMode = "screen";
|
|
}
|
|
|
|
// ─── 알림 메서드 ────────────────────────────────────────────────────────
|
|
public List<string> GetReminderCategories() => _service.Settings.Reminder.EnabledCategories;
|
|
|
|
// ─── 시스템 명령 메서드 ─────────────────────────────────────────────────
|
|
public void ResetSystemCommandAliases()
|
|
{
|
|
AliasLock = ""; AliasSleep = ""; AliasRestart = "";
|
|
AliasShutdown = ""; AliasHibernate = ""; AliasLogout = ""; AliasRecycle = "";
|
|
}
|
|
|
|
private static string FormatAliases(Dictionary<string, List<string>> dict, string key)
|
|
=> dict.TryGetValue(key, out var list) ? string.Join(", ", list) : "";
|
|
|
|
private static List<string> ParseAliases(string input)
|
|
=> input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Where(s => !string.IsNullOrWhiteSpace(s))
|
|
.ToList();
|
|
|
|
public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
|
|
protected void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string? n = null)
|
|
=> PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(n));
|
|
}
|