using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using AxCopilot.SDK; using AxCopilot.Services; namespace AxCopilot.Handlers; public class SnapHandler : IActionHandler { private struct RECT { public int left; public int top; public int right; public int bottom; } private struct MONITORINFO { public int cbSize; public RECT rcMonitor; public RECT rcWork; public uint dwFlags; } private const uint SWP_SHOWWINDOW = 64u; private const uint SWP_NOZORDER = 4u; private const uint MONITOR_DEFAULTTONEAREST = 2u; private const int SW_RESTORE = 9; private const int SW_MAXIMIZE = 3; private static readonly (string Key, string Label, string Desc)[] _snapOptions = new(string, string, string)[20] { ("left", "왼쪽 절반", "화면 왼쪽 50% 영역에 배치"), ("right", "오른쪽 절반", "화면 오른쪽 50% 영역에 배치"), ("top", "위쪽 절반", "화면 위쪽 50% 영역에 배치"), ("bottom", "아래쪽 절반", "화면 아래쪽 50% 영역에 배치"), ("tl", "좌상단 1/4", "화면 좌상단 25% 영역에 배치"), ("tr", "우상단 1/4", "화면 우상단 25% 영역에 배치"), ("bl", "좌하단 1/4", "화면 좌하단 25% 영역에 배치"), ("br", "우하단 1/4", "화면 우하단 25% 영역에 배치"), ("l-rt", "좌반 + 우상", "왼쪽 50% + 오른쪽 상단 25% (2창용)"), ("l-rb", "좌반 + 우하", "왼쪽 50% + 오른쪽 하단 25% (2창용)"), ("r-lt", "우반 + 좌상", "오른쪽 50% + 왼쪽 상단 25% (2창용)"), ("r-lb", "우반 + 좌하", "오른쪽 50% + 왼쪽 하단 25% (2창용)"), ("third-l", "좌측 1/3", "화면 왼쪽 33% 영역에 배치"), ("third-c", "중앙 1/3", "화면 가운데 33% 영역에 배치"), ("third-r", "우측 1/3", "화면 오른쪽 33% 영역에 배치"), ("two3-l", "좌측 2/3", "화면 왼쪽 66% 영역에 배치"), ("two3-r", "우측 2/3", "화면 오른쪽 66% 영역에 배치"), ("full", "전체 화면", "최대화"), ("center", "화면 중앙", "화면 중앙 80% 크기로 배치"), ("restore", "원래 크기 복원", "창을 이전 크기로 복원") }; public string? Prefix => "snap"; public PluginMetadata Metadata => new PluginMetadata("WindowSnap", "창 배치 — 2/3/4분할, 1/3·2/3, 전체화면, 중앙, 복원", "1.1", "AX"); [DllImport("user32.dll")] private static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); [DllImport("user32.dll")] private static extern bool ShowWindow(nint hWnd, int nCmdShow); [DllImport("user32.dll")] private static extern nint MonitorFromWindow(nint hwnd, uint dwFlags); [DllImport("user32.dll")] private static extern bool GetMonitorInfo(nint hMonitor, ref MONITORINFO lpmi); [DllImport("user32.dll")] private static extern bool IsWindow(nint hWnd); [DllImport("user32.dll")] private static extern bool IsIconic(nint hWnd); public Task> GetItemsAsync(string query, CancellationToken ct) { string q = query.Trim().ToLowerInvariant(); nint previousWindow = WindowTracker.PreviousWindow; bool flag = previousWindow != IntPtr.Zero && IsWindow(previousWindow); string hint = (flag ? "Enter로 현재 활성 창에 적용" : "대상 창 없음 — 런처를 열기 전 창에 적용됩니다"); IEnumerable<(string, string, string)> enumerable; if (!string.IsNullOrWhiteSpace(q)) { enumerable = _snapOptions.Where(((string Key, string Label, string Desc) o) => o.Key.StartsWith(q) || o.Label.Contains(q)); } else { IEnumerable<(string, string, string)> snapOptions = _snapOptions; enumerable = snapOptions; } IEnumerable<(string, string, string)> source = enumerable; List list = source.Select<(string, string, string), LauncherItem>(((string Key, string Label, string Desc) o) => new LauncherItem(o.Label, o.Desc + " · " + hint, null, o.Key, null, "\ue8a0")).ToList(); if (!list.Any()) { list.Add(new LauncherItem("알 수 없는 스냅 방향: " + q, "left / right / tl / tr / bl / br / third-l/c/r / two3-l/r / full / center / restore", null, null, null, "\ue7ba")); } return Task.FromResult((IEnumerable)list); } public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) { object data = item.Data; if (!(data is string snapKey)) { return; } nint hwnd = WindowTracker.PreviousWindow; if (hwnd != IntPtr.Zero && IsWindow(hwnd)) { if (IsIconic(hwnd)) { ShowWindow(hwnd, 9); } await Task.Delay(80, ct); ApplySnap(hwnd, snapKey); } } private static void ApplySnap(nint hwnd, string key) { nint hMonitor = MonitorFromWindow(hwnd, 2u); MONITORINFO lpmi = new MONITORINFO { cbSize = Marshal.SizeOf() }; if (!GetMonitorInfo(hMonitor, ref lpmi)) { return; } RECT rcWork = lpmi.rcWork; int num = rcWork.right - rcWork.left; int num2 = rcWork.bottom - rcWork.top; int left = rcWork.left; int top = rcWork.top; if (key == "full") { ShowWindow(hwnd, 3); return; } if (key == "restore") { ShowWindow(hwnd, 9); return; } if (1 == 0) { } (int, int, int, int) tuple = key switch { "left" => (left, top, num / 2, num2), "right" => (left + num / 2, top, num / 2, num2), "top" => (left, top, num, num2 / 2), "bottom" => (left, top + num2 / 2, num, num2 / 2), "tl" => (left, top, num / 2, num2 / 2), "tr" => (left + num / 2, top, num / 2, num2 / 2), "bl" => (left, top + num2 / 2, num / 2, num2 / 2), "br" => (left + num / 2, top + num2 / 2, num / 2, num2 / 2), "l-rt" => (left + num / 2, top, num / 2, num2 / 2), "l-rb" => (left + num / 2, top + num2 / 2, num / 2, num2 / 2), "r-lt" => (left, top, num / 2, num2 / 2), "r-lb" => (left, top + num2 / 2, num / 2, num2 / 2), "third-l" => (left, top, num / 3, num2), "third-c" => (left + num / 3, top, num / 3, num2), "third-r" => (left + num * 2 / 3, top, num / 3, num2), "two3-l" => (left, top, num * 2 / 3, num2), "two3-r" => (left + num / 3, top, num * 2 / 3, num2), "center" => (left + num / 10, top + num2 / 10, num * 8 / 10, num2 * 8 / 10), _ => (left, top, num, num2), }; if (1 == 0) { } var (x, y, cx, cy) = tuple; ShowWindow(hwnd, 9); SetWindowPos(hwnd, IntPtr.Zero, x, y, cx, cy, 68u); } }