Files
AX-Copilot-Codex/src/AxCopilot/Handlers/ClipboardHistoryHandler.cs
lacvet 1778b855c5
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
2026-04-05 20:19:44 +09:00

134 lines
4.4 KiB
C#

using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 클립보드 히스토리 핸들러. "#" 프리픽스로 사용합니다.
/// 예: # (빈 쿼리) → 최근 클립보드 목록
/// # hello → "hello"가 포함된 항목 필터
/// </summary>
public class ClipboardHistoryHandler : IActionHandler
{
private readonly ClipboardHistoryService _historyService;
public string? Prefix => "#";
public PluginMetadata Metadata => new(
"ClipboardHistory",
"클립보드 히스토리 — # 뒤에 검색어 (또는 빈 입력으로 전체 보기)",
"1.0",
"AX");
public ClipboardHistoryHandler(ClipboardHistoryService historyService)
{
_historyService = historyService;
}
private static readonly Dictionary<string, string> CategoryFilters = new(StringComparer.OrdinalIgnoreCase)
{
{ "url", "URL" }, { "코드", "코드" }, { "code", "코드" },
{ "경로", "경로" }, { "path", "경로" }, { "핀", "핀" }, { "pin", "핀" },
};
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var history = _historyService.History;
if (history.Count == 0)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"클립보드 히스토리가 없습니다",
"텍스트를 복사하면 이 곳에 기록됩니다",
null, null, Symbol: Symbols.History)
]);
}
var q = query.Trim().ToLowerInvariant();
string? catFilter = null;
foreach (var (prefix, cat) in CategoryFilters)
{
if (q == prefix || q.StartsWith(prefix + " "))
{
catFilter = cat;
q = q.Length > prefix.Length ? q[(prefix.Length + 1)..].Trim() : "";
break;
}
}
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);
var items = sorted
.Select(e =>
{
var pinMark = e.IsPinned ? "\uD83D\uDCCC " : "";
var catMark = e.Category != "일반" ? $"[{e.Category}] " : "";
return new LauncherItem(
$"{pinMark}{e.Preview}",
$"{catMark}{e.RelativeTime} · {e.CopiedAt:MM/dd HH:mm}",
null,
e,
Symbol: e.IsPinned ? Symbols.Favorite : (e.IsText ? Symbols.History : Symbols.Picture));
})
.ToList();
if (items.Count == 0)
{
items.Add(new LauncherItem(
$"'{query}'에 해당하는 항목 없음",
"#pin #url #코드 #경로 로 필터링 가능",
null, null, Symbol: Symbols.History));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not ClipboardEntry entry) return;
try
{
_historyService.SuppressNextCapture();
_historyService.PromoteEntry(entry);
if (!entry.IsText && entry.Image != null)
{
var originalImg = ClipboardHistoryService.LoadOriginalImage(entry.OriginalImagePath);
Clipboard.SetImage(originalImg ?? entry.Image);
return;
}
if (string.IsNullOrEmpty(entry.Text)) return;
Clipboard.SetText(entry.Text);
var prevWindow = WindowTracker.PreviousWindow;
if (prevWindow == IntPtr.Zero) return;
await ForegroundPasteHelper.PasteClipboardAsync(prevWindow, ct, initialDelayMs: 260);
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
LogService.Warn($"클립보드 히스토리 붙여넣기 실패: {ex.Message}");
}
}
}