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,