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>
186 lines
7.7 KiB
C#
186 lines
7.7 KiB
C#
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을 통해 처리
|
||
}
|
||
}
|