using System.Windows; using System.Windows.Forms; using AxCopilot.Core; using AxCopilot.Services; using AxCopilot.ViewModels; using AxCopilot.Views; namespace AxCopilot; public partial class App { // ─── 설정창 / ChatWindow ───────────────────────────────────────────── private void SetupTrayIcon(PluginHost pluginHost, SettingsService settings) { _trayIcon = new NotifyIcon { Text = "AX Copilot", Visible = true, Icon = LoadAppIcon() }; // ─── WPF 커스텀 트레이 메뉴 구성 ────────────────────────────────── _trayMenu = new Views.TrayMenuWindow(); _trayMenu .AddItem("\uE7C5", "AX Commander 호출하기", () => Dispatcher.Invoke(() => _launcher?.Show())) .AddItem("\uE8BD", "AX Agent 대화하기", () => Dispatcher.Invoke(() => OpenAiChat()), out var aiTrayItem) .AddItem("\uE8A7", "독 바 표시", () => Dispatcher.Invoke(() => ToggleDockBar())) .AddSeparator() .AddItem("\uE72C", "플러그인 재로드", () => { pluginHost.Reload(); _trayIcon!.ShowBalloonTip(2000, "AX Copilot", "플러그인이 재로드되었습니다.", ToolTipIcon.None); }) .AddItem("\uE838", "로그 폴더 열기", () => { var logDir = System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "logs"); System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("explorer.exe", logDir) { UseShellExecute = true }); }) .AddItem("\uE9D9", "사용 통계", () => Dispatcher.Invoke(() => new StatisticsWindow().Show())) .AddItem("\uE736", "사용 가이드 문서보기", () => { try { Dispatcher.Invoke(() => new Views.GuideViewerWindow().Show()); } catch (Exception ex) { LogService.Error($"사용 가이드 열기 실패: {ex.Message}"); } }) .AddSeparator() .AddToggleItem("\uE82F", "Windows 시작 시 자동 실행", IsAutoStartEnabled(), isChecked => SetAutoStart(isChecked), out _, out _) .AddSeparator() .AddItem("\uE713", "설정", () => Dispatcher.Invoke(OpenSettings)) .AddItem("\uE946", "개발 정보", () => Dispatcher.Invoke(() => new AboutWindow().Show())) .AddItem("\uE711", "종료", () => { _inputListener?.Dispose(); _trayIcon?.Dispose(); Shutdown(); }); // AI 기능 활성화 여부에 따라 메뉴 항목 가시성 동적 업데이트 _trayMenu.Opening += () => { aiTrayItem.Visibility = settings.Settings.AiEnabled ? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed; }; // 우클릭 → WPF 메뉴 표시, 좌클릭 → 런처 토글 _trayIcon.MouseClick += (_, e) => { if (e.Button == System.Windows.Forms.MouseButtons.Left) Dispatcher.Invoke(() => _launcher?.Show()); else if (e.Button == System.Windows.Forms.MouseButtons.Right) Dispatcher.Invoke(() => _trayMenu?.ShowWithUpdate()); }; // 타이머/알람 풍선 알림 서비스 연결 NotificationService.Initialize((title, msg) => { Dispatcher.Invoke(() => _trayIcon?.ShowBalloonTip(4000, title, msg, ToolTipIcon.None)); }); } /// ChatWindow 등 외부에서 설정 창을 여는 공개 메서드. public void OpenSettingsFromChat() => Dispatcher.Invoke(OpenSettings); /// AX Agent 창 열기 (트레이 메뉴 등에서 호출). private Views.ChatWindow? _chatWindow; /// /// ChatWindow를 백그라운드에서 미리 생성합니다 (앱 시작 후 저우선순위로 호출). /// 이후 OpenAiChat() 시 창 생성 비용 없이 즉시 Show/Activate만 수행합니다. /// internal void PrewarmChatWindow() { if (_chatWindow != null || _settings == null) return; _chatWindow = new Views.ChatWindow(_settings); } private void OpenAiChat() { if (_settings == null) return; if (_chatWindow == null) { _chatWindow = new Views.ChatWindow(_settings); } _chatWindow.Show(); _chatWindow.Activate(); } public void ToggleDockBar() { if (_dockBar != null && _dockBar.IsVisible) { _dockBar.Hide(); return; } if (_dockBar == null) { _dockBar = new DockBarWindow(); _dockBar.OnQuickSearch = query => { if (_launcher == null) return; _launcher.Show(); _launcher.Activate(); // 독 바 뒤가 아닌 전면에 표시 if (!string.IsNullOrEmpty(query)) Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, () => _launcher.SetInputText(query)); }; _dockBar.OnCapture = async () => { WindowTracker.Capture(); if (_captureHandler != null) await _captureHandler.CaptureDirectAsync("region"); }; _dockBar.OnOpenAgent = () => { if (_launcher == null) return; _launcher.Show(); Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, () => _launcher.SetInputText("!")); }; } var launcher = _settings?.Settings.Launcher; var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" }; _dockBar.BuildFromSettings(dockItems); // Phase L3-7: 독 바 위치 변경 시 모니터별로 저장 _dockBar.OnPositionChanged = (left, top) => { if (_settings == null) return; var lnch = _settings.Settings.Launcher; lnch.DockBarLeft = left; lnch.DockBarTop = top; // 현재 독 바가 속한 모니터 디바이스 이름으로 위치 기억 var deviceName = GetMonitorDeviceNameAt(_dockBar, left, top); if (deviceName != null) lnch.MonitorDockPositions[deviceName] = new[] { left, top }; _settings.Save(); }; _dockBar.Show(); // Phase L3-7: 저장 위치 유효성 검사 — 연결이 끊긴 모니터면 중앙 하단으로 리셋 var savedLeft = launcher?.DockBarLeft ?? -1; var savedTop = launcher?.DockBarTop ?? -1; if (!IsDockPositionOnAnyMonitor(savedLeft, savedTop)) { LogService.Info("[MultiMonitor] 독 바 저장 위치가 연결된 모니터 밖 → 중앙 하단으로 초기화"); savedLeft = -1; savedTop = -1; } _dockBar.ApplySettings( launcher?.DockBarOpacity ?? 0.92, savedLeft, savedTop, launcher?.DockBarRainbowGlow ?? false); } // ─── Phase L3-7: 다중 모니터 헬퍼 ───────────────────────────────────── /// /// WPF 논리 좌표를 물리적 좌표로 변환하여 해당 모니터의 디바이스 이름을 반환합니다. /// private static string? GetMonitorDeviceNameAt( System.Windows.Media.Visual? visual, double wpfLeft, double wpfTop) { try { System.Drawing.Point physPt; var src = visual != null ? PresentationSource.FromVisual(visual) : null; if (src?.CompositionTarget != null) { var m = src.CompositionTarget.TransformToDevice; // logical→physical var pt = m.Transform(new System.Windows.Point(wpfLeft + 10, wpfTop + 10)); physPt = new System.Drawing.Point((int)pt.X, (int)pt.Y); } else { physPt = new System.Drawing.Point((int)wpfLeft + 10, (int)wpfTop + 10); } return System.Windows.Forms.Screen.FromPoint(physPt).DeviceName; } catch { return null; } } /// /// 저장된 독 바 위치(WPF 논리 좌표)가 현재 연결된 모니터 중 하나에 있는지 검사합니다. /// 모니터가 분리되거나 해상도가 변경되어 위치가 화면 밖에 있으면 false를 반환합니다. /// private static bool IsDockPositionOnAnyMonitor(double left, double top) { if (left < 0 || top < 0) return false; // -1 기본값은 항상 재계산 대상 try { // 참고: WPF 논리 좌표와 물리적 픽셀의 차이는 DPI에 따라 다르나 // "화면 밖인지" 판단에는 근사 비교로 충분합니다. return System.Windows.Forms.Screen.AllScreens .Any(s => left >= s.Bounds.Left && left < s.Bounds.Right && top >= s.Bounds.Top && top < s.Bounds.Bottom); } catch { return true; } // 오류 시 보수적으로 true (위치 유지) } /// 독 바를 현재 설정으로 즉시 새로고침합니다. public void RefreshDockBar() { if (_dockBar == null || !_dockBar.IsVisible) return; var launcher = _settings?.Settings.Launcher; var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" }; _dockBar.BuildFromSettings(dockItems); _dockBar.ApplySettings( launcher?.DockBarOpacity ?? 0.92, launcher?.DockBarLeft ?? -1, launcher?.DockBarTop ?? -1, launcher?.DockBarRainbowGlow ?? false); } private void OpenSettings() { if (_settingsWindow != null && _settingsWindow.IsVisible) { _settingsWindow.Activate(); return; } if (_settings == null || _launcher == null) return; var vm = new ViewModels.SettingsViewModel(_settings); // 미리보기 콜백: 현재 편집 중인 색상(vm.ColorRows)으로 런처에 즉시 반영 void PreviewCallback(string themeKey) { if (themeKey == "custom") { var tempColors = new AxCopilot.Models.CustomThemeColors(); foreach (var row in vm.ColorRows) { var prop = typeof(AxCopilot.Models.CustomThemeColors).GetProperty(row.Property); prop?.SetValue(tempColors, row.Hex); } _launcher.ApplyTheme(themeKey, tempColors); } else { _launcher.ApplyTheme(themeKey, _settings.Settings.Launcher.CustomTheme); } } // 취소/X 닫기 콜백: 파일에 저장된 원본 설정으로 복원 void RevertCallback() { _launcher.ApplyTheme( _settings.Settings.Launcher.Theme ?? "system", _settings.Settings.Launcher.CustomTheme); } _settingsWindow = new Views.SettingsWindow(vm, PreviewCallback, RevertCallback) { // 핫키 녹화 중 글로벌 핫키 일시 정지 SuspendHotkeyCallback = suspend => { if (_inputListener != null) _inputListener.SuspendHotkey = suspend; } }; // 저장 완료 시 InputListener 핫키 갱신 + 알림 타이머 재시작 vm.SaveCompleted += (_, _) => { if (_inputListener != null && _settings != null) { _inputListener.UpdateHotkey(_settings.Settings.Hotkey); _inputListener.UpdateCaptureHotkey( _settings.Settings.ScreenCapture.GlobalHotkey, _settings.Settings.ScreenCapture.GlobalHotkeyEnabled); } _worktimeReminder?.RestartTimer(); }; _settingsWindow.Show(); } }