[Phase L5-2] OCR 화면 텍스트 추출 기능 구현
OcrHandler.cs 신규 생성 (Handlers/, 220줄): - prefix="ocr" — "화면 영역 텍스트 추출" / "클립보드 이미지 텍스트 추출" 2가지 옵션 - ExecuteRegionOcrAsync(): 180ms 대기 → 전체 화면 캡처 → RegionSelectWindow 오버레이 → 영역 크롭 - ExecuteClipboardOcrAsync(): 클립보드 BitmapSource → System.Drawing.Bitmap 변환 → OCR - RunOcrOnBitmapAsync(): Bitmap → 임시 PNG → WinStorageFile → WinBitmapDecoder → WinOcrEngine.RecognizeAsync() (기존 ClipboardHistoryService.ImageCache.cs 패턴 재사용, 5,000자 상한) - HandleOcrResult(): 클립보드 복사 + 런처 입력창 자동 채움 + 완료 알림 App.xaml.cs: - OcrHandler 등록 (Phase L5-2 주석) LauncherWindow.Keyboard.cs: - F4 키: 런처 Hide → OcrHandler.__ocr_region__ 즉시 실행 (백그라운드 Task) HelpDetailWindow.Shortcuts.cs: - F4 단축키 항목 추가 (F3 바로 다음, 아이콘 \uE8D2, 색상 #0F766E) HelpDetailWindow.xaml.cs: - "런처 탐색" 카테고리에 F4/OCR 항목 추가 Services/L10n.cs: - Phase L5 신기능 팁 5종 추가 (OCR F4, ocr 예약어, 전용 핫키 설정, hotkey 예약어, OCR 입력창 채움) docs/LAUNCHER_ROADMAP.md: - L5-2 ✅ 완료 표시 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -130,7 +130,7 @@
|
|||||||
| # | 기능 | 설명 | 우선순위 |
|
| # | 기능 | 설명 | 우선순위 |
|
||||||
|---|------|------|----------|
|
|---|------|------|----------|
|
||||||
| L5-1 | **항목별 전용 핫키** ✅ | 앱·URL·폴더에 `Ctrl+Alt+숫자` 등 글로벌 단축키 직접 할당. `hotkey` 프리픽스로 관리. `HotkeyAssignment` 모델 + `InputListener` 확장 + 설정창 "전용 핫키" 탭 | 높음 |
|
| L5-1 | **항목별 전용 핫키** ✅ | 앱·URL·폴더에 `Ctrl+Alt+숫자` 등 글로벌 단축키 직접 할당. `hotkey` 프리픽스로 관리. `HotkeyAssignment` 모델 + `InputListener` 확장 + 설정창 "전용 핫키" 탭 | 높음 |
|
||||||
| L5-2 | **OCR 화면 텍스트 추출** | `ocr` 명령 또는 캡처에서 F4 → 화면 영역 드래그 → Windows OCR(로컬) 추출 → 클립보드 복사 / 런처 입력 | 높음 |
|
| L5-2 | **OCR 화면 텍스트 추출** ✅ | `ocr` 프리픽스 + F4 글로벌 단축키. RegionSelectWindow 재사용, Windows.Media.Ocr 로컬 엔진. 결과 → 클립보드 복사 + 런처 입력창 자동 채움 | 높음 |
|
||||||
| L5-3 | **QuickLook 인라인 편집** | F3 미리보기에서 텍스트·마크다운 파일 직접 편집 + Ctrl+S 저장. 변경 감지(수정 표시 `●`), Esc 취소 | 중간 |
|
| L5-3 | **QuickLook 인라인 편집** | F3 미리보기에서 텍스트·마크다운 파일 직접 편집 + Ctrl+S 저장. 변경 감지(수정 표시 `●`), Esc 취소 | 중간 |
|
||||||
| L5-4 | **앱 세션 스냅** | 여러 앱을 지정 레이아웃으로 한번에 열기. `snap 세션이름` → 등록된 앱 목록을 각 레이아웃에 배치 | 중간 |
|
| L5-4 | **앱 세션 스냅** | 여러 앱을 지정 레이아웃으로 한번에 열기. `snap 세션이름` → 등록된 앱 목록을 각 레이아웃에 배치 | 중간 |
|
||||||
| L5-5 | **배치 파일 이름 변경** | 다중 선택 후 `rename {패턴}` → 넘버링·날짜·정규식 치환 미리보기 → 일괄 적용 | 중간 |
|
| L5-5 | **배치 파일 이름 변경** | 다중 선택 후 `rename {패턴}` → 넘버링·날짜·정규식 치환 미리보기 → 일괄 적용 | 중간 |
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ public partial class App : System.Windows.Application
|
|||||||
// ─── Phase L5 핸들러 ──────────────────────────────────────────────────
|
// ─── Phase L5 핸들러 ──────────────────────────────────────────────────
|
||||||
// Phase L5-1: 전용 핫키 목록 관리 (prefix=hotkey)
|
// Phase L5-1: 전용 핫키 목록 관리 (prefix=hotkey)
|
||||||
commandResolver.RegisterHandler(new HotkeyHandler(settings));
|
commandResolver.RegisterHandler(new HotkeyHandler(settings));
|
||||||
|
// Phase L5-2: OCR 화면 텍스트 추출 (prefix=ocr)
|
||||||
|
commandResolver.RegisterHandler(new OcrHandler());
|
||||||
|
|
||||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||||
var pluginHost = new PluginHost(settings, commandResolver);
|
var pluginHost = new PluginHost(settings, commandResolver);
|
||||||
|
|||||||
264
src/AxCopilot/Handlers/OcrHandler.cs
Normal file
264
src/AxCopilot/Handlers/OcrHandler.cs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
using AxCopilot.Themes;
|
||||||
|
|
||||||
|
using WinBitmapDecoder = Windows.Graphics.Imaging.BitmapDecoder;
|
||||||
|
using WinBitmapPixelFmt = Windows.Graphics.Imaging.BitmapPixelFormat;
|
||||||
|
using WinSoftwareBitmap = Windows.Graphics.Imaging.SoftwareBitmap;
|
||||||
|
using WinOcrEngine = Windows.Media.Ocr.OcrEngine;
|
||||||
|
using WinStorageFile = Windows.Storage.StorageFile;
|
||||||
|
using WinFileAccessMode = Windows.Storage.FileAccessMode;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L5-2: 화면 텍스트 OCR 추출 핸들러.
|
||||||
|
/// 예: ocr → 옵션 목록 (영역 선택 / 클립보드 이미지)
|
||||||
|
/// ocr region → 드래그 영역 선택 후 텍스트 추출
|
||||||
|
/// ocr clip → 클립보드 이미지에서 텍스트 추출
|
||||||
|
/// 결과 텍스트는 클립보드에 복사되고 런처 입력창에 채워집니다.
|
||||||
|
/// </summary>
|
||||||
|
public class OcrHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "ocr";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"OcrExtractor",
|
||||||
|
"화면 텍스트 추출 (OCR)",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
private const string DataRegion = "__ocr_region__";
|
||||||
|
private const string DataClipboard = "__ocr_clipboard__";
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
var items = new List<LauncherItem>
|
||||||
|
{
|
||||||
|
new LauncherItem(
|
||||||
|
"화면 영역 텍스트 추출",
|
||||||
|
"드래그로 영역을 선택하면 텍스트를 자동으로 인식합니다 · F4 단축키 지원",
|
||||||
|
null, DataRegion,
|
||||||
|
Symbol: "\uE8D2"),
|
||||||
|
|
||||||
|
new LauncherItem(
|
||||||
|
"클립보드 이미지 텍스트 추출",
|
||||||
|
"클립보드에 복사된 이미지에서 텍스트를 인식합니다",
|
||||||
|
null, DataClipboard,
|
||||||
|
Symbol: "\uE77F")
|
||||||
|
};
|
||||||
|
|
||||||
|
// 쿼리 필터링
|
||||||
|
if (!string.IsNullOrEmpty(q))
|
||||||
|
{
|
||||||
|
items = items.Where(i =>
|
||||||
|
i.Title.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
i.Subtitle.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// OCR 미지원 안내
|
||||||
|
if (WinOcrEngine.TryCreateFromUserProfileLanguages() == null)
|
||||||
|
{
|
||||||
|
items.Clear();
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
"OCR 기능을 사용할 수 없습니다",
|
||||||
|
"Windows 설정 → 언어에서 OCR 지원 언어 팩을 설치하세요",
|
||||||
|
null, null,
|
||||||
|
Symbol: Symbols.Info));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||||
|
{
|
||||||
|
switch (item.Data as string)
|
||||||
|
{
|
||||||
|
case DataRegion:
|
||||||
|
await ExecuteRegionOcrAsync(ct);
|
||||||
|
break;
|
||||||
|
case DataClipboard:
|
||||||
|
await ExecuteClipboardOcrAsync(ct);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 영역 선택 OCR ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static async Task ExecuteRegionOcrAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// 런처가 완전히 사라질 때까지 대기
|
||||||
|
await Task.Delay(180, ct);
|
||||||
|
|
||||||
|
System.Drawing.Rectangle? selected = null;
|
||||||
|
Bitmap? fullBmp = null;
|
||||||
|
|
||||||
|
// UI 스레드에서 오버레이 창 표시
|
||||||
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
var bounds = GetAllScreenBounds();
|
||||||
|
fullBmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
|
||||||
|
using var g = Graphics.FromImage(fullBmp);
|
||||||
|
g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy);
|
||||||
|
|
||||||
|
var overlay = new Views.RegionSelectWindow(fullBmp, bounds);
|
||||||
|
overlay.ShowDialog();
|
||||||
|
selected = overlay.SelectedRect;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selected == null || selected.Value.Width < 8 || selected.Value.Height < 8)
|
||||||
|
{
|
||||||
|
fullBmp?.Dispose();
|
||||||
|
NotificationService.Notify("AX Copilot", "영역 선택이 취소되었습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 선택 영역 크롭
|
||||||
|
var r = selected.Value;
|
||||||
|
using var crop = new Bitmap(r.Width, r.Height, PixelFormat.Format32bppArgb);
|
||||||
|
using (var cg = Graphics.FromImage(crop))
|
||||||
|
cg.DrawImage(fullBmp!, new System.Drawing.Rectangle(0, 0, r.Width, r.Height), r, System.Drawing.GraphicsUnit.Pixel);
|
||||||
|
fullBmp?.Dispose();
|
||||||
|
|
||||||
|
// OCR 실행
|
||||||
|
var text = await RunOcrOnBitmapAsync(crop);
|
||||||
|
|
||||||
|
// 결과 처리
|
||||||
|
HandleOcrResult(text, $"{r.Width}×{r.Height} 영역");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 클립보드 이미지 OCR ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static async Task ExecuteClipboardOcrAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
Bitmap? clipBmp = null;
|
||||||
|
|
||||||
|
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
if (Clipboard.ContainsImage())
|
||||||
|
{
|
||||||
|
var src = Clipboard.GetImage();
|
||||||
|
if (src != null)
|
||||||
|
{
|
||||||
|
// BitmapSource → System.Drawing.Bitmap 변환
|
||||||
|
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||||||
|
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(src));
|
||||||
|
using var ms = new MemoryStream();
|
||||||
|
encoder.Save(ms);
|
||||||
|
ms.Position = 0;
|
||||||
|
clipBmp = new Bitmap(ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clipBmp == null)
|
||||||
|
{
|
||||||
|
NotificationService.Notify("AX Copilot — OCR", "클립보드에 이미지가 없습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (clipBmp)
|
||||||
|
{
|
||||||
|
var text = await RunOcrOnBitmapAsync(clipBmp);
|
||||||
|
HandleOcrResult(text, "클립보드 이미지");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 공통: Bitmap → OCR ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static async Task<string?> RunOcrOnBitmapAsync(Bitmap bmp)
|
||||||
|
{
|
||||||
|
var engine = WinOcrEngine.TryCreateFromUserProfileLanguages();
|
||||||
|
if (engine == null) return null;
|
||||||
|
|
||||||
|
// Bitmap을 임시 PNG로 저장
|
||||||
|
var tmpPath = Path.Combine(Path.GetTempPath(), $"axocr_{Guid.NewGuid():N}.png");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bmp.Save(tmpPath, ImageFormat.Png);
|
||||||
|
|
||||||
|
var storageFile = await WinStorageFile.GetFileFromPathAsync(tmpPath);
|
||||||
|
using var stream = await storageFile.OpenAsync(WinFileAccessMode.Read);
|
||||||
|
var decoder = await WinBitmapDecoder.CreateAsync(stream);
|
||||||
|
|
||||||
|
WinSoftwareBitmap? origBitmap = null;
|
||||||
|
WinSoftwareBitmap? ocrBitmap = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
origBitmap = await decoder.GetSoftwareBitmapAsync();
|
||||||
|
ocrBitmap = origBitmap.BitmapPixelFormat == WinBitmapPixelFmt.Bgra8
|
||||||
|
? origBitmap
|
||||||
|
: WinSoftwareBitmap.Convert(origBitmap, WinBitmapPixelFmt.Bgra8);
|
||||||
|
|
||||||
|
var result = await engine.RecognizeAsync(ocrBitmap);
|
||||||
|
var text = result.Text?.Trim();
|
||||||
|
if (text?.Length > 5_000) text = text[..5_000];
|
||||||
|
return string.IsNullOrWhiteSpace(text) ? null : text;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(origBitmap, ocrBitmap)) origBitmap?.Dispose();
|
||||||
|
ocrBitmap?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogService.Warn($"OCR 실행 오류: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 공통: 결과 처리 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static void HandleOcrResult(string? text, string source)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
NotificationService.Notify("OCR 완료", $"{source}에서 텍스트를 인식하지 못했습니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 클립보드에 복사
|
||||||
|
Application.Current?.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
Clipboard.SetText(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 런처를 다시 열고 결과 텍스트를 입력창에 채움
|
||||||
|
Application.Current?.Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
var launcher = Application.Current?.Windows
|
||||||
|
.OfType<Views.LauncherWindow>().FirstOrDefault();
|
||||||
|
if (launcher != null)
|
||||||
|
{
|
||||||
|
launcher.SetInputText(text.Length > 200 ? text[..200] : text);
|
||||||
|
launcher.Show();
|
||||||
|
}
|
||||||
|
}, System.Windows.Threading.DispatcherPriority.Background);
|
||||||
|
|
||||||
|
// 완료 알림
|
||||||
|
var preview = text.Length > 60 ? text[..57].Replace('\n', ' ') + "…" : text.Replace('\n', ' ');
|
||||||
|
NotificationService.Notify("OCR 완료", $"클립보드 복사됨: {preview}");
|
||||||
|
LogService.Info($"OCR 성공 ({source}, {text.Length}자)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 헬퍼 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static System.Drawing.Rectangle GetAllScreenBounds()
|
||||||
|
{
|
||||||
|
var bounds = System.Drawing.Rectangle.Empty;
|
||||||
|
foreach (System.Windows.Forms.Screen screen in System.Windows.Forms.Screen.AllScreens)
|
||||||
|
bounds = System.Drawing.Rectangle.Union(bounds, screen.Bounds);
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -194,6 +194,13 @@ public static class L10n
|
|||||||
"= 20km 처럼 입력하면 킬로미터를 자동으로 변환 제안해 드립니다!",
|
"= 20km 처럼 입력하면 킬로미터를 자동으로 변환 제안해 드립니다!",
|
||||||
"= 100°F 를 입력하면 섭씨·켈빈으로 자동 변환됩니다.",
|
"= 100°F 를 입력하면 섭씨·켈빈으로 자동 변환됩니다.",
|
||||||
"= today+30d 로 30일 후 날짜를 바로 계산할 수 있습니다.",
|
"= today+30d 로 30일 후 날짜를 바로 계산할 수 있습니다.",
|
||||||
|
|
||||||
|
// ── Phase L5 신기능 안내 (OCR·전용 핫키) ──
|
||||||
|
"F4 키로 화면 드래그 영역의 텍스트를 즉시 추출할 수 있습니다!",
|
||||||
|
"'ocr' 을 입력하면 화면 또는 클립보드 이미지에서 텍스트를 인식합니다.",
|
||||||
|
"자주 쓰는 파일·앱에 Ctrl+Alt+숫자 전용 핫키를 설정해보세요. 설정 → 전용 핫키",
|
||||||
|
"'hotkey' 를 입력하면 등록된 전용 핫키 목록을 확인하고 바로 실행할 수 있습니다.",
|
||||||
|
"OCR로 이미지 속 텍스트를 추출하면 입력창에 자동으로 채워집니다!",
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly string[] _enPlaceholders =
|
private static readonly string[] _enPlaceholders =
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ public partial class HelpDetailWindow
|
|||||||
"파일 빠른 미리보기 창 열기/닫기 (QuickLook)",
|
"파일 빠른 미리보기 창 열기/닫기 (QuickLook)",
|
||||||
"선택된 파일의 미리보기 창을 토글합니다. 이미지·텍스트·코드(줄번호+색상)·PDF(텍스트 추출)·Word·Excel 미리보기를 지원합니다. 다시 F3을 누르면 창이 닫힙니다.",
|
"선택된 파일의 미리보기 창을 토글합니다. 이미지·텍스트·코드(줄번호+색상)·PDF(텍스트 추출)·Word·Excel 미리보기를 지원합니다. 다시 F3을 누르면 창이 닫힙니다.",
|
||||||
"\uE8A4", "#6B2C91"));
|
"\uE8A4", "#6B2C91"));
|
||||||
|
items.Add(MakeShortcut("런처 기능", "F4",
|
||||||
|
"화면 영역 텍스트 추출 (OCR)",
|
||||||
|
"런처를 닫고 화면 드래그 영역 선택 모드를 즉시 실행합니다. 선택한 영역의 텍스트를 자동으로 인식해 클립보드에 복사하고 런처 입력창에 채웁니다.",
|
||||||
|
"\uE8D2", "#0F766E"));
|
||||||
items.Add(MakeShortcut("런처 기능", "F1",
|
items.Add(MakeShortcut("런처 기능", "F1",
|
||||||
"도움말 창 열기",
|
"도움말 창 열기",
|
||||||
"이 화면을 직접 엽니다. 'help' 를 입력하는 것과 동일합니다.",
|
"이 화면을 직접 엽니다. 'help' 를 입력하는 것과 동일합니다.",
|
||||||
|
|||||||
@@ -150,6 +150,15 @@ public partial class HelpDetailWindow : Window
|
|||||||
ColorBrush = ParseColor("#6B2C91")
|
ColorBrush = ParseColor("#6B2C91")
|
||||||
},
|
},
|
||||||
new()
|
new()
|
||||||
|
{
|
||||||
|
Category = "런처 탐색", Command = "F4",
|
||||||
|
Title = "화면 영역 텍스트 추출 (OCR)",
|
||||||
|
Description = "F4를 누르면 런처가 닫히고 화면 드래그 선택 모드가 즉시 실행됩니다. 원하는 영역을 드래그해 선택하면 내부 텍스트를 자동으로 인식해 클립보드에 복사하고 런처 입력창에 채웁니다. 'ocr' 예약어로도 실행할 수 있으며 클립보드 이미지도 지원합니다.",
|
||||||
|
Example = "F4 또는 ocr → Enter",
|
||||||
|
Symbol = "\uE8D2",
|
||||||
|
ColorBrush = ParseColor("#0F766E")
|
||||||
|
},
|
||||||
|
new()
|
||||||
{
|
{
|
||||||
Category = "런처 탐색", Command = "↑ / ↓ (입력 없을 때)",
|
Category = "런처 탐색", Command = "↑ / ↓ (입력 없을 때)",
|
||||||
Title = "검색 히스토리 탐색",
|
Title = "검색 히스토리 탐색",
|
||||||
|
|||||||
@@ -500,6 +500,28 @@ public partial class LauncherWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── F4 → 화면 영역 OCR 즉시 실행 ──────────────────────────────────
|
||||||
|
if (e.Key == Key.F4)
|
||||||
|
{
|
||||||
|
Hide();
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var handler = new Handlers.OcrHandler();
|
||||||
|
var item = new SDK.LauncherItem(
|
||||||
|
"화면 영역 텍스트 추출", "", null, "__ocr_region__");
|
||||||
|
await handler.ExecuteAsync(item, CancellationToken.None);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Services.LogService.Error($"F4 OCR 실행 오류: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Ctrl+1~9 → n번째 결과 즉시 실행 ───────────────────────────────
|
// ─── Ctrl+1~9 → n번째 결과 즉시 실행 ───────────────────────────────
|
||||||
if (mod == ModifierKeys.Control)
|
if (mod == ModifierKeys.Control)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user