using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using AxCopilot.Services; using AxCopilot.ViewModels; namespace AxCopilot.Views; public partial class SettingsWindow : Window { private readonly SettingsViewModel _vm; private readonly Action _previewCallback; private readonly Action _revertCallback; private bool _saved; /// App 인스턴스 캐시 — Application.Current as App 반복 호출 제거. private static App? CurrentApp => System.Windows.Application.Current as App; /// /// 핫키 녹화 시작/종료를 외부(App.xaml.cs)에 알리는 콜백. /// true = 녹화 시작(핫키 일시 정지), false = 녹화 종료(핫키 재개) /// public Action? SuspendHotkeyCallback { get; set; } /// 테마 키를 받아 런처에 즉시 미리보기 적용 /// 취소/X 닫기 시 원래 설정으로 복원 public SettingsWindow(SettingsViewModel vm, Action previewCallback, Action revertCallback) { InitializeComponent(); _vm = vm; _previewCallback = previewCallback; _revertCallback = revertCallback; DataContext = vm; vm.ThemePreviewRequested += (_, _) => _previewCallback(vm.SelectedThemeKey); vm.SaveCompleted += (_, _) => { _saved = true; Close(); // 인덱스 재빌드를 백그라운드에서 조용히 실행 (UI 차단 없음) _ = Task.Run(async () => { try { var indexService = CurrentApp?.IndexService; if (indexService != null) await indexService.BuildAsync(); } catch (Exception) { /* 인덱싱 실패해도 설정 저장은 완료 */ } }); }; Loaded += async (_, _) => { RefreshHotkeyBadges(); SetVersionText(); EnsureHotkeyInCombo(); BuildQuoteCategoryCheckboxes(); BuildDockBarSettings(); BuildTextActionCommandsPanel(); MoveBlockSectionToEtc(); // 스킬이 아직 로드되지 않았으면 백그라운드에서 로드 후 UI 구성 var app = CurrentApp; var skillsLoaded = Services.Agent.SkillService.Skills.Count > 0; if (!skillsLoaded && (app?.SettingsService?.Settings.Llm.EnableSkillSystem ?? false)) { // 스킬 로드 (RuntimeDetector 포함)를 백그라운드에서 실행 await Task.Run(() => { Services.Agent.SkillService.EnsureSkillFolder(); Services.Agent.SkillService.LoadSkills(app?.SettingsService?.Settings.Llm.SkillsFolderPath); }); } BuildToolRegistryPanel(); LoadAdvancedSettings(); RefreshStorageInfo(); // 개발자 모드는 저장된 설정 유지 (끄면 하위 기능 모두 비활성) UpdateDevModeContentVisibility(); // AI 기능 토글 초기화 ApplyAiEnabledState(app?.SettingsService?.Settings.AiEnabled ?? false, init: true); // 네트워크 모드 토글 초기화 (사내 모드 = true = 차단) if (InternalModeToggle != null) InternalModeToggle.IsChecked = app?.SettingsService?.Settings.InternalModeEnabled ?? true; }; } // ─── 에이전트 차단 섹션 → 기타 탭 이동 ────────────────────────────────────── private void MoveBlockSectionToEtc() { if (AgentBlockSection == null || AgentEtcContent == null) return; var parent = AgentBlockSection.Parent as Panel; if (parent != null) { parent.Children.Remove(AgentBlockSection); AgentBlockSection.Margin = new Thickness(0, 0, 0, 16); AgentEtcContent.Children.Add(AgentBlockSection); } } private void AddSnippet_Click(object sender, RoutedEventArgs e) { if (!_vm.AddSnippet()) CustomMessageBox.Show( "키워드와 내용은 필수 항목입니다.\n동일한 키워드가 이미 존재하면 추가할 수 없습니다.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning); } private void DeleteSnippet_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is ViewModels.SnippetRowModel row) _vm.RemoveSnippet(row); } private void Browse_Click(object sender, RoutedEventArgs e) => _vm.BrowseTarget(); private void AddShortcut_Click(object sender, RoutedEventArgs e) { if (!_vm.AddShortcut()) CustomMessageBox.Show( "키워드와 실행 대상은 필수 항목입니다.\n이미 동일한 키워드가 존재하면 추가할 수 없습니다.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning); } private void DeleteShortcut_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is ViewModels.AppShortcutModel shortcut) _vm.RemoveShortcut(shortcut); } private void AddBatchCommand_Click(object sender, RoutedEventArgs e) { if (!_vm.AddBatchCommand()) CustomMessageBox.Show( "키워드와 명령어는 필수 항목입니다.\n동일한 키워드가 이미 존재하면 추가할 수 없습니다.", "입력 오류", MessageBoxButton.OK, MessageBoxImage.Warning); } private void DeleteBatchCommand_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is ViewModels.BatchCommandModel cmd) _vm.RemoveBatchCommand(cmd); } private void ResetCommandAliases_Click(object sender, RoutedEventArgs e) { _vm.ResetSystemCommandAliases(); } private void AddIndexPath_Click(object sender, RoutedEventArgs e) { var text = NewIndexPathBox?.Text?.Trim() ?? ""; if (!string.IsNullOrWhiteSpace(text)) { _vm.AddIndexPath(text); if (NewIndexPathBox != null) NewIndexPathBox.Text = ""; } } private void BrowseIndexPath_Click(object sender, RoutedEventArgs e) => _vm.BrowseIndexPath(); private void ResetCapPrefix_Click(object sender, RoutedEventArgs e) => _vm.ResetCapPrefix(); private void ResetCapGlobalHotkey_Click(object sender, RoutedEventArgs e) => _vm.ResetCapGlobalHotkey(); private void CapHotkeyRecorder_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) { 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) 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 (AxCopilot.Core.HotkeyParser.TryParse(hotkey, out _)) _vm.CapGlobalHotkey = hotkey; } private void ReminderCorner_Click(object sender, RoutedEventArgs e) { if (sender is System.Windows.Controls.RadioButton rb && rb.Tag is string corner) _vm.ReminderCorner = corner; } private void ReminderPreview_Click(object sender, RoutedEventArgs e) { try { var svc = _vm.Service; var (text, author) = Services.QuoteService.GetRandom(svc.Settings.Reminder.EnabledCategories); var usage = TimeSpan.FromMinutes(42); // 샘플 값 // 현재 편집 중인 값을 임시로 적용 var cfg = svc.Settings.Reminder; var origCorner = cfg.Corner; var origSeconds = cfg.DisplaySeconds; cfg.Corner = _vm.ReminderCorner; cfg.DisplaySeconds = _vm.ReminderDisplaySeconds; var popup = new ReminderPopupWindow(text, author, usage, svc); popup.Show(); cfg.Corner = origCorner; cfg.DisplaySeconds = origSeconds; } catch (Exception ex) { Services.LogService.Warn($"알림 미리보기 실패: {ex.Message}"); } } private void RemoveIndexPath_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is string path) _vm.RemoveIndexPath(path); } private void AddExtension_Click(object sender, RoutedEventArgs e) { var text = NewExtensionBox?.Text?.Trim() ?? ""; if (!string.IsNullOrWhiteSpace(text)) { _vm.AddExtension(text); if (NewExtensionBox != null) NewExtensionBox.Text = ""; } } private void RemoveExtension_Click(object sender, RoutedEventArgs e) { if (sender is Button btn && btn.Tag is string ext) _vm.RemoveExtension(ext); } private void Save_Click(object sender, RoutedEventArgs e) { SaveAdvancedSettings(); // 폴백/MCP 텍스트 박스 → settings 저장 _vm.Save(); CustomMessageBox.Show("설정이 저장되었습니다.", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Information); } private void Cancel_Click(object sender, RoutedEventArgs e) { Close(); } private void ExportSettings_Click(object sender, RoutedEventArgs e) { var dlg = new Microsoft.Win32.SaveFileDialog { Title = "설정 내보내기", Filter = "AX Copilot 설정|*.axsettings", FileName = $"AxCopilot_Settings_{DateTime.Now:yyyyMMdd}", DefaultExt = ".axsettings" }; if (dlg.ShowDialog() != true) return; try { var srcPath = SettingsService.SettingsPath; if (System.IO.File.Exists(srcPath)) { System.IO.File.Copy(srcPath, dlg.FileName, overwrite: true); CustomMessageBox.Show( $"설정이 내보내졌습니다:\n{dlg.FileName}", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Information); } } catch (Exception ex) { CustomMessageBox.Show($"내보내기 실패: {ex.Message}", "오류"); } } private void ImportSettings_Click(object sender, RoutedEventArgs e) { var dlg = new Microsoft.Win32.OpenFileDialog { Title = "설정 불러오기", Filter = "AX Copilot 설정|*.axsettings|모든 파일|*.*" }; if (dlg.ShowDialog() != true) return; var result = CustomMessageBox.Show( "설정을 불러오면 현재 설정이 덮어씌워집니다.\n계속하시겠습니까?", "AX Copilot — 설정 불러오기", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != System.Windows.MessageBoxResult.Yes) return; try { var fileContent = System.IO.File.ReadAllText(dlg.FileName); // 암호화된 파일인지 판별: PortableDecrypt 시도 var json = CryptoService.PortableDecrypt(fileContent); if (string.IsNullOrEmpty(json)) { // 평문 JSON일 수 있음 — 직접 파싱 시도 try { var test = System.Text.Json.JsonSerializer.Deserialize(fileContent); if (test != null) json = fileContent; } catch (Exception) { } } if (string.IsNullOrEmpty(json)) { CustomMessageBox.Show("유효하지 않은 설정 파일입니다.", "오류"); return; } // 유효한 설정인지 최종 확인 var settings = System.Text.Json.JsonSerializer.Deserialize(json); if (settings == null) { CustomMessageBox.Show("설정 파일을 파싱할 수 없습니다.", "오류"); return; } // 암호화하여 저장 var encrypted = CryptoService.PortableEncrypt(json); System.IO.File.WriteAllText(SettingsService.SettingsPath, encrypted); CustomMessageBox.Show( "설정이 불러와졌습니다.\n변경 사항을 적용하려면 앱을 재시작하세요.", "AX Copilot", MessageBoxButton.OK, MessageBoxImage.Information); Close(); } catch (Exception ex) { CustomMessageBox.Show($"불러오기 실패: {ex.Message}", "오류"); } } protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { // 저장하지 않고 닫아도 확인 없이 바로 닫힘 (revert는 OnClosed에서 처리) base.OnClosing(e); } protected override void OnClosed(EventArgs e) { if (!_saved) _revertCallback(); base.OnClosed(e); } }