[Phase 39] FontFamily 캐싱 + LauncherWindow 파셜 클래스 분할

- ThemeResourceHelper에 CascadiaCode/ConsolasCode/ConsolasCourierNew 정적 필드 추가
- 25개 파일, 89개 new FontFamily(...) 호출을 정적 캐시 참조로 교체
- LauncherWindow.xaml.cs (1,563줄) → 5개 파셜 파일로 분할 (63% 감소)
  - LauncherWindow.Theme.cs (116줄): ApplyTheme, 커스텀 딕셔너리 빌드
  - LauncherWindow.Animations.cs (153줄): 무지개 글로우, 애니메이션 헬퍼
  - LauncherWindow.Keyboard.cs (593줄): 단축키 20종, ShowToast, IME 검색
  - LauncherWindow.Shell.cs (177줄): Shell32, SendToRecycleBin, 클릭 핸들러
- 빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 18:54:35 +09:00
parent 08524466d2
commit 0c997f0149
32 changed files with 1160 additions and 1066 deletions

View File

@@ -0,0 +1,177 @@
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;
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을 통해 처리
}
}