using System.Runtime.InteropServices; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// 창 스냅/레이아웃 핸들러. "snap" 프리픽스로 사용합니다. /// 런처 호출 직전의 활성 창을 대상으로 배치합니다. /// /// 예: snap left → 화면 왼쪽 절반 /// snap right → 화면 오른쪽 절반 /// snap top → 화면 위쪽 절반 /// snap bottom → 화면 아래쪽 절반 /// snap full → 전체 화면 (최대화) /// snap tl → 좌상단 1/4 /// snap tr → 우상단 1/4 /// snap bl → 좌하단 1/4 /// snap br → 우하단 1/4 /// snap center → 화면 중앙 (80% 크기) /// snap restore → 이전 크기/위치로 복원 /// public class SnapHandler : IActionHandler { public string? Prefix => "snap"; public PluginMetadata Metadata => new( "WindowSnap", "창 배치 — 2/3/4분할, 1/3·2/3, 전체화면, 중앙, 복원", "1.1", "AX"); // ─── P/Invoke ────────────────────────────────────────────────────────────── [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 GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi); [DllImport("user32.dll")] private static extern bool IsWindow(IntPtr hWnd); [DllImport("user32.dll")] private static extern bool IsIconic(IntPtr hWnd); [StructLayout(LayoutKind.Sequential)] private struct RECT { public int left, top, right, bottom; } [StructLayout(LayoutKind.Sequential)] private struct MONITORINFO { public int cbSize; public RECT rcMonitor; public RECT rcWork; // 작업표시줄 제외 영역 public uint dwFlags; } private const uint SWP_SHOWWINDOW = 0x0040; private const uint SWP_NOZORDER = 0x0004; private const uint MONITOR_DEFAULTTONEAREST = 0x00000002; private const int SW_RESTORE = 9; private const int SW_MAXIMIZE = 3; private static readonly (string Key, string Label, string Desc)[] _snapOptions = [ // ── 2분할 ── ("left", "왼쪽 절반", "화면 왼쪽 50% 영역에 배치"), ("right", "오른쪽 절반", "화면 오른쪽 50% 영역에 배치"), ("top", "위쪽 절반", "화면 위쪽 50% 영역에 배치"), ("bottom", "아래쪽 절반", "화면 아래쪽 50% 영역에 배치"), // ── 4분할 ── ("tl", "좌상단 1/4", "화면 좌상단 25% 영역에 배치"), ("tr", "우상단 1/4", "화면 우상단 25% 영역에 배치"), ("bl", "좌하단 1/4", "화면 좌하단 25% 영역에 배치"), ("br", "우하단 1/4", "화면 우하단 25% 영역에 배치"), // ── 3분할 (좌 50% + 우 상하) ── ("l-rt", "좌반 + 우상", "왼쪽 50% + 오른쪽 상단 25% (2창용)"), ("l-rb", "좌반 + 우하", "왼쪽 50% + 오른쪽 하단 25% (2창용)"), ("r-lt", "우반 + 좌상", "오른쪽 50% + 왼쪽 상단 25% (2창용)"), ("r-lb", "우반 + 좌하", "오른쪽 50% + 왼쪽 하단 25% (2창용)"), // ── 3등분 (가로) ── ("third-l", "좌측 1/3", "화면 왼쪽 33% 영역에 배치"), ("third-c", "중앙 1/3", "화면 가운데 33% 영역에 배치"), ("third-r", "우측 1/3", "화면 오른쪽 33% 영역에 배치"), // ── 2/3 + 1/3 ── ("two3-l", "좌측 2/3", "화면 왼쪽 66% 영역에 배치"), ("two3-r", "우측 2/3", "화면 오른쪽 66% 영역에 배치"), // ── 기타 ── ("full", "전체 화면", "최대화"), ("center", "화면 중앙", "화면 중앙 80% 크기로 배치"), ("restore", "원래 크기 복원", "창을 이전 크기로 복원"), ]; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim().ToLowerInvariant(); var hwnd = WindowTracker.PreviousWindow; var windowValid = hwnd != IntPtr.Zero && IsWindow(hwnd); var hint = windowValid ? "Enter로 현재 활성 창에 적용" : "대상 창 없음 — 런처를 열기 전 창에 적용됩니다"; IEnumerable<(string Key, string Label, string Desc)> options = string.IsNullOrWhiteSpace(q) ? _snapOptions : _snapOptions.Where(o => o.Key.StartsWith(q) || o.Label.Contains(q)); var items = options.Select(o => new LauncherItem( o.Label, $"{o.Desc} · {hint}", null, o.Key, Symbol: Symbols.SnapLayout)).ToList(); if (!items.Any()) items.Add(new LauncherItem( $"알 수 없는 스냅 방향: {q}", "left / right / tl / tr / bl / br / third-l/c/r / two3-l/r / full / center / restore", null, null, Symbol: Symbols.Warning)); return Task.FromResult>(items); } public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is not string snapKey) return; var hwnd = WindowTracker.PreviousWindow; if (hwnd == IntPtr.Zero || !IsWindow(hwnd)) return; // 최대화/아이콘화 상태면 먼저 복원 if (IsIconic(hwnd)) ShowWindow(hwnd, SW_RESTORE); // 잠시 대기 (런처 닫힘 애니메이션 후 적용) await Task.Delay(80, ct); ApplySnap(hwnd, snapKey); } private static void ApplySnap(IntPtr hwnd, string key) { // 창이 속한 모니터의 작업 영역 가져오기 var hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); var mi = new MONITORINFO { cbSize = Marshal.SizeOf() }; if (!GetMonitorInfo(hMonitor, ref mi)) return; var w = mi.rcWork; int mw = w.right - w.left; int mh = w.bottom - w.top; int mx = w.left; int my = w.top; if (key == "full") { ShowWindow(hwnd, SW_MAXIMIZE); return; } if (key == "restore") { ShowWindow(hwnd, SW_RESTORE); return; } var (x, y, cw, ch) = key switch { // 2분할 "left" => (mx, my, mw / 2, mh), "right" => (mx + mw / 2, my, mw / 2, mh), "top" => (mx, my, mw, mh / 2), "bottom" => (mx, my + mh / 2, mw, mh / 2), // 4분할 "tl" => (mx, my, mw / 2, mh / 2), "tr" => (mx + mw / 2, my, mw / 2, mh / 2), "bl" => (mx, my + mh / 2, mw / 2, mh / 2), "br" => (mx + mw / 2, my + mh / 2, mw / 2, mh / 2), // 3분할 (좌반 + 우상/우하) "l-rt" => (mx + mw / 2, my, mw / 2, mh / 2), "l-rb" => (mx + mw / 2, my + mh / 2, mw / 2, mh / 2), "r-lt" => (mx, my, mw / 2, mh / 2), "r-lb" => (mx, my + mh / 2, mw / 2, mh / 2), // 3등분 "third-l" => (mx, my, mw / 3, mh), "third-c" => (mx + mw / 3, my, mw / 3, mh), "third-r" => (mx + mw * 2 / 3, my, mw / 3, mh), // 2/3 + 1/3 "two3-l" => (mx, my, mw * 2 / 3, mh), "two3-r" => (mx + mw / 3, my, mw * 2 / 3, mh), // 기타 "center" => (mx + mw / 10, my + mh / 10, mw * 8 / 10, mh * 8 / 10), _ => (mx, my, mw, mh) }; // SW_RESTORE 후 SetWindowPos — 최대화 플래그 해제 필요 ShowWindow(hwnd, SW_RESTORE); SetWindowPos(hwnd, IntPtr.Zero, x, y, cw, ch, SWP_SHOWWINDOW | SWP_NOZORDER); } }