Files
AX-Copilot/src/AxCopilot/Views/LauncherWindow.Shell.cs
lacvet d6d5f518d0 [Phase L2-4/L2-5] 클립보드 OCR 이미지 검색 + Ctrl+Click 다중 선택
Phase L2-4: 클립보드 이미지 OCR 텍스트 추출 및 검색
- AxCopilot.csproj: TFM net8.0-windows → net8.0-windows10.0.17763.0 (Windows OCR API 활성화)
- ClipboardEntry: OcrText 프로퍼티 추가 (set), Preview → OCR 텍스트 우선 표시 (72자 상한)
- SavedClipEntry: OcrText 직렬화 필드 추가, BuildSnapshot/LoadHistory 연동
- ClipboardHistoryService.OnClipboardUpdate: 이미지 저장 후 백그라운드 OCR 트리거
  (EnableOcrSearch 설정 체크, capturedEntry.OcrText 비동기 갱신)
- ClipboardHistoryService.ImageCache.cs: ExtractOcrTextAsync() 추가
  (WinRT BitmapDecoder → SoftwareBitmap → OcrEngine.RecognizeAsync, 5,000자 상한)
  WinRT 별칭(WinBitmapDecoder, WinSoftwareBitmap 등) 으로 WPF 네임스페이스 충돌 방지
- AppSettings.Models.cs: ClipboardHistorySettings.EnableOcrSearch (default=true)
- ClipboardHistoryHandler.GetItemsAsync: OcrText 포함 검색, 'OCR ·' 표시 배지

Phase L2-5: Ctrl+Click 클립보드 항목 다중 선택
- LauncherWindow.Shell.cs: ResultList_PreviewMouseLeftButtonUp에 Ctrl+Click 분기 추가
  (IsClipboardMode + Ctrl 조합 시 ToggleMergeItem 호출, 기존 단일 선택 흐름 유지)
- LauncherWindow.ShortcutHelp.cs: Ctrl+Click / Shift+↑↓ / 병합 단축키 도움말 추가

빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 10:18:45 +09:00

