Initial commit to new repository
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// 창 스냅/레이아웃 핸들러. "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 → 이전 크기/위치로 복원
|
||||
/// </summary>
|
||||
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<IEnumerable<LauncherItem>> 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<LauncherItem>();
|
||||
|
||||
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<IEnumerable<LauncherItem>>(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<MONITORINFO>() };
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user