diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md index 6670213..dfa601d 100644 --- a/docs/NEXT_ROADMAP.md +++ b/docs/NEXT_ROADMAP.md @@ -4554,5 +4554,35 @@ Week 8: [23] AutoCompact + isEnabled + 최종 검증 --- -최종 업데이트: 2026-04-03 (Phase 22~38 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 6차) +## Phase 39 — FontFamily 캐싱 + LauncherWindow 파셜 분할 (v2.3) ✅ 완료 + +> **목표**: 89개 `new FontFamily(...)` 반복 생성 제거 + LauncherWindow 파셜 분할. + +### FontFamily 캐싱 (25개 파일) + +ThemeResourceHelper에 5개 정적 필드 추가: +- `SegoeMdl2` — `new FontFamily("Segoe MDL2 Assets")` (기존) +- `Consolas` — `new FontFamily("Consolas")` (기존) +- `CascadiaCode` — `new FontFamily("Cascadia Code, Consolas, monospace")` (신규) +- `ConsolasCode` — `new FontFamily("Consolas, Cascadia Code, Segoe UI")` (신규) +- `ConsolasCourierNew` — `new FontFamily("Consolas, Courier New")` (신규) + +총 89개 `new FontFamily(...)` 호출 → 정적 캐시 필드 참조로 교체 (25개 파일) + +### LauncherWindow 파셜 분할 + +| 파일 | 줄 수 | 내용 | +|------|-------|------| +| `LauncherWindow.xaml.cs` (메인) | 578 | Win32 P/Invoke, 생성자, Show(), 아이콘 20종 애니메이션 | +| `LauncherWindow.Theme.cs` | 116 | ApplyTheme, 테마 빌드, BuildCustomDictionary, IsSystemDarkMode | +| `LauncherWindow.Animations.cs` | 153 | 무지개 글로우, 애니메이션 헬퍼, CenterOnScreen, AnimateIn | +| `LauncherWindow.Keyboard.cs` | 593 | IME 검색, PreviewKeyDown, KeyDown 20여 단축키, ShowToast | +| `LauncherWindow.Shell.cs` | 177 | Shell32 P/Invoke, SendToRecycleBin, ShowLargeType, 클릭 핸들러 | + +- **메인 파일**: 1,563줄 → 578줄 (**63.0% 감소**) +- **빌드**: 경고 0, 오류 0 + +--- + +최종 업데이트: 2026-04-03 (Phase 22~39 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 7차) diff --git a/src/AxCopilot/Services/Agent/NotifyTool.cs b/src/AxCopilot/Services/Agent/NotifyTool.cs index e0d1440..31cb577 100644 --- a/src/AxCopilot/Services/Agent/NotifyTool.cs +++ b/src/AxCopilot/Services/Agent/NotifyTool.cs @@ -103,7 +103,7 @@ public class NotifyTool : IAgentTool titleRow.Children.Add(new TextBlock { Text = iconChar, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = ThemeResourceHelper.HexBrush(iconColor), Margin = new Thickness(0, 0, 8, 0), diff --git a/src/AxCopilot/Services/MarkdownRenderer.cs b/src/AxCopilot/Services/MarkdownRenderer.cs index c059248..f393950 100644 --- a/src/AxCopilot/Services/MarkdownRenderer.cs +++ b/src/AxCopilot/Services/MarkdownRenderer.cs @@ -3,6 +3,7 @@ using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; +using AxCopilot.Views; namespace AxCopilot.Services; @@ -245,7 +246,7 @@ public static class MarkdownRenderer Child = new TextBlock { Text = m.Groups[7].Value, - FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"), + FontFamily = ThemeResourceHelper.CascadiaCode, FontSize = 12.5, Foreground = accentColor } @@ -484,7 +485,7 @@ public static class MarkdownRenderer var codeLines = code.Split('\n'); var lineNumbers = new TextBlock { - FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"), + FontFamily = ThemeResourceHelper.CascadiaCode, FontSize = 12.5, Foreground = new SolidColorBrush(Color.FromArgb(0x50, 0xFF, 0xFF, 0xFF)), Padding = new Thickness(10, 10, 6, 14), @@ -497,7 +498,7 @@ public static class MarkdownRenderer var codeText = new TextBlock { - FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"), + FontFamily = ThemeResourceHelper.CascadiaCode, FontSize = 12.5, Foreground = textColor, TextWrapping = TextWrapping.Wrap, @@ -532,7 +533,7 @@ public static class MarkdownRenderer new TextBlock { Text = mdlIcon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10, Foreground = fg, Opacity = 0.6, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0), @@ -585,7 +586,7 @@ public static class MarkdownRenderer var lineNumTb = new TextBlock { - FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"), + FontFamily = ThemeResourceHelper.CascadiaCode, FontSize = 13, LineHeight = 22, Foreground = new SolidColorBrush(Color.FromArgb(0x50, 0xFF, 0xFF, 0xFF)), Padding = new Thickness(16, 16, 8, 16), @@ -597,7 +598,7 @@ public static class MarkdownRenderer var codeTb = new TextBlock { - FontFamily = new FontFamily("Cascadia Code, Consolas, monospace"), + FontFamily = ThemeResourceHelper.CascadiaCode, FontSize = 13, LineHeight = 22, Foreground = textColor, TextWrapping = TextWrapping.Wrap, diff --git a/src/AxCopilot/Views/AgentStatsDashboardWindow.xaml.cs b/src/AxCopilot/Views/AgentStatsDashboardWindow.xaml.cs index bcb2d84..e984a47 100644 --- a/src/AxCopilot/Views/AgentStatsDashboardWindow.xaml.cs +++ b/src/AxCopilot/Views/AgentStatsDashboardWindow.xaml.cs @@ -102,7 +102,7 @@ public partial class AgentStatsDashboardWindow : Window sp.Children.Add(new TextBlock { Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 16, Foreground = new SolidColorBrush(col), Margin = new Thickness(0, 0, 0, 6), @@ -213,7 +213,7 @@ public partial class AgentStatsDashboardWindow : Window { Text = tool, FontSize = 11, - FontFamily = new FontFamily("Consolas"), + FontFamily = ThemeResourceHelper.Consolas, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.White, TextTrimming = TextTrimming.CharacterEllipsis, VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/CommandPaletteWindow.xaml.cs b/src/AxCopilot/Views/CommandPaletteWindow.xaml.cs index f32c79f..a007a9c 100644 --- a/src/AxCopilot/Views/CommandPaletteWindow.xaml.cs +++ b/src/AxCopilot/Views/CommandPaletteWindow.xaml.cs @@ -81,7 +81,7 @@ public partial class CommandPaletteWindow : Window var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { - Text = cmd.Icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = cmd.Icon, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = FindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), }); diff --git a/src/AxCopilot/Views/CustomMessageBox.cs b/src/AxCopilot/Views/CustomMessageBox.cs index c797b2a..6743a01 100644 --- a/src/AxCopilot/Views/CustomMessageBox.cs +++ b/src/AxCopilot/Views/CustomMessageBox.cs @@ -69,7 +69,7 @@ internal sealed class CustomMessageBox : Window titlePanel.Children.Add(new TextBlock { Text = iconText, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 18, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center, @@ -91,7 +91,7 @@ internal sealed class CustomMessageBox : Window var closeBtn = new Button { Content = "\uE8BB", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10, Foreground = secondaryText, Background = Brushes.Transparent, @@ -283,7 +283,7 @@ internal sealed class CustomMessageBox : Window panel.Children.Add(new TextBlock { Text = iconText, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = iconColor, VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/CustomMoodDialog.cs b/src/AxCopilot/Views/CustomMoodDialog.cs index 856900a..d489d1b 100644 --- a/src/AxCopilot/Views/CustomMoodDialog.cs +++ b/src/AxCopilot/Views/CustomMoodDialog.cs @@ -69,7 +69,7 @@ internal sealed partial class CustomMoodDialog : Window header.Children.Add(new TextBlock { Text = "\uE771", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), @@ -123,7 +123,7 @@ internal sealed partial class CustomMoodDialog : Window AddLabel(stack, "CSS 스타일", primaryText); AddHint(stack, "문서에 적용될 CSS입니다. body, h1~h6, table, .callout 등의 스타일을 정의하세요.", secondaryText); _cssBox = CreateTextBox(existingCss, primaryText, itemBg, accentBrush, borderBrush, multiline: true, height: 200); - _cssBox.FontFamily = new FontFamily("Consolas, Courier New, monospace"); + _cssBox.FontFamily = ThemeResourceHelper.ConsolasCourierNew; _cssBox.FontSize = 12; stack.Children.Add(new Border { CornerRadius = new CornerRadius(8), ClipToBounds = true, Child = _cssBox }); diff --git a/src/AxCopilot/Views/CustomPresetDialog.cs b/src/AxCopilot/Views/CustomPresetDialog.cs index 6f35ff7..06e96b9 100644 --- a/src/AxCopilot/Views/CustomPresetDialog.cs +++ b/src/AxCopilot/Views/CustomPresetDialog.cs @@ -121,7 +121,7 @@ internal sealed class CustomPresetDialog : Window header.Children.Add(new TextBlock { Text = "\uE710", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), @@ -166,7 +166,7 @@ internal sealed class CustomPresetDialog : Window _iconPreviewText = new TextBlock { Text = _selectedSymbol, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 20, Foreground = BrushFromHex(_selectedColor), HorizontalAlignment = HorizontalAlignment.Center, @@ -355,7 +355,7 @@ internal sealed class CustomPresetDialog : Window }); var closeBtn = new TextBlock { - Text = "\uE711", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE711", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = secondaryText, Cursor = Cursors.Hand, HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Top, @@ -397,7 +397,7 @@ internal sealed class CustomPresetDialog : Window iconBtn.Child = new TextBlock { Text = symbol, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 16, Foreground = isSelected ? BrushFromHex(_selectedColor) : primaryText, HorizontalAlignment = HorizontalAlignment.Center, diff --git a/src/AxCopilot/Views/DiffViewerPanel.cs b/src/AxCopilot/Views/DiffViewerPanel.cs index 3a47ab0..526e9dd 100644 --- a/src/AxCopilot/Views/DiffViewerPanel.cs +++ b/src/AxCopilot/Views/DiffViewerPanel.cs @@ -60,7 +60,7 @@ public class DiffViewerPanel : Border titlePanel.Children.Add(new TextBlock { Text = "\uE89A", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 13, Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), @@ -143,7 +143,7 @@ public class DiffViewerPanel : Border var oldLineText = new TextBlock { - Text = lineNo, FontSize = 10, FontFamily = new FontFamily("Consolas"), + Text = lineNo, FontSize = 10, FontFamily = ThemeResourceHelper.Consolas, Foreground = new SolidColorBrush(Color.FromRgb(0xAA, 0xAA, 0xCC)), HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 0, 4, 0), }; @@ -152,7 +152,7 @@ public class DiffViewerPanel : Border var newLineText = new TextBlock { - Text = newLineNo, FontSize = 10, FontFamily = new FontFamily("Consolas"), + Text = newLineNo, FontSize = 10, FontFamily = ThemeResourceHelper.Consolas, Foreground = new SolidColorBrush(Color.FromRgb(0xAA, 0xAA, 0xCC)), HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 0, 4, 0), }; @@ -161,7 +161,7 @@ public class DiffViewerPanel : Border var prefixText = new TextBlock { - Text = prefix, FontSize = 11, FontFamily = new FontFamily("Consolas"), + Text = prefix, FontSize = 11, FontFamily = ThemeResourceHelper.Consolas, Foreground = new SolidColorBrush(fg), FontWeight = FontWeights.Bold, }; Grid.SetColumn(prefixText, 2); @@ -169,7 +169,7 @@ public class DiffViewerPanel : Border var contentText = new TextBlock { - Text = line.Content, FontSize = 11, FontFamily = new FontFamily("Consolas"), + Text = line.Content, FontSize = 11, FontFamily = ThemeResourceHelper.Consolas, Foreground = new SolidColorBrush(fg), TextWrapping = TextWrapping.NoWrap, }; diff --git a/src/AxCopilot/Views/DockBarWindow.xaml.cs b/src/AxCopilot/Views/DockBarWindow.xaml.cs index 3d81b9f..2930b33 100644 --- a/src/AxCopilot/Views/DockBarWindow.xaml.cs +++ b/src/AxCopilot/Views/DockBarWindow.xaml.cs @@ -165,7 +165,7 @@ public partial class DockBarWindow : Window var cpuPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; cpuPanel.Children.Add(new TextBlock { - Text = "\uE950", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE950", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 11, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0) }); @@ -180,7 +180,7 @@ public partial class DockBarWindow : Window var ramPanel = new StackPanel { Orientation = Orientation.Horizontal, VerticalAlignment = VerticalAlignment.Center }; ramPanel.Children.Add(new TextBlock { - Text = "\uE7F4", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE7F4", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 11, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 3, 0) }); @@ -199,7 +199,7 @@ public partial class DockBarWindow : Window var inputPanel = new StackPanel { Orientation = Orientation.Horizontal }; inputPanel.Children.Add(new TextBlock { - Text = "\uE721", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE721", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 11, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 5, 0) }); @@ -246,7 +246,7 @@ public partial class DockBarWindow : Window border.Child = new TextBlock { Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = foreground, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/HelpDetailWindow.xaml.cs b/src/AxCopilot/Views/HelpDetailWindow.xaml.cs index 3700c55..6c87f69 100644 --- a/src/AxCopilot/Views/HelpDetailWindow.xaml.cs +++ b/src/AxCopilot/Views/HelpDetailWindow.xaml.cs @@ -419,7 +419,7 @@ public partial class HelpDetailWindow : Window sp.Children.Add(new TextBlock { Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = ThemeAccent, VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/InputDialog.cs b/src/AxCopilot/Views/InputDialog.cs index 957384c..fb58c10 100644 --- a/src/AxCopilot/Views/InputDialog.cs +++ b/src/AxCopilot/Views/InputDialog.cs @@ -54,7 +54,7 @@ internal sealed class InputDialog : Window titlePanel.Children.Add(new TextBlock { Text = "\uE8AC", // 편집 아이콘 - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 16, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), diff --git a/src/AxCopilot/Views/LauncherWindow.Animations.cs b/src/AxCopilot/Views/LauncherWindow.Animations.cs new file mode 100644 index 0000000..d3a8d17 --- /dev/null +++ b/src/AxCopilot/Views/LauncherWindow.Animations.cs @@ -0,0 +1,153 @@ +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace AxCopilot.Views; + +public partial class LauncherWindow +{ + // ─── 무지개 글로우 상시 애니메이션 ──────────────────────────────────────── + + /// 선택 아이템 상시 무지개 글로우 효과를 적용하거나 제거합니다. + private void UpdateSelectionGlow() + { + if (_vm.EnableSelectionGlow) + { + var gs = new System.Windows.Media.GradientStopCollection + { + new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x6B, 0x6B), 0.00), + new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFE, 0xCA, 0x57), 0.17), + new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x48, 0xDB, 0xFB), 0.33), + new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x9F, 0xF3), 0.50), + new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x54, 0xA0, 0xFF), 0.67), + new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x5F, 0x27, 0xCD), 0.83), + new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x6B, 0x6B), 1.00), + }; + Resources["SelectionGlowBrush"] = new System.Windows.Media.LinearGradientBrush( + gs, + new System.Windows.Point(0, 0), + new System.Windows.Point(1, 1)); + Resources["SelectionGlowVisibility"] = Visibility.Visible; + } + else + { + Resources["SelectionGlowBrush"] = System.Windows.Media.Brushes.Transparent; + Resources["SelectionGlowVisibility"] = Visibility.Collapsed; + } + } + + /// 무지개 글로우를 정지하고 숨깁니다. + private void StopRainbowGlow() + { + _rainbowTimer?.Stop(); + _rainbowTimer = null; + if (RainbowGlowBorder != null) RainbowGlowBorder.Opacity = 0; + } + + /// 런처 테두리 무지개 그라데이션 회전을 시작합니다. + private void StartRainbowGlow() + { + _rainbowTimer?.Stop(); + if (LauncherRainbowBrush == null || RainbowGlowBorder == null) return; + + _rainbowTimer = new System.Windows.Threading.DispatcherTimer + { + Interval = TimeSpan.FromMilliseconds(20) + }; + var startTime = DateTime.UtcNow; + _rainbowTimer.Tick += (_, _) => + { + if (!IsVisible) { _rainbowTimer?.Stop(); return; } + var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds; + var shift = (elapsed / 2000.0) % 1.0; // 2초에 1바퀴 (느리게) + var angle = shift * Math.PI * 2; + LauncherRainbowBrush.StartPoint = new Point(0.5 + 0.5 * Math.Cos(angle), 0.5 + 0.5 * Math.Sin(angle)); + LauncherRainbowBrush.EndPoint = new Point(0.5 - 0.5 * Math.Cos(angle), 0.5 - 0.5 * Math.Sin(angle)); + }; + _rainbowTimer.Start(); + } + + // ─── 애니메이션 헬퍼 ────────────────────────────────────────────────────── + + private static KeyTime KT(double sec) => KeyTime.FromTimeSpan(TimeSpan.FromSeconds(sec)); + + private static void AddOpacityPulse(Storyboard sb, UIElement target, int index, double totalSec) + { + var a = new DoubleAnimationUsingKeyFrames(); + a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(index))); + a.KeyFrames.Add(new LinearDoubleKeyFrame(0.25, KT(index + 0.5))); + a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(index + 1))); + a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(totalSec))); + Storyboard.SetTarget(a, target); + Storyboard.SetTargetProperty(a, new PropertyPath(UIElement.OpacityProperty)); + sb.Children.Add(a); + } + + private static void AddGroupFlash(Storyboard sb, UIElement[] group, double startSec, double totalSec) + { + foreach (var p in group) + { + var a = new DoubleAnimationUsingKeyFrames(); + a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(0))); + a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(startSec))); + a.KeyFrames.Add(new LinearDoubleKeyFrame(0.2, KT(startSec + 0.6))); + a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(startSec + 1.2))); + a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(totalSec))); + Storyboard.SetTarget(a, p); + Storyboard.SetTargetProperty(a, new PropertyPath(UIElement.OpacityProperty)); + sb.Children.Add(a); + } + } + + private static DoubleAnimationUsingKeyFrames MakeKeyFrameAnim((double val, double sec)[] frames) + { + var a = new DoubleAnimationUsingKeyFrames(); + foreach (var (val, sec) in frames) + a.KeyFrames.Add(new LinearDoubleKeyFrame(val, KT(sec))); + return a; + } + + // ─── 화면 배치 ──────────────────────────────────────────────────────────── + + private void CenterOnScreen() + { + var screen = SystemParameters.WorkArea; + // ActualHeight/ActualWidth는 첫 Show() 전 레이아웃 패스 이전에 0일 수 있음 → 기본값으로 보호 + var w = ActualWidth > 0 ? ActualWidth : 640; + var h = ActualHeight > 0 ? ActualHeight : 80; + Left = (screen.Width - w) / 2 + screen.Left; + Top = _vm.WindowPosition switch + { + "center" => (screen.Height - h) / 2 + screen.Top, + "bottom" => screen.Height * 0.75 + screen.Top, + _ => screen.Height * 0.2 + screen.Top, // "center-top" (기본) + }; + } + + // ─── 등장 애니메이션 ────────────────────────────────────────────────────── + + private void AnimateIn() + { + Opacity = 0; + + var ease = new CubicEase { EasingMode = EasingMode.EaseOut }; + + var fadeAnim = new DoubleAnimation(0, 1, + TimeSpan.FromMilliseconds(100)) { EasingFunction = ease }; + + var slideAnim = new DoubleAnimation(-8, 0, + TimeSpan.FromMilliseconds(120)) { EasingFunction = ease }; + + BeginAnimation(OpacityProperty, fadeAnim); + + // Window에 AllowsTransparency=True 일 때 RenderTransform을 Window에 직접 설정하면 + // InvalidOperationException 발생 → Content(루트 Border)에 적용 + if (Content is System.Windows.FrameworkElement root) + { + var translate = new TranslateTransform(0, -10); + root.RenderTransform = translate; + root.RenderTransformOrigin = new System.Windows.Point(0.5, 0); + translate.BeginAnimation(TranslateTransform.YProperty, slideAnim); + } + } +} diff --git a/src/AxCopilot/Views/LauncherWindow.Keyboard.cs b/src/AxCopilot/Views/LauncherWindow.Keyboard.cs new file mode 100644 index 0000000..3130485 --- /dev/null +++ b/src/AxCopilot/Views/LauncherWindow.Keyboard.cs @@ -0,0 +1,593 @@ +using System.Windows; +using System.Windows.Input; +using AxCopilot.Services; +using AxCopilot.ViewModels; + +namespace AxCopilot.Views; + +public partial class LauncherWindow +{ + // ─── IME 보완 검색 ──────────────────────────────────────────────────────── + + /// + /// WPF 바인딩(UpdateSourceTrigger=PropertyChanged)은 한글 IME 조합 중에는 + /// ViewModel 업데이트를 지연하므로, TextChanged에서 직접 검색을 트리거합니다. + /// InputText 프로퍼티를 건드리지 않아 IME 조합 상태(音節)가 유지됩니다. + /// + private void InputBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) + { + // 바인딩이 이미 ViewModel을 업데이트한 경우(조합 완료 후)에는 중복 실행 방지 + if (_vm.InputText == InputBox.Text) return; + // 조합 중 텍스트로 즉시 검색 — InputText 바인딩 우회 + _ = _vm.TriggerImeSearchAsync(InputBox.Text); + } + + // ─── 키보드 이벤트 ──────────────────────────────────────────────────────── + + /// + /// Window 레벨 PreviewKeyDown — 터널링으로 먼저 실행되므로 + /// TextBox 내부 ScrollViewer가 Up/Down을 소비하기 전에 인터셉트합니다. + /// + private void Window_PreviewKeyDown(object sender, KeyEventArgs e) + { + bool shift = (Keyboard.Modifiers & ModifierKeys.Shift) != 0; + + switch (e.Key) + { + case Key.Escape: + if (_vm.IsActionMode) + _vm.ExitActionMode(); + else + Hide(); + e.Handled = true; + break; + + case Key.Enter: + // Ctrl+Enter, Alt+Enter → Window_KeyDown에서 처리 + if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 || + (Keyboard.Modifiers & ModifierKeys.Alt) != 0) + return; + + if (shift) + { + // 퍼지 파일 검색 결과: Shift+Enter → 파일이 있는 폴더 열기 + if (_vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry shiftEntry) + { + var expanded = Environment.ExpandEnvironmentVariables(shiftEntry.Path); + Hide(); + // File.Exists/Directory.Exists 생략 — 탐색기가 없는 경로는 알아서 처리 + // 폴더인 경우 바로 열기, 파일인 경우 /select로 위치 표시 + _ = Task.Run(() => + { + try + { + if (shiftEntry.Type == Services.IndexEntryType.Folder) + System.Diagnostics.Process.Start("explorer.exe", $"\"{expanded}\""); + else + System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{expanded}\""); + } + catch (Exception) { } + }); + } + // 캡처 모드: 지연 캡처 타이머 표시 + else if (_vm.ActivePrefix != null && + _vm.ActivePrefix.Equals("cap", StringComparison.OrdinalIgnoreCase) && + _vm.ShowDelayTimerItems()) + { + // 타이머 선택 목록으로 전환됨 — Enter로 선택 + } + else if (_vm.MergeCount > 0) + _vm.ExecuteMerge(); + else + ShowLargeType(); + } + else if (_vm.IsActionMode && TryHandleSpecialAction()) + { + // 삭제/이름 변경 등 특수 액션 처리됨 — 별도 처리 + } + else + { + _ = _vm.ExecuteSelectedAsync(); + } + e.Handled = true; + break; + + case Key.Down: + if (shift) + { + _vm.ToggleMergeItem(_vm.SelectedItem); + _vm.SelectNext(); + } + else + { + _vm.SelectNext(); + } + ScrollToSelected(); + e.Handled = true; + break; + + case Key.Up: + if (shift) + { + _vm.ToggleMergeItem(_vm.SelectedItem); + _vm.SelectPrev(); + } + else + { + _vm.SelectPrev(); + } + ScrollToSelected(); + e.Handled = true; + break; + + case Key.Right: + // 커서가 입력 끝에 있고 선택된 항목이 파일/앱이면 액션 서브메뉴 진입 + if (InputBox.CaretIndex == InputBox.Text.Length + && InputBox.Text.Length > 0 + && _vm.CanEnterActionMode()) + { + _vm.EnterActionMode(_vm.SelectedItem!); + e.Handled = true; + } + break; + + case Key.PageDown: + for (int i = 0; i < 5 && _vm.Results.Count > 0; i++) _vm.SelectNext(); + ScrollToSelected(); + e.Handled = true; + break; + + case Key.PageUp: + for (int i = 0; i < 5 && _vm.Results.Count > 0; i++) _vm.SelectPrev(); + ScrollToSelected(); + e.Handled = true; + break; + + case Key.Home: + // 입력창 커서가 맨 앞이거나 입력이 없을 때 → 목록 첫 항목으로 이동 + if (InputBox.CaretIndex == 0 || string.IsNullOrEmpty(InputBox.Text)) + { + _vm.SelectFirst(); + ScrollToSelected(); + e.Handled = true; + } + break; + + case Key.End: + // 입력창 커서가 맨 끝이거나 입력이 없을 때 → 목록 마지막 항목으로 이동 + if (InputBox.CaretIndex == InputBox.Text.Length || string.IsNullOrEmpty(InputBox.Text)) + { + _vm.SelectLast(); + ScrollToSelected(); + e.Handled = true; + } + break; + + case Key.Tab: + // 자동완성: 선택된 항목의 Title을 입력창에 채우고 커서를 끝으로 이동 + if (_vm.SelectedItem != null) + { + _vm.InputText = _vm.SelectedItem.Title; + // 바인딩 업데이트 후 커서를 끝으로 — Dispatcher로 다음 렌더 사이클에 실행 + Dispatcher.BeginInvoke(() => + { + InputBox.CaretIndex = InputBox.Text.Length; + InputBox.Focus(); + }, System.Windows.Threading.DispatcherPriority.Input); + } + e.Handled = true; + break; + } + } + + private void Window_KeyDown(object sender, KeyEventArgs e) + { + var mod = Keyboard.Modifiers; + + // ─── Ctrl+, → 설정 창 열기 ───────────────────────────────────────── + if (e.Key == Key.OemComma && mod == ModifierKeys.Control) + { + Hide(); + OpenSettingsAction?.Invoke(); + e.Handled = true; + return; + } + + // ─── F1 → 도움말 창 열기 ──────────────────────────────────────────── + if (e.Key == Key.F1) + { + _vm.InputText = "help"; + e.Handled = true; + return; + } + + // ─── F5 → 인덱스 새로 고침 ────────────────────────────────────────── + if (e.Key == Key.F5) + { + var app = (App)System.Windows.Application.Current; + _ = app.IndexService?.BuildAsync(CancellationToken.None); + IndexStatusText.Text = "⟳ 인덱스 재구축 중…"; + IndexStatusText.Visibility = Visibility.Visible; + e.Handled = true; + return; + } + + // ─── Delete → 항목 삭제 ───────────────────────────────────────────── + if (e.Key == Key.Delete && mod == ModifierKeys.None) + { + if (_vm.SelectedItem != null) + { + var input = _vm.InputText ?? ""; + // note 예약어 활성 상태에서 메모 개별 삭제 + if (input.StartsWith("note", StringComparison.OrdinalIgnoreCase) + && _vm.SelectedItem.Data is string noteContent + && noteContent != "__CLEAR__") + { + var title = _vm.SelectedItem.Title; + var result = CustomMessageBox.Show( + $"'{title}' 메모를 삭제하시겠습니까?", + "AX Copilot", + MessageBoxButton.OKCancel, + MessageBoxImage.Question); + + if (result == MessageBoxResult.OK) + { + Handlers.NoteHandler.DeleteNote(noteContent); + // 결과 목록 새로고침 (InputText 재설정으로 SearchAsync 트리거) + var current = _vm.InputText ?? ""; + _vm.InputText = current + " "; + _vm.InputText = current; + } + } + else + { + var title = _vm.SelectedItem.Title; + var result = CustomMessageBox.Show( + $"'{title}' 항목을 목록에서 제거하시겠습니까?", + "AX Copilot", + MessageBoxButton.OKCancel, + MessageBoxImage.Question); + + if (result == MessageBoxResult.OK) + _vm.RemoveSelectedFromRecent(); + } + } + e.Handled = true; + return; + } + + // ─── Ctrl+L → 입력창 초기화 ───────────────────────────────────────── + if (e.Key == Key.L && mod == ModifierKeys.Control) + { + _vm.ClearInput(); + InputBox.Focus(); + e.Handled = true; + return; + } + + // ─── Ctrl+C → 선택 항목 이름 복사 (결과 선택 시) ──────────────────── + if (e.Key == Key.C && mod == ModifierKeys.Control && _vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry) + { + _vm.CopySelectedPath(); + ShowToast("이름 복사됨"); + e.Handled = true; + return; + } + + // ─── Ctrl+Shift+C → 전체 경로 복사 ────────────────────────────────── + if (e.Key == Key.C && mod == (ModifierKeys.Control | ModifierKeys.Shift)) + { + if (_vm.CopySelectedFullPath()) + ShowToast("경로 복사됨"); + e.Handled = true; + return; + } + + // ─── Ctrl+Shift+E → 파일 탐색기에서 열기 ──────────────────────────── + if (e.Key == Key.E && mod == (ModifierKeys.Control | ModifierKeys.Shift)) + { + if (_vm.OpenSelectedInExplorer()) + Hide(); + e.Handled = true; + return; + } + + // ─── Ctrl+Enter → 관리자 권한 실행 ────────────────────────────────── + if (e.Key == Key.Enter && mod == ModifierKeys.Control) + { + if (_vm.RunSelectedAsAdmin()) + Hide(); + e.Handled = true; + return; + } + + // ─── Alt+Enter → 파일 속성 보기 ───────────────────────────────────── + if (e.Key == Key.Enter && mod == ModifierKeys.Alt) + { + _vm.ShowSelectedProperties(); + e.Handled = true; + return; + } + + // ─── Ctrl+H → 클립보드 히스토리 ───────────────────────────────────── + if (e.Key == Key.H && mod == ModifierKeys.Control) + { + _vm.InputText = "#"; + Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, + System.Windows.Threading.DispatcherPriority.Input); + e.Handled = true; + return; + } + + // ─── Ctrl+R → 최근 실행 항목 ──────────────────────────────────────── + if (e.Key == Key.R && mod == ModifierKeys.Control) + { + _vm.InputText = "recent"; + Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, + System.Windows.Threading.DispatcherPriority.Input); + e.Handled = true; + return; + } + + // ─── Ctrl+B → 즐겨찾기 뷰 토글 (fav 이면 이전 검색으로, 아니면 fav로) ─ + if (e.Key == Key.B && mod == ModifierKeys.Control) + { + if (_vm.InputText.TrimStart().Equals("fav", StringComparison.OrdinalIgnoreCase)) + _vm.ClearInput(); + else + _vm.InputText = "fav"; + Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, + System.Windows.Threading.DispatcherPriority.Input); + e.Handled = true; + return; + } + + // ─── Ctrl+K → 단축키 도움말 모달 창 ───────────────────────────────── + if (e.Key == Key.K && mod == ModifierKeys.Control) + { + var helpWin = new ShortcutHelpWindow { Owner = this }; + helpWin.ShowDialog(); + e.Handled = true; + return; + } + + // ─── Ctrl+T → 터미널 열기 (선택 항목 경로, 없으면 홈) ──────────────── + if (e.Key == Key.T && mod == ModifierKeys.Control) + { + _vm.OpenSelectedInTerminal(); + Hide(); + e.Handled = true; + return; + } + + // ─── Ctrl+F → 파일 검색 모드 전환 ─────────────────────────────────── + if (e.Key == Key.F && mod == ModifierKeys.Control) + { + // 입력창 초기화 후 파일 타입 필터 힌트 + _vm.ClearInput(); + Dispatcher.BeginInvoke(() => + { + InputBox.Focus(); + InputBox.CaretIndex = InputBox.Text.Length; + }, System.Windows.Threading.DispatcherPriority.Input); + e.Handled = true; + return; + } + + // ─── Ctrl+P → 클립보드 모드에서 핀 토글 / 일반 모드에서 즐겨찾기 ─── + if (e.Key == Key.P && mod == ModifierKeys.Control) + { + if (_vm.IsClipboardMode && _vm.SelectedItem?.Data is Services.ClipboardEntry clipEntry) + { + var clipSvc = CurrentApp?.ClipboardHistoryService; + clipSvc?.TogglePin(clipEntry); + ShowToast(clipEntry.IsPinned ? "클립보드 핀 고정 📌" : "클립보드 핀 해제"); + // 검색 결과 갱신 + _vm.InputText = _vm.InputText; + } + else + { + var result = _vm.ToggleFavorite(); + if (result == true) + ShowToast("즐겨찾기에 추가됨 ⭐"); + else if (result == false) + ShowToast("즐겨찾기에서 제거됨"); + else + ShowToast("파일/폴더 항목을 선택하세요"); + } + e.Handled = true; + return; + } + + // ─── Ctrl+D → 다운로드 폴더 열기 ─────────────────────────────────── + if (e.Key == Key.D && mod == ModifierKeys.Control) + { + _vm.NavigateToDownloads(); + Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, + System.Windows.Threading.DispatcherPriority.Input); + e.Handled = true; + return; + } + + // ─── Ctrl+W → 런처 창 닫기 ────────────────────────────────────────── + if (e.Key == Key.W && mod == ModifierKeys.Control) + { + Hide(); + e.Handled = true; + return; + } + + // ─── F2 → 선택 파일 이름 바꾸기 ───────────────────────────────────── + if (e.Key == Key.F2) + { + if (_vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry entry) + { + var path = Environment.ExpandEnvironmentVariables(entry.Path); + _vm.InputText = $"rename {path}"; + Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, + System.Windows.Threading.DispatcherPriority.Input); + } + e.Handled = true; + return; + } + + // ─── Ctrl+1~9 → n번째 결과 즉시 실행 ─────────────────────────────── + if (mod == ModifierKeys.Control) + { + int num = e.Key switch + { + Key.D1 => 1, Key.D2 => 2, Key.D3 => 3, + Key.D4 => 4, Key.D5 => 5, Key.D6 => 6, + Key.D7 => 7, Key.D8 => 8, Key.D9 => 9, + _ => 0 + }; + if (num > 0 && num <= _vm.Results.Count) + { + _vm.SelectedItem = _vm.Results[num - 1]; + _ = _vm.ExecuteSelectedAsync(); + Hide(); + e.Handled = true; + return; + } + } + } + + // ─── 단축키 도움말 ──────────────────────────────────────────────────────── + + /// 단축키 도움말 팝업 + private void ShowShortcutHelp() + { + var lines = new[] + { + "[ 전역 ]", + "Alt+Space AX Commander 열기/닫기", + "", + "[ 탐색 ]", + "↑ / ↓ 결과 이동", + "Enter 선택 실행", + "Tab 자동완성", + "→ 액션 모드", + "Escape 닫기 / 뒤로", + "", + "[ 기능 ]", + "F1 도움말", + "F2 파일 이름 바꾸기", + "F5 인덱스 새로 고침", + "Delete 항목 제거", + "Ctrl+, 설정", + "Ctrl+L 입력 초기화", + "Ctrl+C 이름 복사", + "Ctrl+H 클립보드 히스토리", + "Ctrl+R 최근 실행", + "Ctrl+B 즐겨찾기", + "Ctrl+K 이 도움말", + "Ctrl+1~9 N번째 실행", + "Ctrl+Shift+C 경로 복사", + "Ctrl+Shift+E 탐색기에서 열기", + "Ctrl+Enter 관리자 실행", + "Alt+Enter 속성 보기", + "Shift+Enter 대형 텍스트", + }; + + CustomMessageBox.Show( + string.Join("\n", lines), + "AX Commander — 단축키 도움말", + MessageBoxButton.OK, + MessageBoxImage.Information); + } + + // ─── 토스트 알림 ────────────────────────────────────────────────────────── + + /// 오버레이 토스트 표시 (페이드인 → 2초 대기 → 페이드아웃) + private void ShowToast(string message, string icon = "\uE73E") + { + ToastText.Text = message; + ToastIcon.Text = icon; + ToastOverlay.Visibility = Visibility.Visible; + ToastOverlay.Opacity = 0; + + // 페이드인 + var fadeIn = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeIn"); + fadeIn.Begin(this); + + _indexStatusTimer?.Stop(); + _indexStatusTimer = new System.Windows.Threading.DispatcherTimer + { + Interval = TimeSpan.FromSeconds(2) + }; + _indexStatusTimer.Tick += (_, _) => + { + _indexStatusTimer.Stop(); + // 페이드아웃 후 Collapsed + var fadeOut = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeOut"); + EventHandler? onCompleted = null; + onCompleted = (__, ___) => + { + fadeOut.Completed -= onCompleted; + ToastOverlay.Visibility = Visibility.Collapsed; + }; + fadeOut.Completed += onCompleted; + fadeOut.Begin(this); + }; + _indexStatusTimer.Start(); + } + + // ─── 특수 액션 처리 ─────────────────────────────────────────────────────── + + /// + /// 액션 모드에서 특수 처리가 필요한 동작(삭제/이름변경)을 처리합니다. + /// 처리되면 true 반환 → ExecuteSelectedAsync 호출 생략. + /// + private bool TryHandleSpecialAction() + { + if (_vm.SelectedItem?.Data is not AxCopilot.ViewModels.FileActionData actionData) + return false; + + switch (actionData.Action) + { + case AxCopilot.ViewModels.FileAction.DeleteToRecycleBin: + { + var path = actionData.Path; + var name = System.IO.Path.GetFileName(path); + var r = CustomMessageBox.Show( + $"'{name}'\n\n이 항목을 휴지통으로 보내겠습니까?", + "AX Copilot — 삭제 확인", + MessageBoxButton.OKCancel, + MessageBoxImage.Warning); + + if (r == MessageBoxResult.OK) + { + try + { + SendToRecycleBin(path); + _vm.ExitActionMode(); + ShowToast("휴지통으로 이동됨", "\uE74D"); + } + catch (Exception ex) + { + CustomMessageBox.Show($"삭제 실패: {ex.Message}", "오류", + MessageBoxButton.OK, MessageBoxImage.Error); + } + } + else + { + _vm.ExitActionMode(); + } + return true; + } + + case AxCopilot.ViewModels.FileAction.Rename: + { + var path = actionData.Path; + _vm.ExitActionMode(); + _vm.InputText = $"rename {path}"; + Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, + System.Windows.Threading.DispatcherPriority.Input); + return true; + } + + default: + return false; + } + } +} diff --git a/src/AxCopilot/Views/LauncherWindow.Shell.cs b/src/AxCopilot/Views/LauncherWindow.Shell.cs new file mode 100644 index 0000000..2f96728 --- /dev/null +++ b/src/AxCopilot/Views/LauncherWindow.Shell.cs @@ -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; + + /// 파일·폴더를 Windows 휴지통으로 보냅니다. + 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(); + } + + // ─── 마우스 클릭 처리 ─────────────────────────────────────────────────── + + /// 이미 선택된 아이템을 클릭하면 Execute, 아직 선택되지 않은 아이템 클릭은 선택만. + 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을 통해 처리 + } +} diff --git a/src/AxCopilot/Views/LauncherWindow.Theme.cs b/src/AxCopilot/Views/LauncherWindow.Theme.cs new file mode 100644 index 0000000..a6296d7 --- /dev/null +++ b/src/AxCopilot/Views/LauncherWindow.Theme.cs @@ -0,0 +1,116 @@ +using System.Windows; +using System.Windows.Media; +using Microsoft.Win32; +using AxCopilot.Models; + +namespace AxCopilot.Views; + +public partial class LauncherWindow +{ + // ─── 테마 ──────────────────────────────────────────────────────────────── + + // 지원 테마 이름 목록 + private static readonly HashSet KnownThemes = + new(StringComparer.OrdinalIgnoreCase) + { "Dark", "Light", "OLED", "Nord", "Monokai", "Catppuccin", "Sepia", "Alfred", "AlfredLight", "Codex" }; + + internal void ApplyTheme() => + ApplyTheme(_vm.ThemeSetting, _vm.CustomThemeColors); + + /// + /// 테마를 즉시 교체합니다. 설정 창 미리보기에서도 호출됩니다. + /// + internal void ApplyTheme(string? themeKey, AxCopilot.Models.CustomThemeColors? customColors) + { + var appDicts = System.Windows.Application.Current.Resources.MergedDictionaries; + // 기존 테마 딕셔너리 제거 (Source 기반 또는 커스텀 빌드) + var existing = appDicts.FirstOrDefault(d => + d.Source?.ToString().Contains("/Themes/") == true || d.Contains("LauncherBackground")); + if (existing != null) + appDicts.Remove(existing); + + var setting = (themeKey ?? "system").ToLowerInvariant(); + + if (setting == "custom" && customColors != null) + { + appDicts.Add(BuildCustomDictionary(customColors)); + UpdateSelectionGlow(); // 커스텀 테마도 AccentColor 적용 + return; + } + + var themeName = GetEffectiveThemeName(setting); + appDicts.Add(new ResourceDictionary + { + Source = new Uri($"pack://application:,,,/Themes/{themeName}.xaml") + }); + UpdateSelectionGlow(); // 테마 변경 시 AccentColor 기반으로 글로우 색 갱신 + } + + private static string GetEffectiveThemeName(string setting) => setting switch + { + "dark" => "Dark", + "light" => "Light", + "oled" => "OLED", + "nord" => "Nord", + "monokai" => "Monokai", + "catppuccin" => "Catppuccin", + "sepia" => "Sepia", + "alfred" => "Alfred", + "alfredlight" => "AlfredLight", + "codex" => "Codex", + _ => IsSystemDarkMode() ? "Dark" : "Light" // "system" 또는 미지원 값 + }; + + private static ResourceDictionary BuildCustomDictionary(CustomThemeColors c) + { + SolidColorBrush Brush(string hex) + { + var color = (Color)ColorConverter.ConvertFromString(hex); + return new SolidColorBrush(color); + } + + return new ResourceDictionary + { + { "LauncherBackground", Brush(c.LauncherBackground) }, + { "ItemBackground", Brush(c.ItemBackground) }, + { "ItemSelectedBackground", Brush(c.ItemSelectedBackground) }, + { "ItemSelectedHoverBackground", LightenBrush(Brush(c.ItemSelectedBackground), 0.15) }, + { "ItemHoverBackground", Brush(c.ItemHoverBackground) }, + { "PrimaryText", Brush(c.PrimaryText) }, + { "SecondaryText", Brush(c.SecondaryText) }, + { "PlaceholderText", Brush(c.PlaceholderText) }, + { "AccentColor", Brush(c.AccentColor) }, + { "SeparatorColor", Brush(c.SeparatorColor) }, + { "HintBackground", Brush(c.HintBackground) }, + { "HintText", Brush(c.HintText) }, + { "BorderColor", Brush(c.BorderColor) }, + { "ScrollbarThumb", Brush(c.ScrollbarThumb) }, + { "ShadowColor", (Color)ColorConverter.ConvertFromString(c.ShadowColor) }, + // 커스텀 테마: 사용자가 설정한 라운딩 적용 + { "WindowCornerRadius", new CornerRadius(Math.Clamp(c.WindowCornerRadius, 0, 30)) }, + { "ItemCornerRadius", new CornerRadius(Math.Clamp(c.ItemCornerRadius, 0, 20)) }, + }; + } + + /// SolidColorBrush를 지정 비율만큼 밝게 합니다. + private static SolidColorBrush LightenBrush(SolidColorBrush brush, double amount) + { + var c = brush.Color; + byte Clamp(int v) => (byte)Math.Min(255, Math.Max(0, v)); + return new SolidColorBrush(Color.FromRgb( + Clamp(c.R + (int)(255 * amount)), + Clamp(c.G + (int)(255 * amount)), + Clamp(c.B + (int)(255 * amount)))); + } + + private static bool IsSystemDarkMode() + { + try + { + using var key = Registry.CurrentUser.OpenSubKey( + @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); + return key?.GetValue("AppsUseLightTheme") is int v && v == 0; + } + catch (Exception) { return true; } + } +} diff --git a/src/AxCopilot/Views/LauncherWindow.xaml.cs b/src/AxCopilot/Views/LauncherWindow.xaml.cs index 6b8b9ea..eb8b172 100644 --- a/src/AxCopilot/Views/LauncherWindow.xaml.cs +++ b/src/AxCopilot/Views/LauncherWindow.xaml.cs @@ -575,989 +575,4 @@ public partial class LauncherWindow : Window ApplyRandomIconAnimation(); } - // ─── 무지개 글로우 상시 애니메이션 ──────────────────────────────────── - - /// 선택 아이템 상시 무지개 글로우 효과를 적용하거나 제거합니다. - private void UpdateSelectionGlow() - { - if (_vm.EnableSelectionGlow) - { - var gs = new System.Windows.Media.GradientStopCollection - { - new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x6B, 0x6B), 0.00), - new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFE, 0xCA, 0x57), 0.17), - new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x48, 0xDB, 0xFB), 0.33), - new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x9F, 0xF3), 0.50), - new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x54, 0xA0, 0xFF), 0.67), - new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0x5F, 0x27, 0xCD), 0.83), - new System.Windows.Media.GradientStop(System.Windows.Media.Color.FromRgb(0xFF, 0x6B, 0x6B), 1.00), - }; - Resources["SelectionGlowBrush"] = new System.Windows.Media.LinearGradientBrush( - gs, - new System.Windows.Point(0, 0), - new System.Windows.Point(1, 1)); - Resources["SelectionGlowVisibility"] = Visibility.Visible; - } - else - { - Resources["SelectionGlowBrush"] = System.Windows.Media.Brushes.Transparent; - Resources["SelectionGlowVisibility"] = Visibility.Collapsed; - } - } - - /// 무지개 글로우를 정지하고 숨깁니다. - private void StopRainbowGlow() - { - _rainbowTimer?.Stop(); - _rainbowTimer = null; - if (RainbowGlowBorder != null) RainbowGlowBorder.Opacity = 0; - } - - /// 런처 테두리 무지개 그라데이션 회전을 시작합니다. - private void StartRainbowGlow() - { - _rainbowTimer?.Stop(); - if (LauncherRainbowBrush == null || RainbowGlowBorder == null) return; - - _rainbowTimer = new System.Windows.Threading.DispatcherTimer - { - Interval = TimeSpan.FromMilliseconds(20) - }; - var startTime = DateTime.UtcNow; - _rainbowTimer.Tick += (_, _) => - { - if (!IsVisible) { _rainbowTimer?.Stop(); return; } - var elapsed = (DateTime.UtcNow - startTime).TotalMilliseconds; - var shift = (elapsed / 2000.0) % 1.0; // 2초에 1바퀴 (느리게) - var angle = shift * Math.PI * 2; - LauncherRainbowBrush.StartPoint = new Point(0.5 + 0.5 * Math.Cos(angle), 0.5 + 0.5 * Math.Sin(angle)); - LauncherRainbowBrush.EndPoint = new Point(0.5 - 0.5 * Math.Cos(angle), 0.5 - 0.5 * Math.Sin(angle)); - }; - _rainbowTimer.Start(); - } - - // ─── 애니메이션 헬퍼 ────────────────────────────────────────────────── - - private static KeyTime KT(double sec) => KeyTime.FromTimeSpan(TimeSpan.FromSeconds(sec)); - - private static void AddOpacityPulse(Storyboard sb, UIElement target, int index, double totalSec) - { - var a = new DoubleAnimationUsingKeyFrames(); - a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(index))); - a.KeyFrames.Add(new LinearDoubleKeyFrame(0.25, KT(index + 0.5))); - a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(index + 1))); - a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(totalSec))); - Storyboard.SetTarget(a, target); - Storyboard.SetTargetProperty(a, new PropertyPath(UIElement.OpacityProperty)); - sb.Children.Add(a); - } - - private static void AddGroupFlash(Storyboard sb, UIElement[] group, double startSec, double totalSec) - { - foreach (var p in group) - { - var a = new DoubleAnimationUsingKeyFrames(); - a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(0))); - a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(startSec))); - a.KeyFrames.Add(new LinearDoubleKeyFrame(0.2, KT(startSec + 0.6))); - a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(startSec + 1.2))); - a.KeyFrames.Add(new LinearDoubleKeyFrame(1, KT(totalSec))); - Storyboard.SetTarget(a, p); - Storyboard.SetTargetProperty(a, new PropertyPath(UIElement.OpacityProperty)); - sb.Children.Add(a); - } - } - - private static DoubleAnimationUsingKeyFrames MakeKeyFrameAnim((double val, double sec)[] frames) - { - var a = new DoubleAnimationUsingKeyFrames(); - foreach (var (val, sec) in frames) - a.KeyFrames.Add(new LinearDoubleKeyFrame(val, KT(sec))); - return a; - } - - private void CenterOnScreen() - { - var screen = SystemParameters.WorkArea; - // ActualHeight/ActualWidth는 첫 Show() 전 레이아웃 패스 이전에 0일 수 있음 → 기본값으로 보호 - var w = ActualWidth > 0 ? ActualWidth : 640; - var h = ActualHeight > 0 ? ActualHeight : 80; - Left = (screen.Width - w) / 2 + screen.Left; - Top = _vm.WindowPosition switch - { - "center" => (screen.Height - h) / 2 + screen.Top, - "bottom" => screen.Height * 0.75 + screen.Top, - _ => screen.Height * 0.2 + screen.Top, // "center-top" (기본) - }; - } - - // 지원 테마 이름 목록 - private static readonly HashSet KnownThemes = - new(StringComparer.OrdinalIgnoreCase) - { "Dark", "Light", "OLED", "Nord", "Monokai", "Catppuccin", "Sepia", "Alfred", "AlfredLight", "Codex" }; - - internal void ApplyTheme() => - ApplyTheme(_vm.ThemeSetting, _vm.CustomThemeColors); - - /// - /// 테마를 즉시 교체합니다. 설정 창 미리보기에서도 호출됩니다. - /// - internal void ApplyTheme(string? themeKey, AxCopilot.Models.CustomThemeColors? customColors) - { - var appDicts = System.Windows.Application.Current.Resources.MergedDictionaries; - // 기존 테마 딕셔너리 제거 (Source 기반 또는 커스텀 빌드) - var existing = appDicts.FirstOrDefault(d => - d.Source?.ToString().Contains("/Themes/") == true || d.Contains("LauncherBackground")); - if (existing != null) - appDicts.Remove(existing); - - var setting = (themeKey ?? "system").ToLowerInvariant(); - - if (setting == "custom" && customColors != null) - { - appDicts.Add(BuildCustomDictionary(customColors)); - UpdateSelectionGlow(); // 커스텀 테마도 AccentColor 적용 - return; - } - - var themeName = GetEffectiveThemeName(setting); - appDicts.Add(new ResourceDictionary - { - Source = new Uri($"pack://application:,,,/Themes/{themeName}.xaml") - }); - UpdateSelectionGlow(); // 테마 변경 시 AccentColor 기반으로 글로우 색 갱신 - } - - private static string GetEffectiveThemeName(string setting) => setting switch - { - "dark" => "Dark", - "light" => "Light", - "oled" => "OLED", - "nord" => "Nord", - "monokai" => "Monokai", - "catppuccin" => "Catppuccin", - "sepia" => "Sepia", - "alfred" => "Alfred", - "alfredlight" => "AlfredLight", - "codex" => "Codex", - _ => IsSystemDarkMode() ? "Dark" : "Light" // "system" 또는 미지원 값 - }; - - private static ResourceDictionary BuildCustomDictionary(CustomThemeColors c) - { - SolidColorBrush Brush(string hex) - { - var color = (Color)ColorConverter.ConvertFromString(hex); - return new SolidColorBrush(color); - } - - return new ResourceDictionary - { - { "LauncherBackground", Brush(c.LauncherBackground) }, - { "ItemBackground", Brush(c.ItemBackground) }, - { "ItemSelectedBackground", Brush(c.ItemSelectedBackground) }, - { "ItemSelectedHoverBackground", LightenBrush(Brush(c.ItemSelectedBackground), 0.15) }, - { "ItemHoverBackground", Brush(c.ItemHoverBackground) }, - { "PrimaryText", Brush(c.PrimaryText) }, - { "SecondaryText", Brush(c.SecondaryText) }, - { "PlaceholderText", Brush(c.PlaceholderText) }, - { "AccentColor", Brush(c.AccentColor) }, - { "SeparatorColor", Brush(c.SeparatorColor) }, - { "HintBackground", Brush(c.HintBackground) }, - { "HintText", Brush(c.HintText) }, - { "BorderColor", Brush(c.BorderColor) }, - { "ScrollbarThumb", Brush(c.ScrollbarThumb) }, - { "ShadowColor", (Color)ColorConverter.ConvertFromString(c.ShadowColor) }, - // 커스텀 테마: 사용자가 설정한 라운딩 적용 - { "WindowCornerRadius", new CornerRadius(Math.Clamp(c.WindowCornerRadius, 0, 30)) }, - { "ItemCornerRadius", new CornerRadius(Math.Clamp(c.ItemCornerRadius, 0, 20)) }, - }; - } - - /// SolidColorBrush를 지정 비율만큼 밝게 합니다. - private static SolidColorBrush LightenBrush(SolidColorBrush brush, double amount) - { - var c = brush.Color; - byte Clamp(int v) => (byte)Math.Min(255, Math.Max(0, v)); - return new SolidColorBrush(Color.FromRgb( - Clamp(c.R + (int)(255 * amount)), - Clamp(c.G + (int)(255 * amount)), - Clamp(c.B + (int)(255 * amount)))); - } - - private static bool IsSystemDarkMode() - { - try - { - using var key = Registry.CurrentUser.OpenSubKey( - @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"); - return key?.GetValue("AppsUseLightTheme") is int v && v == 0; - } - catch (Exception) { return true; } - } - - private void AnimateIn() - { - Opacity = 0; - - var ease = new System.Windows.Media.Animation.CubicEase - { EasingMode = System.Windows.Media.Animation.EasingMode.EaseOut }; - - var fadeAnim = new System.Windows.Media.Animation.DoubleAnimation(0, 1, - TimeSpan.FromMilliseconds(100)) { EasingFunction = ease }; - - var slideAnim = new System.Windows.Media.Animation.DoubleAnimation(-8, 0, - TimeSpan.FromMilliseconds(120)) { EasingFunction = ease }; - - BeginAnimation(OpacityProperty, fadeAnim); - - // Window에 AllowsTransparency=True 일 때 RenderTransform을 Window에 직접 설정하면 - // InvalidOperationException 발생 → Content(루트 Border)에 적용 - if (Content is System.Windows.FrameworkElement root) - { - var translate = new System.Windows.Media.TranslateTransform(0, -10); - root.RenderTransform = translate; - root.RenderTransformOrigin = new System.Windows.Point(0.5, 0); - translate.BeginAnimation(System.Windows.Media.TranslateTransform.YProperty, slideAnim); - } - } - - // ─── IME 보완 검색 ──────────────────────────────────────────────────────── - - /// - /// WPF 바인딩(UpdateSourceTrigger=PropertyChanged)은 한글 IME 조합 중에는 - /// ViewModel 업데이트를 지연하므로, TextChanged에서 직접 검색을 트리거합니다. - /// InputText 프로퍼티를 건드리지 않아 IME 조합 상태(音節)가 유지됩니다. - /// - private void InputBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) - { - // 바인딩이 이미 ViewModel을 업데이트한 경우(조합 완료 후)에는 중복 실행 방지 - if (_vm.InputText == InputBox.Text) return; - // 조합 중 텍스트로 즉시 검색 — InputText 바인딩 우회 - _ = _vm.TriggerImeSearchAsync(InputBox.Text); - } - - // ─── 키보드 이벤트 ──────────────────────────────────────────────────────── - - /// - /// Window 레벨 PreviewKeyDown — 터널링으로 먼저 실행되므로 - /// TextBox 내부 ScrollViewer가 Up/Down을 소비하기 전에 인터셉트합니다. - /// - private void Window_PreviewKeyDown(object sender, KeyEventArgs e) - { - bool shift = (Keyboard.Modifiers & ModifierKeys.Shift) != 0; - - switch (e.Key) - { - case Key.Escape: - if (_vm.IsActionMode) - _vm.ExitActionMode(); - else - Hide(); - e.Handled = true; - break; - - case Key.Enter: - // Ctrl+Enter, Alt+Enter → Window_KeyDown에서 처리 - if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 || - (Keyboard.Modifiers & ModifierKeys.Alt) != 0) - return; - - if (shift) - { - // 퍼지 파일 검색 결과: Shift+Enter → 파일이 있는 폴더 열기 - if (_vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry shiftEntry) - { - var expanded = Environment.ExpandEnvironmentVariables(shiftEntry.Path); - Hide(); - // File.Exists/Directory.Exists 생략 — 탐색기가 없는 경로는 알아서 처리 - // 폴더인 경우 바로 열기, 파일인 경우 /select로 위치 표시 - _ = Task.Run(() => - { - try - { - if (shiftEntry.Type == Services.IndexEntryType.Folder) - System.Diagnostics.Process.Start("explorer.exe", $"\"{expanded}\""); - else - System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{expanded}\""); - } - catch (Exception) { } - }); - } - // 캡처 모드: 지연 캡처 타이머 표시 - else if (_vm.ActivePrefix != null && - _vm.ActivePrefix.Equals("cap", StringComparison.OrdinalIgnoreCase) && - _vm.ShowDelayTimerItems()) - { - // 타이머 선택 목록으로 전환됨 — Enter로 선택 - } - else if (_vm.MergeCount > 0) - _vm.ExecuteMerge(); - else - ShowLargeType(); - } - else if (_vm.IsActionMode && TryHandleSpecialAction()) - { - // 삭제/이름 변경 등 특수 액션 처리됨 — 별도 처리 - } - else - { - _ = _vm.ExecuteSelectedAsync(); - } - e.Handled = true; - break; - - case Key.Down: - if (shift) - { - _vm.ToggleMergeItem(_vm.SelectedItem); - _vm.SelectNext(); - } - else - { - _vm.SelectNext(); - } - ScrollToSelected(); - e.Handled = true; - break; - - case Key.Up: - if (shift) - { - _vm.ToggleMergeItem(_vm.SelectedItem); - _vm.SelectPrev(); - } - else - { - _vm.SelectPrev(); - } - ScrollToSelected(); - e.Handled = true; - break; - - case Key.Right: - // 커서가 입력 끝에 있고 선택된 항목이 파일/앱이면 액션 서브메뉴 진입 - if (InputBox.CaretIndex == InputBox.Text.Length - && InputBox.Text.Length > 0 - && _vm.CanEnterActionMode()) - { - _vm.EnterActionMode(_vm.SelectedItem!); - e.Handled = true; - } - break; - - case Key.PageDown: - for (int i = 0; i < 5 && _vm.Results.Count > 0; i++) _vm.SelectNext(); - ScrollToSelected(); - e.Handled = true; - break; - - case Key.PageUp: - for (int i = 0; i < 5 && _vm.Results.Count > 0; i++) _vm.SelectPrev(); - ScrollToSelected(); - e.Handled = true; - break; - - case Key.Home: - // 입력창 커서가 맨 앞이거나 입력이 없을 때 → 목록 첫 항목으로 이동 - if (InputBox.CaretIndex == 0 || string.IsNullOrEmpty(InputBox.Text)) - { - _vm.SelectFirst(); - ScrollToSelected(); - e.Handled = true; - } - break; - - case Key.End: - // 입력창 커서가 맨 끝이거나 입력이 없을 때 → 목록 마지막 항목으로 이동 - if (InputBox.CaretIndex == InputBox.Text.Length || string.IsNullOrEmpty(InputBox.Text)) - { - _vm.SelectLast(); - ScrollToSelected(); - e.Handled = true; - } - break; - - case Key.Tab: - // 자동완성: 선택된 항목의 Title을 입력창에 채우고 커서를 끝으로 이동 - if (_vm.SelectedItem != null) - { - _vm.InputText = _vm.SelectedItem.Title; - // 바인딩 업데이트 후 커서를 끝으로 — Dispatcher로 다음 렌더 사이클에 실행 - Dispatcher.BeginInvoke(() => - { - InputBox.CaretIndex = InputBox.Text.Length; - InputBox.Focus(); - }, System.Windows.Threading.DispatcherPriority.Input); - } - e.Handled = true; - break; - } - } - - private void Window_KeyDown(object sender, KeyEventArgs e) - { - var mod = Keyboard.Modifiers; - - // ─── Ctrl+, → 설정 창 열기 ───────────────────────────────────────── - if (e.Key == Key.OemComma && mod == ModifierKeys.Control) - { - Hide(); - OpenSettingsAction?.Invoke(); - e.Handled = true; - return; - } - - // ─── F1 → 도움말 창 열기 ──────────────────────────────────────────── - if (e.Key == Key.F1) - { - _vm.InputText = "help"; - e.Handled = true; - return; - } - - // ─── F5 → 인덱스 새로 고침 ────────────────────────────────────────── - if (e.Key == Key.F5) - { - var app = (App)System.Windows.Application.Current; - _ = app.IndexService?.BuildAsync(CancellationToken.None); - IndexStatusText.Text = "⟳ 인덱스 재구축 중…"; - IndexStatusText.Visibility = Visibility.Visible; - e.Handled = true; - return; - } - - // ─── Delete → 항목 삭제 ───────────────────────────────────────────── - if (e.Key == Key.Delete && mod == ModifierKeys.None) - { - if (_vm.SelectedItem != null) - { - var input = _vm.InputText ?? ""; - // note 예약어 활성 상태에서 메모 개별 삭제 - if (input.StartsWith("note", StringComparison.OrdinalIgnoreCase) - && _vm.SelectedItem.Data is string noteContent - && noteContent != "__CLEAR__") - { - var title = _vm.SelectedItem.Title; - var result = CustomMessageBox.Show( - $"'{title}' 메모를 삭제하시겠습니까?", - "AX Copilot", - MessageBoxButton.OKCancel, - MessageBoxImage.Question); - - if (result == MessageBoxResult.OK) - { - Handlers.NoteHandler.DeleteNote(noteContent); - // 결과 목록 새로고침 (InputText 재설정으로 SearchAsync 트리거) - var current = _vm.InputText ?? ""; - _vm.InputText = current + " "; - _vm.InputText = current; - } - } - else - { - var title = _vm.SelectedItem.Title; - var result = CustomMessageBox.Show( - $"'{title}' 항목을 목록에서 제거하시겠습니까?", - "AX Copilot", - MessageBoxButton.OKCancel, - MessageBoxImage.Question); - - if (result == MessageBoxResult.OK) - _vm.RemoveSelectedFromRecent(); - } - } - e.Handled = true; - return; - } - - // ─── Ctrl+L → 입력창 초기화 ───────────────────────────────────────── - if (e.Key == Key.L && mod == ModifierKeys.Control) - { - _vm.ClearInput(); - InputBox.Focus(); - e.Handled = true; - return; - } - - // ─── Ctrl+C → 선택 항목 이름 복사 (결과 선택 시) ──────────────────── - if (e.Key == Key.C && mod == ModifierKeys.Control && _vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry) - { - _vm.CopySelectedPath(); - ShowToast("이름 복사됨"); - e.Handled = true; - return; - } - - // ─── Ctrl+Shift+C → 전체 경로 복사 ────────────────────────────────── - if (e.Key == Key.C && mod == (ModifierKeys.Control | ModifierKeys.Shift)) - { - if (_vm.CopySelectedFullPath()) - ShowToast("경로 복사됨"); - e.Handled = true; - return; - } - - // ─── Ctrl+Shift+E → 파일 탐색기에서 열기 ──────────────────────────── - if (e.Key == Key.E && mod == (ModifierKeys.Control | ModifierKeys.Shift)) - { - if (_vm.OpenSelectedInExplorer()) - Hide(); - e.Handled = true; - return; - } - - // ─── Ctrl+Enter → 관리자 권한 실행 ────────────────────────────────── - if (e.Key == Key.Enter && mod == ModifierKeys.Control) - { - if (_vm.RunSelectedAsAdmin()) - Hide(); - e.Handled = true; - return; - } - - // ─── Alt+Enter → 파일 속성 보기 ───────────────────────────────────── - if (e.Key == Key.Enter && mod == ModifierKeys.Alt) - { - _vm.ShowSelectedProperties(); - e.Handled = true; - return; - } - - // ─── Ctrl+H → 클립보드 히스토리 ───────────────────────────────────── - if (e.Key == Key.H && mod == ModifierKeys.Control) - { - _vm.InputText = "#"; - Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, - System.Windows.Threading.DispatcherPriority.Input); - e.Handled = true; - return; - } - - // ─── Ctrl+R → 최근 실행 항목 ──────────────────────────────────────── - if (e.Key == Key.R && mod == ModifierKeys.Control) - { - _vm.InputText = "recent"; - Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, - System.Windows.Threading.DispatcherPriority.Input); - e.Handled = true; - return; - } - - // ─── Ctrl+B → 즐겨찾기 뷰 토글 (fav 이면 이전 검색으로, 아니면 fav로) ─ - if (e.Key == Key.B && mod == ModifierKeys.Control) - { - if (_vm.InputText.TrimStart().Equals("fav", StringComparison.OrdinalIgnoreCase)) - _vm.ClearInput(); - else - _vm.InputText = "fav"; - Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, - System.Windows.Threading.DispatcherPriority.Input); - e.Handled = true; - return; - } - - // ─── Ctrl+K → 단축키 도움말 모달 창 ───────────────────────────────── - if (e.Key == Key.K && mod == ModifierKeys.Control) - { - var helpWin = new ShortcutHelpWindow { Owner = this }; - helpWin.ShowDialog(); - e.Handled = true; - return; - } - - // ─── Ctrl+T → 터미널 열기 (선택 항목 경로, 없으면 홈) ──────────────── - if (e.Key == Key.T && mod == ModifierKeys.Control) - { - _vm.OpenSelectedInTerminal(); - Hide(); - e.Handled = true; - return; - } - - // ─── Ctrl+F → 파일 검색 모드 전환 ─────────────────────────────────── - if (e.Key == Key.F && mod == ModifierKeys.Control) - { - // 입력창 초기화 후 파일 타입 필터 힌트 - _vm.ClearInput(); - Dispatcher.BeginInvoke(() => - { - InputBox.Focus(); - InputBox.CaretIndex = InputBox.Text.Length; - }, System.Windows.Threading.DispatcherPriority.Input); - e.Handled = true; - return; - } - - // ─── Ctrl+P → 클립보드 모드에서 핀 토글 / 일반 모드에서 즐겨찾기 ─── - if (e.Key == Key.P && mod == ModifierKeys.Control) - { - if (_vm.IsClipboardMode && _vm.SelectedItem?.Data is Services.ClipboardEntry clipEntry) - { - var clipSvc = CurrentApp?.ClipboardHistoryService; - clipSvc?.TogglePin(clipEntry); - ShowToast(clipEntry.IsPinned ? "클립보드 핀 고정 📌" : "클립보드 핀 해제"); - // 검색 결과 갱신 - _vm.InputText = _vm.InputText; - } - else - { - var result = _vm.ToggleFavorite(); - if (result == true) - ShowToast("즐겨찾기에 추가됨 ⭐"); - else if (result == false) - ShowToast("즐겨찾기에서 제거됨"); - else - ShowToast("파일/폴더 항목을 선택하세요"); - } - e.Handled = true; - return; - } - - // ─── Ctrl+D → 다운로드 폴더 열기 ─────────────────────────────────── - if (e.Key == Key.D && mod == ModifierKeys.Control) - { - _vm.NavigateToDownloads(); - Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, - System.Windows.Threading.DispatcherPriority.Input); - e.Handled = true; - return; - } - - // ─── Ctrl+W → 런처 창 닫기 ────────────────────────────────────────── - if (e.Key == Key.W && mod == ModifierKeys.Control) - { - Hide(); - e.Handled = true; - return; - } - - // ─── F2 → 선택 파일 이름 바꾸기 ───────────────────────────────────── - if (e.Key == Key.F2) - { - if (_vm.SelectedItem?.Data is AxCopilot.Services.IndexEntry entry) - { - var path = Environment.ExpandEnvironmentVariables(entry.Path); - _vm.InputText = $"rename {path}"; - Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, - System.Windows.Threading.DispatcherPriority.Input); - } - e.Handled = true; - return; - } - - // ─── Ctrl+1~9 → n번째 결과 즉시 실행 ─────────────────────────────── - if (mod == ModifierKeys.Control) - { - int num = e.Key switch - { - Key.D1 => 1, Key.D2 => 2, Key.D3 => 3, - Key.D4 => 4, Key.D5 => 5, Key.D6 => 6, - Key.D7 => 7, Key.D8 => 8, Key.D9 => 9, - _ => 0 - }; - if (num > 0 && num <= _vm.Results.Count) - { - _vm.SelectedItem = _vm.Results[num - 1]; - _ = _vm.ExecuteSelectedAsync(); - Hide(); - e.Handled = true; - return; - } - } - } - - /// 단축키 도움말 팝업 - private void ShowShortcutHelp() - { - var lines = new[] - { - "[ 전역 ]", - "Alt+Space AX Commander 열기/닫기", - "", - "[ 탐색 ]", - "↑ / ↓ 결과 이동", - "Enter 선택 실행", - "Tab 자동완성", - "→ 액션 모드", - "Escape 닫기 / 뒤로", - "", - "[ 기능 ]", - "F1 도움말", - "F2 파일 이름 바꾸기", - "F5 인덱스 새로 고침", - "Delete 항목 제거", - "Ctrl+, 설정", - "Ctrl+L 입력 초기화", - "Ctrl+C 이름 복사", - "Ctrl+H 클립보드 히스토리", - "Ctrl+R 최근 실행", - "Ctrl+B 즐겨찾기", - "Ctrl+K 이 도움말", - "Ctrl+1~9 N번째 실행", - "Ctrl+Shift+C 경로 복사", - "Ctrl+Shift+E 탐색기에서 열기", - "Ctrl+Enter 관리자 실행", - "Alt+Enter 속성 보기", - "Shift+Enter 대형 텍스트", - }; - - CustomMessageBox.Show( - string.Join("\n", lines), - "AX Commander — 단축키 도움말", - MessageBoxButton.OK, - MessageBoxImage.Information); - } - - /// 오버레이 토스트 표시 (페이드인 → 2초 대기 → 페이드아웃) - private void ShowToast(string message, string icon = "\uE73E") - { - ToastText.Text = message; - ToastIcon.Text = icon; - ToastOverlay.Visibility = Visibility.Visible; - ToastOverlay.Opacity = 0; - - // 페이드인 - var fadeIn = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeIn"); - fadeIn.Begin(this); - - _indexStatusTimer?.Stop(); - _indexStatusTimer = new System.Windows.Threading.DispatcherTimer - { - Interval = TimeSpan.FromSeconds(2) - }; - _indexStatusTimer.Tick += (_, _) => - { - _indexStatusTimer.Stop(); - // 페이드아웃 후 Collapsed - var fadeOut = (System.Windows.Media.Animation.Storyboard)FindResource("ToastFadeOut"); - EventHandler? onCompleted = null; - onCompleted = (__, ___) => - { - fadeOut.Completed -= onCompleted; - ToastOverlay.Visibility = Visibility.Collapsed; - }; - fadeOut.Completed += onCompleted; - fadeOut.Begin(this); - }; - _indexStatusTimer.Start(); - } - - /// - /// 액션 모드에서 특수 처리가 필요한 동작(삭제/이름변경)을 처리합니다. - /// 처리되면 true 반환 → ExecuteSelectedAsync 호출 생략. - /// - private bool TryHandleSpecialAction() - { - if (_vm.SelectedItem?.Data is not AxCopilot.ViewModels.FileActionData actionData) - return false; - - switch (actionData.Action) - { - case AxCopilot.ViewModels.FileAction.DeleteToRecycleBin: - { - var path = actionData.Path; - var name = System.IO.Path.GetFileName(path); - var r = CustomMessageBox.Show( - $"'{name}'\n\n이 항목을 휴지통으로 보내겠습니까?", - "AX Copilot — 삭제 확인", - MessageBoxButton.OKCancel, - MessageBoxImage.Warning); - - if (r == MessageBoxResult.OK) - { - try - { - SendToRecycleBin(path); - _vm.ExitActionMode(); - ShowToast("휴지통으로 이동됨", "\uE74D"); - } - catch (Exception ex) - { - CustomMessageBox.Show($"삭제 실패: {ex.Message}", "오류", - MessageBoxButton.OK, MessageBoxImage.Error); - } - } - else - { - _vm.ExitActionMode(); - } - return true; - } - - case AxCopilot.ViewModels.FileAction.Rename: - { - var path = actionData.Path; - _vm.ExitActionMode(); - _vm.InputText = $"rename {path}"; - Dispatcher.BeginInvoke(() => { InputBox.CaretIndex = InputBox.Text.Length; }, - System.Windows.Threading.DispatcherPriority.Input); - return true; - } - - default: - return false; - } - } - - // ─── 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; - - /// 파일·폴더를 Windows 휴지통으로 보냅니다. - private void SendToRecycleBin(string path) - { - // pFrom은 null-terminated + 추가 null 필요 - var op = new SHFILEOPSTRUCT - { - hwnd = new System.Windows.Interop.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(); - } - - /// 이미 선택된 아이템을 클릭하면 Execute, 아직 선택되지 않은 아이템 클릭은 선택만. - 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을 통해 처리 - } } diff --git a/src/AxCopilot/Views/ModelRegistrationDialog.cs b/src/AxCopilot/Views/ModelRegistrationDialog.cs index 29df61b..8f1ddff 100644 --- a/src/AxCopilot/Views/ModelRegistrationDialog.cs +++ b/src/AxCopilot/Views/ModelRegistrationDialog.cs @@ -78,7 +78,7 @@ internal sealed class ModelRegistrationDialog : Window header.Children.Add(new TextBlock { Text = "\uEA86", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, @@ -323,7 +323,7 @@ internal sealed class ModelRegistrationDialog : Window securityNote.Children.Add(new TextBlock { Text = "\uE72E", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 11, Foreground = new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/PlanViewerWindow.cs b/src/AxCopilot/Views/PlanViewerWindow.cs index 268069a..373fc02 100644 --- a/src/AxCopilot/Views/PlanViewerWindow.cs +++ b/src/AxCopilot/Views/PlanViewerWindow.cs @@ -91,7 +91,7 @@ internal sealed class PlanViewerWindow : Window var titleSp = new StackPanel { Orientation = Orientation.Horizontal }; titleSp.Children.Add(new TextBlock { - Text = "\uE9D2", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE9D2", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), }); @@ -109,7 +109,7 @@ internal sealed class PlanViewerWindow : Window HorizontalAlignment = HorizontalAlignment.Right, VerticalAlignment = VerticalAlignment.Center, Child = new TextBlock { - Text = "\uE8BB", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE8BB", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = secondaryText, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, }, @@ -222,7 +222,7 @@ internal sealed class PlanViewerWindow : Window var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { - Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 9, Foreground = fg, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 4, 0), }); @@ -408,7 +408,7 @@ internal sealed class PlanViewerWindow : Window Child = new TextBlock { Text = "\uE8FD", // Sort/Lines 아이콘 (드래그 핸들) - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 11, Foreground = dimBrush, HorizontalAlignment = HorizontalAlignment.Center, @@ -493,7 +493,7 @@ internal sealed class PlanViewerWindow : Window { badge = new TextBlock { - Text = "\uE73E", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE73E", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 13, Foreground = new SolidColorBrush(Color.FromRgb(0x10, 0xB9, 0x81)), VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), Width = 20, TextAlignment = TextAlignment.Center, @@ -503,7 +503,7 @@ internal sealed class PlanViewerWindow : Window { badge = new TextBlock { - Text = "\uE768", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE768", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 13, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), Width = 20, TextAlignment = TextAlignment.Center, @@ -563,7 +563,7 @@ internal sealed class PlanViewerWindow : Window Child = new TextBlock { Text = isExpanded ? "\uE70E" : "\uE70D", // ChevronUp / ChevronDown - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 9, Foreground = new SolidColorBrush(Color.FromArgb(0x70, 0x80, 0x80, 0x80)), HorizontalAlignment = HorizontalAlignment.Center, @@ -653,7 +653,7 @@ internal sealed class PlanViewerWindow : Window var addSp = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center }; addSp.Children.Add(new TextBlock { - Text = "\uE710", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE710", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = st2, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); @@ -884,7 +884,7 @@ internal sealed class PlanViewerWindow : Window Margin = new Thickness(1, 0, 1, 0), Child = new TextBlock { - Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10, Foreground = fg, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, @@ -914,7 +914,7 @@ internal sealed class PlanViewerWindow : Window var sp = new StackPanel { Orientation = Orientation.Horizontal }; sp.Children.Add(new TextBlock { - Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = icon, FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = filled ? Brushes.White : textColor, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0), }); diff --git a/src/AxCopilot/Views/PreviewWindow.xaml.cs b/src/AxCopilot/Views/PreviewWindow.xaml.cs index f2f42db..384de58 100644 --- a/src/AxCopilot/Views/PreviewWindow.xaml.cs +++ b/src/AxCopilot/Views/PreviewWindow.xaml.cs @@ -248,7 +248,7 @@ public partial class PreviewWindow : Window Child = new TextBlock { Text = "\uE711", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 8, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/PromptTemplateDialog.cs b/src/AxCopilot/Views/PromptTemplateDialog.cs index 3f3467e..29ea02d 100644 --- a/src/AxCopilot/Views/PromptTemplateDialog.cs +++ b/src/AxCopilot/Views/PromptTemplateDialog.cs @@ -60,7 +60,7 @@ internal sealed class PromptTemplateDialog : Window header.Children.Add(new TextBlock { Text = "\uE8A5", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 18, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs b/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs index 7f8f113..b2e1a1b 100644 --- a/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs +++ b/src/AxCopilot/Views/SettingsWindow.AgentConfig.cs @@ -898,7 +898,7 @@ public partial class SettingsWindow }; editBtn.Child = new TextBlock { - Text = "\uE70F", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE70F", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = secondaryText, }; editBtn.MouseLeftButtonUp += (_, _) => ShowHookEditDialog(hooks[idx], idx); @@ -913,7 +913,7 @@ public partial class SettingsWindow }; delBtn.Child = new TextBlock { - Text = "\uE74D", FontFamily = new FontFamily("Segoe MDL2 Assets"), + Text = "\uE74D", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = new SolidColorBrush(Color.FromRgb(0xEF, 0x53, 0x50)), }; delBtn.MouseLeftButtonUp += (_, _) => @@ -1120,7 +1120,7 @@ public partial class SettingsWindow var label = new TextBlock { - Text = modelName, FontSize = 12, FontFamily = new FontFamily("Consolas, Courier New"), + Text = modelName, FontSize = 12, FontFamily = ThemeResourceHelper.ConsolasCourierNew, VerticalAlignment = VerticalAlignment.Center, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black, }; diff --git a/src/AxCopilot/Views/SettingsWindow.Tools.cs b/src/AxCopilot/Views/SettingsWindow.Tools.cs index 288f105..e8e456d 100644 --- a/src/AxCopilot/Views/SettingsWindow.Tools.cs +++ b/src/AxCopilot/Views/SettingsWindow.Tools.cs @@ -127,7 +127,7 @@ public partial class SettingsWindow var arrow = new TextBlock { Text = "\uE70D", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 9, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, @@ -147,7 +147,7 @@ public partial class SettingsWindow catHeaderPanel.Children.Add(new TextBlock { Text = group.Icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = ThemeResourceHelper.HexBrush(group.IconColor), VerticalAlignment = VerticalAlignment.Center, @@ -196,7 +196,7 @@ public partial class SettingsWindow { Text = name, FontSize = 12, - FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"), + FontFamily = ThemeResourceHelper.ConsolasCode, Foreground = TryFindResource("AccentColor") as Brush ?? ThemeResourceHelper.HexBrush("#4B5EFC"), VerticalAlignment = VerticalAlignment.Center, @@ -298,7 +298,7 @@ public partial class SettingsWindow importContent.Children.Add(new TextBlock { Text = "\uE8B5", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = Brushes.White, VerticalAlignment = VerticalAlignment.Center, @@ -330,7 +330,7 @@ public partial class SettingsWindow exportContent.Children.Add(new TextBlock { Text = "\uEDE1", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = ThemeResourceHelper.HexBrush("#555"), VerticalAlignment = VerticalAlignment.Center, @@ -371,7 +371,7 @@ public partial class SettingsWindow galleryContent.Children.Add(new TextBlock { Text = "\uE768", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = ThemeResourceHelper.HexBrush("#4B5EFC"), VerticalAlignment = VerticalAlignment.Center, @@ -406,7 +406,7 @@ public partial class SettingsWindow statsContent.Children.Add(new TextBlock { Text = "\uE9D9", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = ThemeResourceHelper.HexBrush("#A78BFA"), VerticalAlignment = VerticalAlignment.Center, @@ -468,7 +468,7 @@ public partial class SettingsWindow var arrow = new TextBlock { Text = "\uE70D", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 9, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, VerticalAlignment = VerticalAlignment.Center, @@ -488,7 +488,7 @@ public partial class SettingsWindow catHeaderPanel.Children.Add(new TextBlock { Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = new SolidColorBrush(color), VerticalAlignment = VerticalAlignment.Center, @@ -536,7 +536,7 @@ public partial class SettingsWindow { Text = $"/{skill.Name}", FontSize = 12, - FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"), + FontFamily = ThemeResourceHelper.ConsolasCode, Foreground = skill.IsAvailable ? (TryFindResource("AccentColor") as Brush ?? ThemeResourceHelper.HexBrush("#4B5EFC")) : Brushes.Gray, diff --git a/src/AxCopilot/Views/SettingsWindow.UI.cs b/src/AxCopilot/Views/SettingsWindow.UI.cs index c9dd31d..d9536f1 100644 --- a/src/AxCopilot/Views/SettingsWindow.UI.cs +++ b/src/AxCopilot/Views/SettingsWindow.UI.cs @@ -29,7 +29,7 @@ public partial class SettingsWindow var arrow = new TextBlock { Text = "\uE70D", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 9, Foreground = headerColor, VerticalAlignment = VerticalAlignment.Center, @@ -350,7 +350,7 @@ public partial class SettingsWindow Grid.SetColumn(labelTb, 0); row.Children.Add(labelTb); - var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = new FontFamily("Consolas"), Foreground = Brushes.Gray, VerticalAlignment = VerticalAlignment.Center }; + var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = ThemeResourceHelper.Consolas, Foreground = Brushes.Gray, VerticalAlignment = VerticalAlignment.Center }; Grid.SetColumn(sizeTb, 1); row.Children.Add(sizeTb); @@ -385,7 +385,7 @@ public partial class SettingsWindow row.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); var labelTb = new TextBlock { Text = label, FontSize = 12, Foreground = TryFindResource("PrimaryText") as Brush ?? Brushes.Black }; Grid.SetColumn(labelTb, 0); row.Children.Add(labelTb); - var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = new FontFamily("Consolas"), Foreground = Brushes.Gray }; + var sizeTb = new TextBlock { Text = StorageAnalyzer.FormatSize(size), FontSize = 12, FontFamily = ThemeResourceHelper.Consolas, Foreground = Brushes.Gray }; Grid.SetColumn(sizeTb, 1); row.Children.Add(sizeTb); StorageDetailPanel2.Children.Add(row); } diff --git a/src/AxCopilot/Views/ShortcutHelpWindow.xaml.cs b/src/AxCopilot/Views/ShortcutHelpWindow.xaml.cs index bf18ad2..36b6642 100644 --- a/src/AxCopilot/Views/ShortcutHelpWindow.xaml.cs +++ b/src/AxCopilot/Views/ShortcutHelpWindow.xaml.cs @@ -146,7 +146,7 @@ public partial class ShortcutHelpWindow : Window iconBorder.Child = new TextBlock { Text = row.Icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10, Foreground = ParseBrush(colorHex), HorizontalAlignment = HorizontalAlignment.Center, @@ -168,7 +168,7 @@ public partial class ShortcutHelpWindow : Window keyBorder.Child = new TextBlock { Text = row.Key, - FontFamily = new FontFamily("Consolas, Courier New"), + FontFamily = ThemeResourceHelper.ConsolasCourierNew, FontSize = 11, Foreground = primaryText, VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/SkillEditorWindow.xaml.cs b/src/AxCopilot/Views/SkillEditorWindow.xaml.cs index 345215a..5c44849 100644 --- a/src/AxCopilot/Views/SkillEditorWindow.xaml.cs +++ b/src/AxCopilot/Views/SkillEditorWindow.xaml.cs @@ -98,7 +98,7 @@ public partial class SkillEditorWindow : Window border.Child = new TextBlock { Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = isSelected ? Brushes.White : subBrush, HorizontalAlignment = HorizontalAlignment.Center, @@ -166,7 +166,7 @@ public partial class SkillEditorWindow : Window { Text = tool.Name, FontSize = 11.5, - FontFamily = new FontFamily("Consolas"), + FontFamily = ThemeResourceHelper.Consolas, Foreground = fgBrush, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(6, 0, 0, 0), @@ -364,7 +364,7 @@ public partial class SkillEditorWindow : Window var textBox = new TextBox { Text = content, - FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"), + FontFamily = ThemeResourceHelper.ConsolasCode, FontSize = 12.5, IsReadOnly = true, AcceptsReturn = true, diff --git a/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs b/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs index 8505fa9..0fbda14 100644 --- a/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs +++ b/src/AxCopilot/Views/SkillGalleryWindow.xaml.cs @@ -202,7 +202,7 @@ public partial class SkillGalleryWindow : Window iconBorder.Child = new TextBlock { Text = skill.Icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 16, Foreground = skill.IsAvailable ? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)) @@ -222,7 +222,7 @@ public partial class SkillGalleryWindow : Window Text = $"/{skill.Name}", FontSize = 13, FontWeight = FontWeights.SemiBold, - FontFamily = new FontFamily("Consolas"), + FontFamily = ThemeResourceHelper.Consolas, Foreground = skill.IsAvailable ? new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)) : subBrush, @@ -415,7 +415,7 @@ public partial class SkillGalleryWindow : Window Child = new TextBlock { Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 12, Foreground = new SolidColorBrush(col), HorizontalAlignment = HorizontalAlignment.Center, @@ -488,7 +488,7 @@ public partial class SkillGalleryWindow : Window titleLeft.Children.Add(new TextBlock { Text = skill.Icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = new SolidColorBrush(Color.FromRgb(0x4B, 0x5E, 0xFC)), VerticalAlignment = VerticalAlignment.Center, @@ -515,7 +515,7 @@ public partial class SkillGalleryWindow : Window closeBtn.Child = new TextBlock { Text = "\uE8BB", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10, Foreground = subBrush, HorizontalAlignment = HorizontalAlignment.Center, @@ -611,7 +611,7 @@ public partial class SkillGalleryWindow : Window { Text = promptText, FontSize = 11.5, - FontFamily = new FontFamily("Consolas, Cascadia Code, Segoe UI"), + FontFamily = ThemeResourceHelper.ConsolasCode, Foreground = fgBrush, TextWrapping = TextWrapping.Wrap, Opacity = 0.85, diff --git a/src/AxCopilot/Views/TextActionPopup.xaml.cs b/src/AxCopilot/Views/TextActionPopup.xaml.cs index 45a3b49..979dd03 100644 --- a/src/AxCopilot/Views/TextActionPopup.xaml.cs +++ b/src/AxCopilot/Views/TextActionPopup.xaml.cs @@ -82,7 +82,7 @@ public partial class TextActionPopup : Window sp.Children.Add(new TextBlock { Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/ThemeResourceHelper.cs b/src/AxCopilot/Views/ThemeResourceHelper.cs index b52e84a..b575681 100644 --- a/src/AxCopilot/Views/ThemeResourceHelper.cs +++ b/src/AxCopilot/Views/ThemeResourceHelper.cs @@ -116,4 +116,13 @@ public static class ThemeResourceHelper /// Consolas FontFamily (캐시됨). public static readonly FontFamily Consolas = new("Consolas"); + + /// Cascadia Code → Consolas → monospace 폴백 체인 (코드 블록용, 캐시됨). + public static readonly FontFamily CascadiaCode = new("Cascadia Code, Consolas, monospace"); + + /// Consolas → Cascadia Code → Segoe UI 폴백 체인 (인라인 코드용, 캐시됨). + public static readonly FontFamily ConsolasCode = new("Consolas, Cascadia Code, Segoe UI"); + + /// Consolas → Courier New 폴백 체인 (터미널 스타일, 캐시됨). + public static readonly FontFamily ConsolasCourierNew = new("Consolas, Courier New"); } diff --git a/src/AxCopilot/Views/TrayMenuWindow.xaml.cs b/src/AxCopilot/Views/TrayMenuWindow.xaml.cs index 238774d..4602cc9 100644 --- a/src/AxCopilot/Views/TrayMenuWindow.xaml.cs +++ b/src/AxCopilot/Views/TrayMenuWindow.xaml.cs @@ -71,7 +71,7 @@ public partial class TrayMenuWindow : Window var glyphBlock = new TextBlock { Text = glyph, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, Foreground = isChecked ? BulbOnBrush : BulbOffBrush, VerticalAlignment = VerticalAlignment.Center, @@ -206,7 +206,7 @@ public partial class TrayMenuWindow : Window var glyphBlock = new TextBlock { Text = glyph, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 14, VerticalAlignment = VerticalAlignment.Center, Width = 20, diff --git a/src/AxCopilot/Views/UserAskDialog.cs b/src/AxCopilot/Views/UserAskDialog.cs index dd34505..071202f 100644 --- a/src/AxCopilot/Views/UserAskDialog.cs +++ b/src/AxCopilot/Views/UserAskDialog.cs @@ -67,7 +67,7 @@ internal sealed class UserAskDialog : Window titleSp.Children.Add(new TextBlock { Text = "\uE9CE", - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 16, Foreground = accentBrush, VerticalAlignment = VerticalAlignment.Center, diff --git a/src/AxCopilot/Views/WorkflowAnalyzerWindow.xaml.cs b/src/AxCopilot/Views/WorkflowAnalyzerWindow.xaml.cs index a8b0c0a..0aab725 100644 --- a/src/AxCopilot/Views/WorkflowAnalyzerWindow.xaml.cs +++ b/src/AxCopilot/Views/WorkflowAnalyzerWindow.xaml.cs @@ -340,7 +340,7 @@ public partial class WorkflowAnalyzerWindow : Window { Text = Truncate(entry.Label, 12), FontSize = 10, - FontFamily = new FontFamily("Consolas"), + FontFamily = ThemeResourceHelper.Consolas, Foreground = new SolidColorBrush(barColor), Width = labelWidth, TextAlignment = TextAlignment.Right, @@ -372,7 +372,7 @@ public partial class WorkflowAnalyzerWindow : Window Text = FormatMs(entry.DurationMs), FontSize = 9, Foreground = new SolidColorBrush(barColor), - FontFamily = new FontFamily("Consolas"), + FontFamily = ThemeResourceHelper.Consolas, }; Canvas.SetLeft(timeText, labelWidth + 8 + barStart + barWidth + 4); Canvas.SetTop(timeText, y + 3); @@ -443,7 +443,7 @@ public partial class WorkflowAnalyzerWindow : Window { Text = Truncate(name, 12), FontSize = 11, - FontFamily = new FontFamily("Consolas"), + FontFamily = ThemeResourceHelper.Consolas, Foreground = new SolidColorBrush(barColor), TextAlignment = TextAlignment.Right, VerticalAlignment = VerticalAlignment.Center, @@ -476,7 +476,7 @@ public partial class WorkflowAnalyzerWindow : Window Text = FormatMs(ms), FontSize = 10, Foreground = new SolidColorBrush(barColor), - FontFamily = new FontFamily("Consolas"), + FontFamily = ThemeResourceHelper.Consolas, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(4, 0, 0, 0), }; @@ -691,7 +691,7 @@ public partial class WorkflowAnalyzerWindow : Window iconBorder.Child = new TextBlock { Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10, Foreground = Brushes.White, HorizontalAlignment = HorizontalAlignment.Center,