런처 클립보드 붙여넣기 포커스 복원 경로 통일
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

@@ -1,4 +1,3 @@
using System.Runtime.InteropServices;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
@@ -7,13 +6,8 @@ using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L27-6: 클립보드 순차 붙여넣기 핸들러. "paste" 프리픽스로 사용합니다.
///
/// 예: paste → 번호 매긴 클립보드 히스토리 목록
/// paste 3 1 5 → 3번→1번→5번 항목을 순서대로 붙여넣기
/// paste all → 전체 히스토리를 순서대로 붙여넣기
/// Enter → 이전 창에서 순서대로 Ctrl+V 실행.
/// Raycast "Paste Sequentially" 대응.
/// paste prefix 핸들러: 클립보드 히스토리를 순서대로 붙여넣습니다.
/// 예: paste / paste 3 1 5 / paste all
/// </summary>
public class PasteHandler : IActionHandler
{
@@ -34,7 +28,7 @@ public class PasteHandler : IActionHandler
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var q = query.Trim();
var items = new List<LauncherItem>();
var history = _history.History.Where(e => e.IsText && !string.IsNullOrEmpty(e.Text)).ToList();
@@ -47,7 +41,6 @@ public class PasteHandler : IActionHandler
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// ── 번호 시퀀스 파싱 ──────────────────────────────────────────────────
if (!string.IsNullOrWhiteSpace(q) && q != "all")
{
var nums = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
@@ -60,16 +53,15 @@ public class PasteHandler : IActionHandler
if (indices.Count > 0)
{
var preview = string.Join(" ", indices.Select(i => $"#{i}"));
var preview = string.Join(" · ", indices.Select(i => $"#{i}"));
var texts = indices.Select(i => history[i - 1].Text ?? "").ToList();
var totalLen = texts.Sum(t => t.Length);
items.Add(new LauncherItem(
$"순차 붙여넣기: {preview}",
$"{indices.Count}개 항목 · {totalLen}자 · Enter: 순서대로 붙여넣기",
$"{indices.Count}개 항목 · {totalLen}자 · Enter 순서대로 붙여넣기",
null, ("seq", texts), Symbol: Symbols.ClipPaste));
// 미리보기
for (int i = 0; i < indices.Count; i++)
{
var entry = history[indices[i] - 1];
@@ -83,21 +75,19 @@ public class PasteHandler : IActionHandler
}
}
// ── all 명령 ──────────────────────────────────────────────────────────
if (q.Equals("all", StringComparison.OrdinalIgnoreCase))
{
var texts = history.Take(20).Select(e => e.Text ?? "").ToList();
items.Add(new LauncherItem(
$"전체 순차 붙여넣기 ({texts.Count}개)",
$"Enter: 최근 {texts.Count}개 항목을 순서대로 붙여넣기",
$"Enter 최근 {texts.Count}개 항목을 순서대로 붙여넣기",
null, ("seq", texts), Symbol: Symbols.ClipPaste));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// ── 빈 쿼리 → 번호 매긴 목록 ─────────────────────────────────────────
items.Add(new LauncherItem(
"순차 붙여넣기 번호를 입력하세요",
"예: paste 3 1 5 → 3번→1번→5번 순서로 붙여넣기 · paste all → 전체",
"순차 붙여넣기 번호를 입력하세요",
"예: paste 3 1 5 · paste all",
null, null, Symbol: Symbols.ClipPaste));
for (int i = 0; i < Math.Min(history.Count, 15); i++)
@@ -117,12 +107,10 @@ public class PasteHandler : IActionHandler
{
if (item.Data is ("single", string singleText))
{
// 단일 항목 붙여넣기
await PasteTexts([singleText], ct);
}
else if (item.Data is ("seq", List<string> texts))
{
// 순차 붙여넣기
await PasteTexts(texts, ct);
}
}
@@ -138,16 +126,9 @@ public class PasteHandler : IActionHandler
_history.SuppressNextCapture();
// 이전 창 포커스 복원 대기
await Task.Delay(300, ct);
var targetThread = GetWindowThreadProcessId(prevWindow, out _);
var currentThread = GetCurrentThreadId();
AttachThreadInput(currentThread, targetThread, true);
SetForegroundWindow(prevWindow);
AttachThreadInput(currentThread, targetThread, false);
await Task.Delay(100, ct);
var restored = await ForegroundPasteHelper.RestoreWindowAsync(prevWindow, ct, initialDelayMs: 260);
if (!restored)
return;
foreach (var text in texts)
{
@@ -157,8 +138,8 @@ public class PasteHandler : IActionHandler
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
await Task.Delay(50, ct);
SendCtrlV();
await Task.Delay(200, ct); // 항목 간 간격
await ForegroundPasteHelper.PasteClipboardAsync(prevWindow, ct, initialDelayMs: 0);
await Task.Delay(200, ct);
}
NotificationService.Notify("paste", $"{texts.Count}개 항목 붙여넣기 완료");
@@ -172,46 +153,4 @@ public class PasteHandler : IActionHandler
private static string Truncate(string s, int max)
=> s.Length <= max ? s : s[..max] + "…";
// ─── Ctrl+V 주입 (ClipboardHistoryHandler와 동일 패턴) ────────────────────
private static void SendCtrlV()
{
const uint INPUT_KEYBOARD = 1;
const uint KEYEVENTF_KEYUP = 0x0002;
const ushort VK_CONTROL = 0x11;
const ushort VK_V = 0x56;
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>());
}
// ─── P/Invoke ──────────────────────────────────────────────────────────────
[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 uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
[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);
}