From 1778b855c5f52db290a659a74a7df76a0026f7f2 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sun, 5 Apr 2026 20:19:44 +0900 Subject: [PATCH] =?UTF-8?q?=EB=9F=B0=EC=B2=98=20=ED=81=B4=EB=A6=BD?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=B6=99=EC=97=AC=EB=84=A3=EA=B8=B0=20?= =?UTF-8?q?=ED=8F=AC=EC=BB=A4=EC=8A=A4=20=EB=B3=B5=EC=9B=90=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 클립보드 히스토리, 클립보드 변환, 순차 붙여넣기 실행 경로에 공통 포커스 복원 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 --- README.md | 4 +- docs/DEVELOPMENT.md | 2 + src/AxCopilot/Handlers/ClipboardHandler.cs | 29 ++-- .../Handlers/ClipboardHistoryHandler.cs | 89 +------------ src/AxCopilot/Handlers/PasteHandler.cs | 87 ++---------- .../Services/ForegroundPasteHelper.cs | 126 ++++++++++++++++++ 6 files changed, 156 insertions(+), 181 deletions(-) create mode 100644 src/AxCopilot/Services/ForegroundPasteHelper.cs diff --git a/README.md b/README.md index b041376..4f957c6 100644 --- a/README.md +++ b/README.md @@ -1040,4 +1040,6 @@ MIT License - 업데이트: 2026-04-05 22:53 (KST) - 하단 컨텍스트 토큰 라벨이 hover 후 남아 있던 문제를 수정하고, 토큰 심볼/팝업의 흐린 배경·그림자 느낌을 줄여 더 깔끔한 테두리 중심 스타일로 정리했다. - 업데이트: 2026-04-05 22:57 (KST) - - 채팅/코워크 프리셋 카드 hover 시 설명 라벨을 `Collapsed/Visible`로 토글하던 방식을 없애고, 같은 자리에서 `Opacity`만 바꾸도록 조정해 카드가 깜빡이듯 다시 그려지던 현상을 줄였다. +- 채팅/코워크 프리셋 카드 hover 시 설명 라벨을 `Collapsed/Visible`로 토글하던 방식을 없애고, 같은 자리에서 `Opacity`만 바꾸도록 조정해 카드가 깜빡이듯 다시 그려지던 현상을 줄였다. +- 업데이트: 2026-04-05 20:17 (KST) +- 런처의 클립보드 히스토리/클립보드 변환/순차 붙여넣기 실행 경로를 공통 포커스 복원 helper 기반으로 정리했다. 이전 활성 창 복원, 최소 대기, Ctrl+V 주입 순서를 [ForegroundPasteHelper.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ForegroundPasteHelper.cs) 로 통일해 포커스가 원래 창으로 돌아가지 않아 붙여넣기가 누락되던 문제를 줄였다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 0f237a8..46a4871 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4786,3 +4786,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - [ChatWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml), [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 에서 하단 컨텍스트 토큰 심볼과 hover 팝업을 다듬었다. 토큰 카드가 숨겨질 때 팝업도 함께 강제 종료되도록 보강했고, hover 종료 시 실제로 카드/팝업 둘 다 벗어난 경우에만 닫히게 조건을 정리했다. 또 심볼과 팝업은 흐린 배경/강한 그림자 대신 얇은 테두리와 약한 그림자 중심으로 수정했다. - 업데이트: 2026-04-05 22:57 (KST) - [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs) 의 `BuildTopicButtons()`에서 프리셋 카드 hover 설명 라벨을 `Visibility` 토글 대신 `Opacity` 전환 방식으로 바꿨다. 카드 hover 중 설명 라벨이 나타날 때 레이아웃이 다시 잡히며 깜빡이던 현상을 줄이기 위한 보정이다. +- 업데이트: 2026-04-05 20:17 (KST) + - 런처 클립보드 붙여넣기 포커스 경로를 공통 helper 기반으로 재정리했다. [ForegroundPasteHelper.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/ForegroundPasteHelper.cs) 를 추가해 이전 활성 창 복원, 최소 대기, `Ctrl+V` 주입 순서를 하나로 통일했고, [ClipboardHistoryHandler.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Handlers/ClipboardHistoryHandler.cs), [ClipboardHandler.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Handlers/ClipboardHandler.cs), [PasteHandler.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Handlers/PasteHandler.cs) 가 모두 같은 포커스 복원 로직을 사용하도록 맞췄다. diff --git a/src/AxCopilot/Handlers/ClipboardHandler.cs b/src/AxCopilot/Handlers/ClipboardHandler.cs index 95e2afb..0af0abd 100644 --- a/src/AxCopilot/Handlers/ClipboardHandler.cs +++ b/src/AxCopilot/Handlers/ClipboardHandler.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; +using System.Diagnostics; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; @@ -26,7 +25,6 @@ public class ClipboardHandler : IActionHandler { var items = new List(); - // 빌트인 변환 목록 var builtins = GetBuiltinTransformers() .Where(t => string.IsNullOrEmpty(query) || t.Key.Contains(query, StringComparison.OrdinalIgnoreCase)); @@ -34,14 +32,12 @@ public class ClipboardHandler : IActionHandler foreach (var t in builtins) items.Add(new LauncherItem(t.Key, t.Description ?? "", null, t, Symbol: Symbols.Clipboard)); - // 사용자 정의 변환 var custom = _settings.Settings.ClipboardTransformers .Where(t => string.IsNullOrEmpty(query) || t.Key.Contains(query, StringComparison.OrdinalIgnoreCase)) .Select(t => new LauncherItem(t.Key, t.Description ?? t.Type, null, t, Symbol: Symbols.Clipboard)); items.AddRange(custom); - return Task.FromResult>(items); } @@ -49,7 +45,6 @@ public class ClipboardHandler : IActionHandler { if (item.Data is not ClipboardTransformer transformer) return; - // 클립보드에서 텍스트 읽기 (STA 스레드 필요) string? input = null; Application.Current.Dispatcher.Invoke(() => { @@ -61,17 +56,12 @@ public class ClipboardHandler : IActionHandler string? result = await TransformAsync(transformer, input, ct); if (result == null) return; - // 결과를 클립보드에 쓰고 이전 활성 창으로 포커스 복원 후 붙여넣기 Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(result)); - // 런처 호출 전 활성 창으로 포커스 복원 var prevHwnd = WindowTracker.PreviousWindow; - if (prevHwnd != IntPtr.Zero) - SetForegroundWindow(prevHwnd); - - await Task.Delay(120, ct); - System.Windows.Forms.SendKeys.SendWait("^v"); + if (prevHwnd == IntPtr.Zero) return; + await ForegroundPasteHelper.PasteClipboardAsync(prevHwnd, ct, initialDelayMs: 220); LogService.Info($"클립보드 변환: '{transformer.Key}' 적용"); } @@ -81,7 +71,6 @@ public class ClipboardHandler : IActionHandler { return t.Type switch { - // ReDoS 방지: 사용자 정의 패턴에 타임아웃 적용 "regex" when t.Pattern != null && t.Replace != null => Regex.Replace(input, t.Pattern, t.Replace, RegexOptions.None, TimeSpan.FromMilliseconds(t.Timeout > 0 ? t.Timeout : 5000)), @@ -120,8 +109,6 @@ public class ClipboardHandler : IActionHandler return await proc.StandardOutput.ReadToEndAsync(cts.Token); } - // ─── 빌트인 변환 ───────────────────────────────────────────────────────── - internal static string? ExecuteBuiltin(string key, string input) { return key switch @@ -149,7 +136,10 @@ public class ClipboardHandler : IActionHandler var doc = JsonDocument.Parse(input); return JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true }); } - catch { return input; } + catch + { + return input; + } } private static string? TryParseTimestamp(string input) @@ -159,6 +149,7 @@ public class ClipboardHandler : IActionHandler var dt = DateTimeOffset.FromUnixTimeSeconds(ts).LocalDateTime; return dt.ToString("yyyy-MM-dd HH:mm:ss"); } + return null; } @@ -166,15 +157,13 @@ public class ClipboardHandler : IActionHandler { if (DateTime.TryParse(input.Trim(), out var dt)) return new DateTimeOffset(dt).ToUnixTimeSeconds().ToString(); + return null; } private static string StripMarkdown(string input) => Regex.Replace(input, @"(\*\*|__)(.*?)\1|(\*|_)(.*?)\3|`(.+?)`|#{1,6}\s*", "$2$4$5"); - [DllImport("user32.dll")] - private static extern bool SetForegroundWindow(IntPtr hWnd); - private static IEnumerable GetBuiltinTransformers() => [ new() { Key = "$json", Type = "builtin", Description = "JSON 포맷팅 (들여쓰기 적용)" }, diff --git a/src/AxCopilot/Handlers/ClipboardHistoryHandler.cs b/src/AxCopilot/Handlers/ClipboardHistoryHandler.cs index 5e08235..3998edc 100644 --- a/src/AxCopilot/Handlers/ClipboardHistoryHandler.cs +++ b/src/AxCopilot/Handlers/ClipboardHistoryHandler.cs @@ -1,4 +1,3 @@ -using System.Runtime.InteropServices; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; @@ -28,7 +27,6 @@ public class ClipboardHistoryHandler : IActionHandler _historyService = historyService; } - // 카테고리 필터 프리픽스: #url, #코드, #경로 private static readonly Dictionary CategoryFilters = new(StringComparer.OrdinalIgnoreCase) { { "url", "URL" }, { "코드", "코드" }, { "code", "코드" }, @@ -52,7 +50,6 @@ public class ClipboardHistoryHandler : IActionHandler var q = query.Trim().ToLowerInvariant(); - // 카테고리 필터 감지 (예: #url, #핀) string? catFilter = null; foreach (var (prefix, cat) in CategoryFilters) { @@ -66,17 +63,14 @@ public class ClipboardHistoryHandler : IActionHandler var filtered = history.AsEnumerable(); - // 카테고리 필터 적용 if (catFilter == "핀") filtered = filtered.Where(e => e.IsPinned); else if (catFilter != null) filtered = filtered.Where(e => e.Category == catFilter); - // 텍스트 검색 if (!string.IsNullOrEmpty(q)) filtered = filtered.Where(e => e.Preview.ToLowerInvariant().Contains(q)); - // 핀 항목을 상단에 배치 var sorted = filtered .OrderByDescending(e => e.IsPinned) .ThenByDescending(e => e.CopiedAt); @@ -113,14 +107,13 @@ public class ClipboardHistoryHandler : IActionHandler try { _historyService.SuppressNextCapture(); - _historyService.PromoteEntry(entry); // 사용 시각 갱신 + 목록 맨 위로 + _historyService.PromoteEntry(entry); if (!entry.IsText && entry.Image != null) { - // 원본 이미지가 있으면 원본 해상도로 클립보드 복사 var originalImg = ClipboardHistoryService.LoadOriginalImage(entry.OriginalImagePath); Clipboard.SetImage(originalImg ?? entry.Image); - return; // 이미지는 붙여넣기 시뮬레이션 없이 클립보드만 설정 + return; } if (string.IsNullOrEmpty(entry.Text)) return; @@ -129,25 +122,7 @@ public class ClipboardHistoryHandler : IActionHandler var prevWindow = WindowTracker.PreviousWindow; if (prevWindow == IntPtr.Zero) return; - // ── 이전 창 포커스 복원 후 Ctrl+V ──────────────────────────────── - // 런처 창이 완전히 숨겨지고 이전 창이 포커스를 회복할 시간 확보 - await Task.Delay(300, ct); - - // AttachThreadInput으로 포그라운드 전환 권한 획득 후 SetForegroundWindow 호출 - // (Alt 트릭 대비: Alt 키 주입 없이 안정적으로 전환, 대상 앱 메뉴 트리거 방지) - var targetThread = GetWindowThreadProcessId(prevWindow, out _); - var currentThread = GetCurrentThreadId(); - AttachThreadInput(currentThread, targetThread, true); - SetForegroundWindow(prevWindow); - AttachThreadInput(currentThread, targetThread, false); - - // 포커스 전환이 완전히 반영될 때까지 대기 - await Task.Delay(100, ct); - - // SendInput으로 Ctrl+V 주입 - // Win32 INPUT 구조체: type(4) + 패딩(4) + union(32) = 40 bytes on x64 - // KEYBDINPUT.dwExtraInfo = ULONG_PTR → union 8바이트 정렬 필요 → offset 8에서 시작 - SendCtrlV(); + await ForegroundPasteHelper.PasteClipboardAsync(prevWindow, ct, initialDelayMs: 260); } catch (OperationCanceledException) { } catch (Exception ex) @@ -155,62 +130,4 @@ public class ClipboardHistoryHandler : IActionHandler LogService.Warn($"클립보드 히스토리 붙여넣기 실패: {ex.Message}"); } } - - 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]; - - // Ctrl 누름 - inputs[0].Type = INPUT_KEYBOARD; - inputs[0].ki.wVk = VK_CONTROL; - - // V 누름 - inputs[1].Type = INPUT_KEYBOARD; - inputs[1].ki.wVk = VK_V; - - // V 뗌 - inputs[2].Type = INPUT_KEYBOARD; - inputs[2].ki.wVk = VK_V; - inputs[2].ki.dwFlags = KEYEVENTF_KEYUP; - - // Ctrl 뗌 - inputs[3].Type = INPUT_KEYBOARD; - inputs[3].ki.wVk = VK_CONTROL; - inputs[3].ki.dwFlags = KEYEVENTF_KEYUP; - - SendInput((uint)inputs.Length, inputs, Marshal.SizeOf()); - } - - // ─── P/Invoke ────────────────────────────────────────────────────────── - - // Win32 INPUT 구조체 — x64에서 40바이트 - // type(4) + 패딩(4) + union은 offset 8에서 시작 (MOUSEINPUT 32바이트가 최대) - [StructLayout(LayoutKind.Explicit, Size = 40)] - private struct INPUT - { - [FieldOffset(0)] public uint Type; - [FieldOffset(8)] public KEYBDINPUT ki; - } - - // KEYBDINPUT: 2+2+4+4 = 12, dwExtraInfo(IntPtr)는 8바이트 정렬 → 패딩 포함 24바이트 - [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); } diff --git a/src/AxCopilot/Handlers/PasteHandler.cs b/src/AxCopilot/Handlers/PasteHandler.cs index 4c095a8..a7e612e 100644 --- a/src/AxCopilot/Handlers/PasteHandler.cs +++ b/src/AxCopilot/Handlers/PasteHandler.cs @@ -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; /// -/// 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 /// public class PasteHandler : IActionHandler { @@ -34,7 +28,7 @@ public class PasteHandler : IActionHandler public Task> GetItemsAsync(string query, CancellationToken ct) { - var q = query.Trim(); + var q = query.Trim(); var items = new List(); var history = _history.History.Where(e => e.IsText && !string.IsNullOrEmpty(e.Text)).ToList(); @@ -47,7 +41,6 @@ public class PasteHandler : IActionHandler return Task.FromResult>(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>(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 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()); - } - - // ─── 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); } diff --git a/src/AxCopilot/Services/ForegroundPasteHelper.cs b/src/AxCopilot/Services/ForegroundPasteHelper.cs new file mode 100644 index 0000000..5cddd2f --- /dev/null +++ b/src/AxCopilot/Services/ForegroundPasteHelper.cs @@ -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 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 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()); + } + + [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); +}