186 lines
7.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using AxCopilot.Services;
namespace AxCopilot.Views;
public partial class LauncherWindow
{
// ─── Shell32 휴지통 삭제 ────────────────────────────────────────────────
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct SHFILEOPSTRUCT
{
public IntPtr hwnd;
public uint wFunc;
[MarshalAs(UnmanagedType.LPWStr)] public string pFrom;
[MarshalAs(UnmanagedType.LPWStr)] public string? pTo;
public ushort fFlags;
[MarshalAs(UnmanagedType.Bool)] public bool fAnyOperationsAborted;
public IntPtr hNameMappings;
[MarshalAs(UnmanagedType.LPWStr)] public string? lpszProgressTitle;
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern int SHFileOperation(ref SHFILEOPSTRUCT lpFileOp);
private const uint FO_DELETE = 0x0003;
private const ushort FOF_ALLOWUNDO = 0x0040;
private const ushort FOF_NOCONFIRMATION = 0x0010;
private const ushort FOF_SILENT = 0x0004;
/// <summary>파일·폴더를 Windows 휴지통으로 보냅니다.</summary>
private void SendToRecycleBin(string path)
{
// pFrom은 null-terminated + 추가 null 필요
var op = new SHFILEOPSTRUCT
{
hwnd = new WindowInteropHelper(this).Handle,
wFunc = FO_DELETE,
pFrom = path + '\0',
fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT,
};
int result = SHFileOperation(ref op);
if (result != 0)
throw new System.ComponentModel.Win32Exception(result, $"SHFileOperation 실패 (코드 {result})");
}
// ─── 대형 텍스트 / 클립보드 외부 뷰어 ──────────────────────────────────
private void ShowLargeType()
{
// 클립보드 항목 → 시스템 클립보드에 자동 복사 + 외부 앱에서 열기
if (_vm.SelectedItem?.Data is Services.ClipboardEntry clipEntry)
{
try
{
// 자동 클립보드 복사 억제 (히스토리 중복 방지)
CurrentApp?.ClipboardHistoryService?.SuppressNextCapture();
if (!clipEntry.IsText && clipEntry.Image != null)
{
// 원본 이미지가 있으면 원본 사용, 없으면 썸네일 사용
var originalImg = Services.ClipboardHistoryService.LoadOriginalImage(clipEntry.OriginalImagePath);
var imgToUse = originalImg ?? clipEntry.Image;
// 시스템 클립보드에 원본 복사
Clipboard.SetImage(imgToUse);
// 이미지: PNG로 저장 → 기본 이미지 뷰어
string path;
if (!string.IsNullOrEmpty(clipEntry.OriginalImagePath) &&
System.IO.File.Exists(clipEntry.OriginalImagePath))
{
path = clipEntry.OriginalImagePath; // 원본 파일 직접 열기
}
else
{
path = Services.TempFileService.CreateTempFile("clip_image", ".png");
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(imgToUse));
using var fs = new System.IO.FileStream(path, System.IO.FileMode.Create);
encoder.Save(fs);
}
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true });
}
else if (!string.IsNullOrEmpty(clipEntry.Text))
{
// 시스템 클립보드에 텍스트 복사
Clipboard.SetText(clipEntry.Text);
// 텍스트: txt로 저장 → 메모장
var path = Services.TempFileService.CreateTempFile("clip_text", ".txt");
System.IO.File.WriteAllText(path, clipEntry.Text, System.Text.Encoding.UTF8);
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("notepad.exe", $"\"{path}\"") { UseShellExecute = true });
}
}
catch (Exception ex)
{
Services.LogService.Warn($"클립보드 외부 뷰어 실패: {ex.Message}");
}
return;
}
var text = _vm.GetLargeTypeText();
if (string.IsNullOrWhiteSpace(text)) return;
new LargeTypeWindow(text).Show();
}
// ─── 마우스 클릭 처리 ───────────────────────────────────────────────────
/// <summary>이미 선택된 아이템을 클릭하면 Execute, 아직 선택되지 않은 아이템 클릭은 선택만.</summary>
private SDK.LauncherItem? _lastClickedItem;
private DateTime _lastClickTime;
private void ResultList_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// 클릭한 ListViewItem 찾기
var dep = e.OriginalSource as DependencyObject;
while (dep != null && dep is not System.Windows.Controls.ListViewItem)
dep = System.Windows.Media.VisualTreeHelper.GetParent(dep);
if (dep is not System.Windows.Controls.ListViewItem lvi) return;
var clickedItem = lvi.Content as SDK.LauncherItem;
if (clickedItem == null) return;
// Phase L2-5: Ctrl+Click → 클립보드 히스토리 모드에서 다중 선택 토글
if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 && _vm.IsClipboardMode)
{
_vm.ToggleMergeItem(clickedItem);
e.Handled = true;
return;
}
var now = DateTime.UtcNow;
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
if (_lastClickedItem == clickedItem && timeSinceLastClick < 600)
{
// 같은 아이템을 짧은 간격으로 재클릭 → 액션 모드 또는 실행
if (!_vm.IsActionMode && _vm.CanEnterActionMode())
{
_vm.EnterActionMode(clickedItem);
e.Handled = true;
}
else
{
_ = _vm.ExecuteSelectedAsync();
e.Handled = true;
}
_lastClickedItem = null;
return;
}
// 첫 번째 클릭 → 선택만
_lastClickedItem = clickedItem;
_lastClickTime = now;
}
private void ResultList_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
_ = _vm.ExecuteSelectedAsync();
}
// ─── 창 이벤트 / 스크롤 / 알림 ─────────────────────────────────────────
private void Window_Deactivated(object sender, EventArgs e)
{
// 설정 기능 "포커스 잃으면 닫기"가 켜진 경우에만 자동 숨김
if (_vm.CloseOnFocusLost) Hide();
}
private void ScrollToSelected()
{
if (_vm.SelectedItem != null)
ResultList.ScrollIntoView(_vm.SelectedItem);
}
private void ShowNotification(string message)
{
// 시스템 트레이 토스트 알림 표시
// App.xaml.cs의 TrayIcon을 통해 처리
}
}