런처 클립보드 붙여넣기 포커스 복원 경로 통일
Some checks failed
Release Gate / gate (push) Has been cancelled

- 클립보드 히스토리, 클립보드 변환, 순차 붙여넣기 실행 경로에 공통 포커스 복원 helper를 추가했습니다.
- 이전 활성 창 복원, 최소 대기, Ctrl+V 주입 순서를 하나로 맞춰 포커스 누락으로 내용이 원래 창에 들어가지 않던 문제를 완화했습니다.
- 관련 변경 이력을 README와 DEVELOPMENT 문서에 반영했습니다.

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify\ -p:IntermediateOutputPath=obj\verify\
- 경고 0 / 오류 0
This commit is contained in:
2026-04-05 20:19:44 +09:00
parent db957039d4
commit 1778b855c5
6 changed files with 156 additions and 181 deletions

View File

@@ -0,0 +1,126 @@
using System.Runtime.InteropServices;
namespace AxCopilot.Services;
internal static class ForegroundPasteHelper
{
private const int SW_RESTORE = 9;
private const uint INPUT_KEYBOARD = 1;
private const uint KEYEVENTF_KEYUP = 0x0002;
private const ushort VK_CONTROL = 0x11;
private const ushort VK_V = 0x56;
public static async Task<bool> RestoreWindowAsync(
IntPtr hwnd,
CancellationToken ct,
int initialDelayMs = 220,
int settleDelayMs = 60,
int maxAttempts = 4)
{
if (hwnd == IntPtr.Zero || !IsWindow(hwnd))
return false;
await Task.Delay(initialDelayMs, ct);
if (IsIconic(hwnd))
ShowWindow(hwnd, SW_RESTORE);
for (var attempt = 0; attempt < maxAttempts; attempt++)
{
uint currentThread = GetCurrentThreadId();
uint targetThread = GetWindowThreadProcessId(hwnd, out _);
var foreground = GetForegroundWindow();
uint foregroundThread = foreground != IntPtr.Zero
? GetWindowThreadProcessId(foreground, out _)
: 0;
var attachedTarget = false;
var attachedForeground = false;
try
{
if (targetThread != 0 && targetThread != currentThread)
{
AttachThreadInput(currentThread, targetThread, true);
attachedTarget = true;
}
if (foregroundThread != 0 && foregroundThread != currentThread && foregroundThread != targetThread)
{
AttachThreadInput(currentThread, foregroundThread, true);
attachedForeground = true;
}
BringWindowToTop(hwnd);
SetForegroundWindow(hwnd);
}
finally
{
if (attachedForeground)
AttachThreadInput(currentThread, foregroundThread, false);
if (attachedTarget)
AttachThreadInput(currentThread, targetThread, false);
}
await Task.Delay(settleDelayMs, ct);
if (GetForegroundWindow() == hwnd)
return true;
}
return GetForegroundWindow() == hwnd;
}
public static async Task<bool> PasteClipboardAsync(
IntPtr hwnd,
CancellationToken ct,
int initialDelayMs = 220)
{
var restored = await RestoreWindowAsync(hwnd, ct, initialDelayMs);
if (!restored)
return false;
await Task.Delay(40, ct);
SendCtrlV();
return true;
}
private static void SendCtrlV()
{
var inputs = new INPUT[4];
inputs[0] = new INPUT { Type = INPUT_KEYBOARD, Ki = new KEYBDINPUT { wVk = VK_CONTROL } };
inputs[1] = new INPUT { Type = INPUT_KEYBOARD, Ki = new KEYBDINPUT { wVk = VK_V } };
inputs[2] = new INPUT { Type = INPUT_KEYBOARD, Ki = new KEYBDINPUT { wVk = VK_V, dwFlags = KEYEVENTF_KEYUP } };
inputs[3] = new INPUT { Type = INPUT_KEYBOARD, Ki = new KEYBDINPUT { wVk = VK_CONTROL, dwFlags = KEYEVENTF_KEYUP } };
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
}
[StructLayout(LayoutKind.Explicit, Size = 40)]
private struct INPUT
{
[FieldOffset(0)] public uint Type;
[FieldOffset(8)] public KEYBDINPUT Ki;
}
[StructLayout(LayoutKind.Sequential)]
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] private static extern bool BringWindowToTop(IntPtr hWnd);
[DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
[DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] private static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll")] private static extern bool IsIconic(IntPtr hWnd);
[DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")] private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize);
}