using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using AxCopilot.Models; using AxCopilot.Services; namespace AxCopilot.Core; /// /// Windows 창의 HWND, Rect, 프로세스 경로를 수집하고 복원합니다 (The Shifter). /// public class ContextManager { private readonly SettingsService _settings; public ContextManager(SettingsService settings) { _settings = settings; // 모니터 구성 변경 감지는 MainWindow의 WndProc에서 WM_DISPLAYCHANGE를 통해 처리 } // ─── Snapshot ──────────────────────────────────────────────────────────── /// /// 현재 화면에 열린 모든 업무용 창을 캡처하여 프로필로 저장합니다. /// public WorkspaceProfile CaptureProfile(string name) { var snapshots = new List(); var monitorMap = BuildMonitorMap(); EnumWindows((hWnd, _) => { if (!IsWindowVisible(hWnd)) return true; if (IsIconic(hWnd)) return true; // 최소화된 창 제외 여부는 설정으로 조정 가능 var title = GetWindowTitle(hWnd); if (string.IsNullOrWhiteSpace(title)) return true; // 작업표시줄, 바탕화면 등 시스템 창 제외 if (IsSystemWindow(hWnd)) return true; var exePath = GetProcessPath(hWnd); if (string.IsNullOrEmpty(exePath)) return true; GetWindowRect(hWnd, out RECT rect); GetWindowPlacement(hWnd, out WINDOWPLACEMENT placement); var showCmd = placement.showCmd switch { 1 => "Normal", 2 => "Minimized", 3 => "Maximized", _ => "Normal" }; int monitorIndex = GetMonitorIndex(hWnd, monitorMap); snapshots.Add(new WindowSnapshot { Exe = exePath, Title = title, Rect = new WindowRect { X = rect.Left, Y = rect.Top, Width = rect.Right - rect.Left, Height = rect.Bottom - rect.Top }, ShowCmd = showCmd, Monitor = monitorIndex }); return true; }, IntPtr.Zero); var profile = new WorkspaceProfile { Name = name, Windows = snapshots, CreatedAt = DateTime.Now }; // settings.json에 저장 var existing = _settings.Settings.Profiles.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (existing != null) _settings.Settings.Profiles.Remove(existing); _settings.Settings.Profiles.Add(profile); _settings.Save(); LogService.Info($"프로필 '{name}' 저장 완료: {snapshots.Count}개 창"); return profile; } // ─── Restore ───────────────────────────────────────────────────────────── /// /// 저장된 프로필을 복원합니다. /// public async Task RestoreProfileAsync(string name, CancellationToken ct = default) { var profile = _settings.Settings.Profiles .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (profile == null) return new RestoreResult(false, $"프로필 '{name}'을 찾을 수 없습니다."); var results = new List(); var monitorCount = GetMonitorCount(); foreach (var snapshot in profile.Windows) { ct.ThrowIfCancellationRequested(); // 1. 실행 중인 창 찾기 var hWnd = FindMatchingWindow(snapshot); // 2. 창이 없으면 EXE 실행 후 대기 if (hWnd == IntPtr.Zero && File.Exists(snapshot.Exe)) { try { Process.Start(new ProcessStartInfo(snapshot.Exe) { UseShellExecute = true }); hWnd = await WaitForWindowAsync(snapshot.Exe, TimeSpan.FromSeconds(3), ct); } catch (Exception ex) { results.Add($"⚠ {snapshot.Title}: 실행 실패 ({ex.Message})"); LogService.Warn($"앱 실행 실패: {snapshot.Exe} - {ex.Message}"); continue; } } if (hWnd == IntPtr.Zero) { results.Add($"⏭ {snapshot.Title}: 창 없음, 건너뜀"); continue; } // 3. 모니터 불일치 처리 if (snapshot.Monitor >= monitorCount) { var policy = _settings.Settings.MonitorMismatch; if (policy == "skip") { results.Add($"⏭ {snapshot.Title}: 모니터 불일치, 건너뜀"); continue; } // "fit" 또는 "warn" → 첫 번째 모니터에 배치 } // 4. 창 위치/크기 복원 try { ShowWindow(hWnd, snapshot.ShowCmd switch { "Maximized" => 3, "Minimized" => 2, _ => 9 // SW_RESTORE }); if (snapshot.ShowCmd == "Normal") { SetWindowPos(hWnd, IntPtr.Zero, snapshot.Rect.X, snapshot.Rect.Y, snapshot.Rect.Width, snapshot.Rect.Height, SWP_NOZORDER | SWP_NOACTIVATE); } results.Add($"✓ {snapshot.Title}: 복원 완료"); } catch (Exception ex) { results.Add($"⚠ {snapshot.Title}: 복원 실패 ({ex.Message})"); LogService.Warn($"창 복원 실패 (권한 문제 가능): {snapshot.Exe}"); } } LogService.Info($"프로필 '{name}' 복원: {string.Join(", ", results)}"); return new RestoreResult(true, string.Join("\n", results)); } // ─── Profile Management ────────────────────────────────────────────────── public bool DeleteProfile(string name) { var profile = _settings.Settings.Profiles .FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (profile == null) return false; _settings.Settings.Profiles.Remove(profile); _settings.Save(); return true; } public bool RenameProfile(string oldName, string newName) { var profile = _settings.Settings.Profiles .FirstOrDefault(p => p.Name.Equals(oldName, StringComparison.OrdinalIgnoreCase)); if (profile == null) return false; profile.Name = newName; _settings.Save(); return true; } // ─── Helpers ───────────────────────────────────────────────────────────── private static IntPtr FindMatchingWindow(WindowSnapshot snapshot) { IntPtr found = IntPtr.Zero; EnumWindows((hWnd, _) => { var path = GetProcessPath(hWnd); if (string.Equals(path, snapshot.Exe, StringComparison.OrdinalIgnoreCase)) { found = hWnd; return false; // 첫 번째 매칭 창에서 중단 } return true; }, IntPtr.Zero); return found; } private static async Task WaitForWindowAsync(string exePath, TimeSpan timeout, CancellationToken ct) { var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { ct.ThrowIfCancellationRequested(); var hWnd = FindMatchingWindow(new WindowSnapshot { Exe = exePath }); if (hWnd != IntPtr.Zero) return hWnd; await Task.Delay(200, ct); } return IntPtr.Zero; } private static string GetWindowTitle(IntPtr hWnd) { var sb = new StringBuilder(256); GetWindowText(hWnd, sb, sb.Capacity); return sb.ToString(); } private static string GetProcessPath(IntPtr hWnd) { try { GetWindowThreadProcessId(hWnd, out uint pid); if (pid == 0) return ""; var proc = Process.GetProcessById((int)pid); return proc.MainModule?.FileName ?? ""; } catch (Exception) { return ""; } } private static bool IsSystemWindow(IntPtr hWnd) { var cls = new StringBuilder(256); GetClassName(hWnd, cls, cls.Capacity); var name = cls.ToString(); return name is "Shell_TrayWnd" or "Progman" or "WorkerW" or "DV2ControlHost"; } private static Dictionary BuildMonitorMap() { var monitors = new List(); EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprc, IntPtr dwData) => { monitors.Add(hMonitor); return true; }, IntPtr.Zero); return monitors .Select((hm, idx) => (hm, idx)) .ToDictionary(t => t.hm, t => t.idx); } private static int GetMonitorIndex(IntPtr hWnd, Dictionary map) { var monitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST); return map.TryGetValue(monitor, out int idx) ? idx : 0; } private static int GetMonitorCount() { int count = 0; EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprc, IntPtr dwData) => { count++; return true; }, IntPtr.Zero); return count; } // ─── P/Invoke ──────────────────────────────────────────────────────────── private const uint SWP_NOZORDER = 0x0004; private const uint SWP_NOACTIVATE = 0x0010; private const uint MONITOR_DEFAULTTONEAREST = 0x00000002; [StructLayout(LayoutKind.Sequential)] private struct RECT { public int Left, Top, Right, Bottom; } [StructLayout(LayoutKind.Sequential)] private struct WINDOWPLACEMENT { public uint length, flags; public uint showCmd; public POINT ptMinPosition, ptMaxPosition; public RECT rcNormalPosition; } [StructLayout(LayoutKind.Sequential)] private struct POINT { public int x, y; } private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); private delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); [DllImport("user32.dll")] private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); [DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool IsIconic(IntPtr hWnd); [DllImport("user32.dll")] private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); [DllImport("user32.dll")] private static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl); [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); [DllImport("user32.dll")] private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); [DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags); [DllImport("user32.dll")] private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); } public record RestoreResult(bool Success, string Message);