From ff048a61987a9647c126b7037b939caf73da41b8 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 09:55:59 +0900 Subject: [PATCH] =?UTF-8?q?[Phase=20L2-3]=20=ED=81=B4=EB=A6=BD=EB=B3=B4?= =?UTF-8?q?=EB=93=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=AF=B8=EB=A6=AC?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EC=B0=BD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 신규 파일: - ClipboardImagePreviewWindow.xaml (신규, 140줄): WindowStyle=None, 커스텀 타이틀바, 이미지 ScaleTransform 줌, 하단 툴바(축소/줌레이블/확대 / 1:1 / 창맞춤 / 저장 / 복사) - ClipboardImagePreviewWindow.xaml.cs (신규, 173줄): 원본 해상도 이미지 로드(OriginalImagePath → 썸네일 폴백), FitToWindow() 초기 자동 맞춤, ApplyZoom(ScaleTransform), Ctrl+휠 줌, 키보드 단축키(+/-/0/F/Esc/Ctrl+C/Ctrl+S), PNG·JPEG·BMP 저장(SaveFileDialog), NotificationCenterService 피드백 변경 파일: - LauncherWindow.Keyboard.cs: Shift+Enter 시 ClipboardEntry 이미지 항목 → 미리보기 창 열기(기존 폴더열기 분기 전에 추가) - LauncherWindow.ShortcutHelp.cs: "Shift+Enter — 대형 텍스트 / 이미지 미리보기 (#)" 안내 갱신 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 --- .../Views/ClipboardImagePreviewWindow.xaml | 225 +++++++++++++++++ .../Views/ClipboardImagePreviewWindow.xaml.cs | 231 ++++++++++++++++++ .../Views/LauncherWindow.Keyboard.cs | 10 + .../Views/LauncherWindow.ShortcutHelp.cs | 2 +- 4 files changed, 467 insertions(+), 1 deletion(-) create mode 100644 src/AxCopilot/Views/ClipboardImagePreviewWindow.xaml create mode 100644 src/AxCopilot/Views/ClipboardImagePreviewWindow.xaml.cs diff --git a/src/AxCopilot/Views/ClipboardImagePreviewWindow.xaml b/src/AxCopilot/Views/ClipboardImagePreviewWindow.xaml new file mode 100644 index 0000000..8a0dd13 --- /dev/null +++ b/src/AxCopilot/Views/ClipboardImagePreviewWindow.xaml @@ -0,0 +1,225 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AxCopilot/Views/ClipboardImagePreviewWindow.xaml.cs b/src/AxCopilot/Views/ClipboardImagePreviewWindow.xaml.cs new file mode 100644 index 0000000..bc15721 --- /dev/null +++ b/src/AxCopilot/Views/ClipboardImagePreviewWindow.xaml.cs @@ -0,0 +1,231 @@ +using System.IO; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media.Imaging; +using AxCopilot.Services; +using Microsoft.Win32; + +namespace AxCopilot.Views; + +/// +/// Phase L2-3: 클립보드 이미지 미리보기 창. +/// 원본 해상도 이미지 표시 + 확대/축소 + 클립보드 복사 + 파일 저장. +/// Shift+Enter (이미지 항목 선택 시) 또는 런처 내 미리보기 액션으로 열립니다. +/// +public partial class ClipboardImagePreviewWindow : Window +{ + private readonly ClipboardEntry _entry; + private BitmapSource? _fullImage; + private double _zoomFactor = 1.0; + + private const double ZoomStep = 0.25; + private const double MinZoom = 0.05; + private const double MaxZoom = 8.0; + + public ClipboardImagePreviewWindow(ClipboardEntry entry) + { + InitializeComponent(); + _entry = entry; + KeyDown += OnKeyDown; + Loaded += OnLoaded; + } + + // ─── 초기화 ────────────────────────────────────────────────────────── + + private void OnLoaded(object sender, RoutedEventArgs e) + { + LoadImage(); + } + + private void LoadImage() + { + // 원본 해상도 이미지 우선, 없으면 썸네일 폴백 + _fullImage = ClipboardHistoryService.LoadOriginalImage(_entry.OriginalImagePath) + ?? _entry.Image; + + if (_fullImage == null) + { + SizeLabel.Text = "(이미지 없음)"; + return; + } + + PreviewImage.Source = _fullImage; + SizeLabel.Text = $"{_fullImage.PixelWidth} × {_fullImage.PixelHeight} px"; + + // 창 레이아웃이 확정된 후 Fit 적용 + Dispatcher.InvokeAsync(FitToWindow, System.Windows.Threading.DispatcherPriority.Loaded); + } + + // ─── 줌 제어 ───────────────────────────────────────────────────────── + + private void FitToWindow() + { + if (_fullImage == null) return; + var availW = ImageScroll.ActualWidth - 24; + var availH = ImageScroll.ActualHeight - 24; + if (availW <= 0 || availH <= 0) return; + + var scaleX = availW / _fullImage.PixelWidth; + var scaleY = availH / _fullImage.PixelHeight; + _zoomFactor = Math.Min(1.0, Math.Min(scaleX, scaleY)); + ApplyZoom(); + } + + private void ApplyZoom() + { + ZoomTransform.ScaleX = _zoomFactor; + ZoomTransform.ScaleY = _zoomFactor; + ZoomLabel.Text = $"{_zoomFactor * 100:F0}%"; + } + + // ─── 타이틀바 ──────────────────────────────────────────────────────── + + private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e) + { + if (e.LeftButton == MouseButtonState.Pressed) + DragMove(); + } + + // ─── 버튼 핸들러 ───────────────────────────────────────────────────── + + private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close(); + + private void BtnZoomIn_Click(object sender, MouseButtonEventArgs e) + { + _zoomFactor = Math.Min(MaxZoom, Math.Round(_zoomFactor + ZoomStep, 2)); + ApplyZoom(); + } + + private void BtnZoomOut_Click(object sender, MouseButtonEventArgs e) + { + _zoomFactor = Math.Max(MinZoom, Math.Round(_zoomFactor - ZoomStep, 2)); + ApplyZoom(); + } + + private void BtnActualSize_Click(object sender, MouseButtonEventArgs e) + { + _zoomFactor = 1.0; + ApplyZoom(); + } + + private void BtnFitToWindow_Click(object sender, MouseButtonEventArgs e) + => FitToWindow(); + + private void BtnCopy_Click(object sender, MouseButtonEventArgs e) + => CopyToClipboard(); + + private void BtnSave_Click(object sender, MouseButtonEventArgs e) + => SaveToFile(); + + // ─── 스크롤 휠 줌 ──────────────────────────────────────────────────── + + private void ImageScroll_PreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + if ((Keyboard.Modifiers & ModifierKeys.Control) == 0) return; + e.Handled = true; + + var delta = e.Delta > 0 ? ZoomStep : -ZoomStep; + _zoomFactor = Math.Clamp(Math.Round(_zoomFactor + delta, 2), MinZoom, MaxZoom); + ApplyZoom(); + } + + // ─── 키보드 단축키 ─────────────────────────────────────────────────── + + private void OnKeyDown(object sender, KeyEventArgs e) + { + switch (e.Key) + { + case Key.Escape: + Close(); + e.Handled = true; + break; + + case Key.Add: + case Key.OemPlus: + _zoomFactor = Math.Min(MaxZoom, Math.Round(_zoomFactor + ZoomStep, 2)); + ApplyZoom(); + e.Handled = true; + break; + + case Key.Subtract: + case Key.OemMinus: + _zoomFactor = Math.Max(MinZoom, Math.Round(_zoomFactor - ZoomStep, 2)); + ApplyZoom(); + e.Handled = true; + break; + + case Key.D0: + case Key.NumPad0: + _zoomFactor = 1.0; + ApplyZoom(); + e.Handled = true; + break; + + case Key.F: + FitToWindow(); + e.Handled = true; + break; + + case Key.C when (Keyboard.Modifiers & ModifierKeys.Control) != 0: + CopyToClipboard(); + e.Handled = true; + break; + + case Key.S when (Keyboard.Modifiers & ModifierKeys.Control) != 0: + SaveToFile(); + e.Handled = true; + break; + } + } + + // ─── 내부 동작 ─────────────────────────────────────────────────────── + + private void CopyToClipboard() + { + if (_fullImage == null) return; + try + { + Clipboard.SetImage(_fullImage); + NotificationCenterService.Show("클립보드", "이미지를 클립보드에 복사했습니다.", + NotificationType.Success); + } + catch (Exception ex) + { + NotificationCenterService.Show("복사 실패", ex.Message, NotificationType.Error); + } + } + + private void SaveToFile() + { + if (_fullImage == null) return; + + var dlg = new SaveFileDialog + { + Title = "이미지 저장", + Filter = "PNG 이미지 (*.png)|*.png|JPEG 이미지 (*.jpg)|*.jpg|BMP 이미지 (*.bmp)|*.bmp", + FileName = $"clipboard_{DateTime.Now:yyyyMMdd_HHmmss}", + DefaultExt = "png", + }; + if (dlg.ShowDialog(this) != true) return; + + try + { + BitmapEncoder encoder = Path.GetExtension(dlg.FileName).ToLowerInvariant() switch + { + ".jpg" or ".jpeg" => new JpegBitmapEncoder { QualityLevel = 95 }, + ".bmp" => new BmpBitmapEncoder(), + _ => new PngBitmapEncoder(), + }; + encoder.Frames.Add(BitmapFrame.Create(_fullImage)); + using var fs = File.OpenWrite(dlg.FileName); + encoder.Save(fs); + + NotificationCenterService.Show("저장 완료", Path.GetFileName(dlg.FileName), + NotificationType.Success); + } + catch (Exception ex) + { + NotificationCenterService.Show("저장 실패", ex.Message, NotificationType.Error); + } + } +} diff --git a/src/AxCopilot/Views/LauncherWindow.Keyboard.cs b/src/AxCopilot/Views/LauncherWindow.Keyboard.cs index 749c00b..dc6c006 100644 --- a/src/AxCopilot/Views/LauncherWindow.Keyboard.cs +++ b/src/AxCopilot/Views/LauncherWindow.Keyboard.cs @@ -50,6 +50,16 @@ public partial class LauncherWindow if (shift) { + // Phase L2-3: 클립보드 이미지 항목 → 이미지 미리보기 창 열기 + if (_vm.SelectedItem?.Data is AxCopilot.Services.ClipboardEntry imgEntry && !imgEntry.IsText) + { + e.Handled = true; + Hide(); + var previewWin = new ClipboardImagePreviewWindow(imgEntry); + previewWin.Show(); + break; + } + // 퍼지 파일 검색 결과: Shift+Enter → 파일이 있는 폴더 열기 if (_vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry shiftEntry) { diff --git a/src/AxCopilot/Views/LauncherWindow.ShortcutHelp.cs b/src/AxCopilot/Views/LauncherWindow.ShortcutHelp.cs index 582d4a4..de11d9d 100644 --- a/src/AxCopilot/Views/LauncherWindow.ShortcutHelp.cs +++ b/src/AxCopilot/Views/LauncherWindow.ShortcutHelp.cs @@ -41,7 +41,7 @@ public partial class LauncherWindow "Ctrl+Shift+E 탐색기에서 열기", "Ctrl+Enter 관리자 실행", "Alt+Enter 속성 보기", - "Shift+Enter 대형 텍스트", + "Shift+Enter 대형 텍스트 / 이미지 미리보기 (#)", }; CustomMessageBox.Show(