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);