diff --git a/src/AxCopilot/Services/MarkdownRenderer.CodeBlock.cs b/src/AxCopilot/Services/MarkdownRenderer.CodeBlock.cs
new file mode 100644
index 0000000..706bd1b
--- /dev/null
+++ b/src/AxCopilot/Services/MarkdownRenderer.CodeBlock.cs
@@ -0,0 +1,227 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using AxCopilot.Views;
+
+namespace AxCopilot.Services;
+
+public static partial class MarkdownRenderer
+{
+ /// 코드 블록 UI 생성 (헤더 + 복사 버튼 + 코드)
+ private static Border CreateCodeBlock(string code, string lang, Brush textColor, Brush codeBg, Brush accentColor)
+ {
+ var container = new Border
+ {
+ Background = codeBg,
+ CornerRadius = new CornerRadius(10),
+ Margin = new Thickness(0, 6, 0, 6),
+ Padding = new Thickness(0)
+ };
+
+ var stack = new StackPanel();
+
+ // 헤더 (언어 + 복사 버튼)
+ var header = new Border
+ {
+ Background = new SolidColorBrush(Color.FromArgb(30, 255, 255, 255)),
+ CornerRadius = new CornerRadius(10, 10, 0, 0),
+ Padding = new Thickness(14, 6, 8, 6)
+ };
+ var headerGrid = new Grid();
+ headerGrid.Children.Add(new TextBlock
+ {
+ Text = string.IsNullOrEmpty(lang) ? "code" : lang,
+ FontSize = 11,
+ Foreground = accentColor,
+ FontWeight = FontWeights.SemiBold,
+ VerticalAlignment = VerticalAlignment.Center
+ });
+
+ // 우측 버튼 패널
+ var btnPanel = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+
+ var capturedCode = code;
+ var capturedLang = lang;
+
+ // 파일 저장 버튼
+ var saveBtn = CreateCodeHeaderButton("\uE74E", "저장", textColor);
+ saveBtn.Click += (_, _) =>
+ {
+ try
+ {
+ var ext = GetExtensionForLang(capturedLang);
+ var dlg = new Microsoft.Win32.SaveFileDialog
+ {
+ FileName = $"code{ext}",
+ Filter = $"코드 파일 (*{ext})|*{ext}|모든 파일 (*.*)|*.*",
+ };
+ if (dlg.ShowDialog() == true)
+ System.IO.File.WriteAllText(dlg.FileName, capturedCode);
+ }
+ catch (Exception) { }
+ };
+ btnPanel.Children.Add(saveBtn);
+
+ // 전체화면 버튼
+ var expandBtn = CreateCodeHeaderButton("\uE740", "확대", textColor);
+ expandBtn.Click += (_, _) => ShowCodeFullScreen(capturedCode, capturedLang, codeBg, textColor);
+ btnPanel.Children.Add(expandBtn);
+
+ // 복사 버튼
+ var copyBtn = CreateCodeHeaderButton("\uE8C8", "복사", textColor);
+ copyBtn.Click += (_, _) => { try { Clipboard.SetText(capturedCode); } catch (Exception) { } };
+ btnPanel.Children.Add(copyBtn);
+
+ headerGrid.Children.Add(btnPanel);
+ header.Child = headerGrid;
+ stack.Children.Add(header);
+
+ // 코드 본문 (라인 번호 + 구문 하이라이팅)
+ var codeGrid = new Grid { Margin = new Thickness(0, 0, 0, 4) };
+ codeGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ codeGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+
+ // 라인 번호
+ var codeLines = code.Split('\n');
+ var lineNumbers = new TextBlock
+ {
+ FontFamily = ThemeResourceHelper.CascadiaCode,
+ FontSize = 12.5,
+ Foreground = new SolidColorBrush(Color.FromArgb(0x50, 0xFF, 0xFF, 0xFF)),
+ Padding = new Thickness(10, 10, 6, 14),
+ LineHeight = 20,
+ TextAlignment = TextAlignment.Right,
+ Text = string.Join("\n", Enumerable.Range(1, codeLines.Length)),
+ };
+ Grid.SetColumn(lineNumbers, 0);
+ codeGrid.Children.Add(lineNumbers);
+
+ var codeText = new TextBlock
+ {
+ FontFamily = ThemeResourceHelper.CascadiaCode,
+ FontSize = 12.5,
+ Foreground = textColor,
+ TextWrapping = TextWrapping.Wrap,
+ Padding = new Thickness(8, 10, 14, 14),
+ LineHeight = 20
+ };
+ ApplySyntaxHighlighting(codeText, code, lang, textColor);
+ Grid.SetColumn(codeText, 1);
+ codeGrid.Children.Add(codeText);
+
+ stack.Children.Add(codeGrid);
+
+ container.Child = stack;
+ return container;
+ }
+
+ private static Button CreateCodeHeaderButton(string mdlIcon, string label, Brush fg)
+ {
+ return new Button
+ {
+ Background = Brushes.Transparent,
+ BorderThickness = new Thickness(0),
+ Cursor = System.Windows.Input.Cursors.Hand,
+ VerticalAlignment = VerticalAlignment.Center,
+ Padding = new Thickness(5, 2, 5, 2),
+ Margin = new Thickness(2, 0, 0, 0),
+ Content = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ Children =
+ {
+ new TextBlock
+ {
+ Text = mdlIcon,
+ FontFamily = ThemeResourceHelper.SegoeMdl2,
+ FontSize = 10, Foreground = fg, Opacity = 0.6,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = new Thickness(0, 0, 3, 0),
+ },
+ new TextBlock { Text = label, FontSize = 10, Foreground = fg, Opacity = 0.6 },
+ }
+ }
+ };
+ }
+
+ private static string GetExtensionForLang(string lang) => (lang ?? "").ToLowerInvariant() switch
+ {
+ "csharp" or "cs" => ".cs",
+ "python" or "py" => ".py",
+ "javascript" or "js" => ".js",
+ "typescript" or "ts" => ".ts",
+ "java" => ".java",
+ "html" => ".html",
+ "css" => ".css",
+ "json" => ".json",
+ "xml" => ".xml",
+ "sql" => ".sql",
+ "bash" or "sh" or "shell" => ".sh",
+ "powershell" or "ps1" => ".ps1",
+ "bat" or "cmd" => ".bat",
+ "yaml" or "yml" => ".yml",
+ "markdown" or "md" => ".md",
+ "cpp" or "c++" => ".cpp",
+ "c" => ".c",
+ "go" => ".go",
+ "rust" or "rs" => ".rs",
+ _ => ".txt",
+ };
+
+ private static void ShowCodeFullScreen(string code, string lang, Brush codeBg, Brush textColor)
+ {
+ var win = new Window
+ {
+ Title = $"코드 — {(string.IsNullOrEmpty(lang) ? "code" : lang)}",
+ Width = 900, Height = 650,
+ WindowStartupLocation = WindowStartupLocation.CenterScreen,
+ Background = codeBg is SolidColorBrush scb ? new SolidColorBrush(scb.Color) : Brushes.Black,
+ };
+
+ var grid = new Grid();
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+ grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+
+ var lines = code.Split('\n');
+
+ var lineNumTb = new TextBlock
+ {
+ FontFamily = ThemeResourceHelper.CascadiaCode,
+ FontSize = 13, LineHeight = 22,
+ Foreground = new SolidColorBrush(Color.FromArgb(0x50, 0xFF, 0xFF, 0xFF)),
+ Padding = new Thickness(16, 16, 8, 16),
+ TextAlignment = TextAlignment.Right,
+ Text = string.Join("\n", Enumerable.Range(1, lines.Length)),
+ };
+ Grid.SetColumn(lineNumTb, 0);
+ grid.Children.Add(lineNumTb);
+
+ var codeTb = new TextBlock
+ {
+ FontFamily = ThemeResourceHelper.CascadiaCode,
+ FontSize = 13, LineHeight = 22,
+ Foreground = textColor,
+ TextWrapping = TextWrapping.Wrap,
+ Padding = new Thickness(8, 16, 16, 16),
+ };
+ ApplySyntaxHighlighting(codeTb, code, lang, textColor);
+ Grid.SetColumn(codeTb, 1);
+ grid.Children.Add(codeTb);
+
+ var sv = new ScrollViewer
+ {
+ VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
+ HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
+ Content = grid,
+ };
+ win.Content = sv;
+ win.Show();
+ }
+
+}
diff --git a/src/AxCopilot/Services/MarkdownRenderer.cs b/src/AxCopilot/Services/MarkdownRenderer.cs
index fbe687d..00b3194 100644
--- a/src/AxCopilot/Services/MarkdownRenderer.cs
+++ b/src/AxCopilot/Services/MarkdownRenderer.cs
@@ -402,220 +402,4 @@ public static partial class MarkdownRenderer
return wrapper;
}
- /// 코드 블록 UI 생성 (헤더 + 복사 버튼 + 코드)
- private static Border CreateCodeBlock(string code, string lang, Brush textColor, Brush codeBg, Brush accentColor)
- {
- var container = new Border
- {
- Background = codeBg,
- CornerRadius = new CornerRadius(10),
- Margin = new Thickness(0, 6, 0, 6),
- Padding = new Thickness(0)
- };
-
- var stack = new StackPanel();
-
- // 헤더 (언어 + 복사 버튼)
- var header = new Border
- {
- Background = new SolidColorBrush(Color.FromArgb(30, 255, 255, 255)),
- CornerRadius = new CornerRadius(10, 10, 0, 0),
- Padding = new Thickness(14, 6, 8, 6)
- };
- var headerGrid = new Grid();
- headerGrid.Children.Add(new TextBlock
- {
- Text = string.IsNullOrEmpty(lang) ? "code" : lang,
- FontSize = 11,
- Foreground = accentColor,
- FontWeight = FontWeights.SemiBold,
- VerticalAlignment = VerticalAlignment.Center
- });
-
- // 우측 버튼 패널
- var btnPanel = new StackPanel
- {
- Orientation = Orientation.Horizontal,
- HorizontalAlignment = HorizontalAlignment.Right,
- VerticalAlignment = VerticalAlignment.Center,
- };
-
- var capturedCode = code;
- var capturedLang = lang;
-
- // 파일 저장 버튼
- var saveBtn = CreateCodeHeaderButton("\uE74E", "저장", textColor);
- saveBtn.Click += (_, _) =>
- {
- try
- {
- var ext = GetExtensionForLang(capturedLang);
- var dlg = new Microsoft.Win32.SaveFileDialog
- {
- FileName = $"code{ext}",
- Filter = $"코드 파일 (*{ext})|*{ext}|모든 파일 (*.*)|*.*",
- };
- if (dlg.ShowDialog() == true)
- System.IO.File.WriteAllText(dlg.FileName, capturedCode);
- }
- catch (Exception) { }
- };
- btnPanel.Children.Add(saveBtn);
-
- // 전체화면 버튼
- var expandBtn = CreateCodeHeaderButton("\uE740", "확대", textColor);
- expandBtn.Click += (_, _) => ShowCodeFullScreen(capturedCode, capturedLang, codeBg, textColor);
- btnPanel.Children.Add(expandBtn);
-
- // 복사 버튼
- var copyBtn = CreateCodeHeaderButton("\uE8C8", "복사", textColor);
- copyBtn.Click += (_, _) => { try { Clipboard.SetText(capturedCode); } catch (Exception) { } };
- btnPanel.Children.Add(copyBtn);
-
- headerGrid.Children.Add(btnPanel);
- header.Child = headerGrid;
- stack.Children.Add(header);
-
- // 코드 본문 (라인 번호 + 구문 하이라이팅)
- var codeGrid = new Grid { Margin = new Thickness(0, 0, 0, 4) };
- codeGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
- codeGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
-
- // 라인 번호
- var codeLines = code.Split('\n');
- var lineNumbers = new TextBlock
- {
- FontFamily = ThemeResourceHelper.CascadiaCode,
- FontSize = 12.5,
- Foreground = new SolidColorBrush(Color.FromArgb(0x50, 0xFF, 0xFF, 0xFF)),
- Padding = new Thickness(10, 10, 6, 14),
- LineHeight = 20,
- TextAlignment = TextAlignment.Right,
- Text = string.Join("\n", Enumerable.Range(1, codeLines.Length)),
- };
- Grid.SetColumn(lineNumbers, 0);
- codeGrid.Children.Add(lineNumbers);
-
- var codeText = new TextBlock
- {
- FontFamily = ThemeResourceHelper.CascadiaCode,
- FontSize = 12.5,
- Foreground = textColor,
- TextWrapping = TextWrapping.Wrap,
- Padding = new Thickness(8, 10, 14, 14),
- LineHeight = 20
- };
- ApplySyntaxHighlighting(codeText, code, lang, textColor);
- Grid.SetColumn(codeText, 1);
- codeGrid.Children.Add(codeText);
-
- stack.Children.Add(codeGrid);
-
- container.Child = stack;
- return container;
- }
-
- private static Button CreateCodeHeaderButton(string mdlIcon, string label, Brush fg)
- {
- return new Button
- {
- Background = Brushes.Transparent,
- BorderThickness = new Thickness(0),
- Cursor = System.Windows.Input.Cursors.Hand,
- VerticalAlignment = VerticalAlignment.Center,
- Padding = new Thickness(5, 2, 5, 2),
- Margin = new Thickness(2, 0, 0, 0),
- Content = new StackPanel
- {
- Orientation = Orientation.Horizontal,
- Children =
- {
- new TextBlock
- {
- Text = mdlIcon,
- FontFamily = ThemeResourceHelper.SegoeMdl2,
- FontSize = 10, Foreground = fg, Opacity = 0.6,
- VerticalAlignment = VerticalAlignment.Center,
- Margin = new Thickness(0, 0, 3, 0),
- },
- new TextBlock { Text = label, FontSize = 10, Foreground = fg, Opacity = 0.6 },
- }
- }
- };
- }
-
- private static string GetExtensionForLang(string lang) => (lang ?? "").ToLowerInvariant() switch
- {
- "csharp" or "cs" => ".cs",
- "python" or "py" => ".py",
- "javascript" or "js" => ".js",
- "typescript" or "ts" => ".ts",
- "java" => ".java",
- "html" => ".html",
- "css" => ".css",
- "json" => ".json",
- "xml" => ".xml",
- "sql" => ".sql",
- "bash" or "sh" or "shell" => ".sh",
- "powershell" or "ps1" => ".ps1",
- "bat" or "cmd" => ".bat",
- "yaml" or "yml" => ".yml",
- "markdown" or "md" => ".md",
- "cpp" or "c++" => ".cpp",
- "c" => ".c",
- "go" => ".go",
- "rust" or "rs" => ".rs",
- _ => ".txt",
- };
-
- private static void ShowCodeFullScreen(string code, string lang, Brush codeBg, Brush textColor)
- {
- var win = new Window
- {
- Title = $"코드 — {(string.IsNullOrEmpty(lang) ? "code" : lang)}",
- Width = 900, Height = 650,
- WindowStartupLocation = WindowStartupLocation.CenterScreen,
- Background = codeBg is SolidColorBrush scb ? new SolidColorBrush(scb.Color) : Brushes.Black,
- };
-
- var grid = new Grid();
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
-
- var lines = code.Split('\n');
-
- var lineNumTb = new TextBlock
- {
- FontFamily = ThemeResourceHelper.CascadiaCode,
- FontSize = 13, LineHeight = 22,
- Foreground = new SolidColorBrush(Color.FromArgb(0x50, 0xFF, 0xFF, 0xFF)),
- Padding = new Thickness(16, 16, 8, 16),
- TextAlignment = TextAlignment.Right,
- Text = string.Join("\n", Enumerable.Range(1, lines.Length)),
- };
- Grid.SetColumn(lineNumTb, 0);
- grid.Children.Add(lineNumTb);
-
- var codeTb = new TextBlock
- {
- FontFamily = ThemeResourceHelper.CascadiaCode,
- FontSize = 13, LineHeight = 22,
- Foreground = textColor,
- TextWrapping = TextWrapping.Wrap,
- Padding = new Thickness(8, 16, 16, 16),
- };
- ApplySyntaxHighlighting(codeTb, code, lang, textColor);
- Grid.SetColumn(codeTb, 1);
- grid.Children.Add(codeTb);
-
- var sv = new ScrollViewer
- {
- VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
- HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
- Content = grid,
- };
- win.Content = sv;
- win.Show();
- }
-
}
diff --git a/src/AxCopilot/Views/ChatWindow.Animations.cs b/src/AxCopilot/Views/ChatWindow.Animations.cs
new file mode 100644
index 0000000..47f6dcb
--- /dev/null
+++ b/src/AxCopilot/Views/ChatWindow.Animations.cs
@@ -0,0 +1,199 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using AxCopilot.Services;
+
+namespace AxCopilot.Views;
+
+public partial class ChatWindow
+{
+ // ─── 애니메이션 + 체크 헬퍼 ─────────────────────────────────────────
+
+ // ─── 커스텀 체크 아이콘 (모든 팝업 메뉴 공통) ─────────────────────────
+
+ /// 커스텀 체크/미선택 아이콘을 생성합니다. Path 도형 기반, 선택 시 스케일 바운스 애니메이션.
+ /// 현재 테마의 체크 스타일을 반환합니다.
+ private string GetCheckStyle()
+ {
+ var theme = (_settings.Settings.Launcher.Theme ?? "system").ToLowerInvariant();
+ return theme switch
+ {
+ "dark" or "system" => "circle", // 원 + 체크마크, 바운스
+ "oled" => "glow", // 네온 글로우 원, 페이드인
+ "light" => "roundrect", // 둥근 사각형, 슬라이드인
+ "nord" => "diamond", // 다이아몬드(마름모), 스무스 스케일
+ "catppuccin" => "pill", // 필 모양, 스프링 바운스
+ "monokai" => "square", // 정사각형, 퀵 팝
+ "sepia" => "stamp", // 도장 스타일 원, 회전 등장
+ "alfred" => "minimal", // 미니멀 원, 우아한 페이드
+ "alfredlight" => "minimal", // 미니멀 원, 우아한 페이드
+ _ => "circle",
+ };
+ }
+
+ private FrameworkElement CreateCheckIcon(bool isChecked, Brush? accentBrush = null)
+ {
+ var accent = accentBrush ?? ThemeResourceHelper.Accent(this);
+
+ // 심플 V 체크 — 선택 시 컬러 V, 미선택 시 빈 공간
+ if (isChecked)
+ {
+ return CreateSimpleCheck(accent, 14);
+ }
+
+ // 미선택: 동일 크기 빈 공간 (정렬 유지)
+ return new System.Windows.Shapes.Rectangle
+ {
+ Width = 14, Height = 14,
+ Fill = Brushes.Transparent,
+ Margin = new Thickness(0, 0, 10, 0),
+ };
+ }
+
+ /// ScaleTransform 바운스/스케일 애니메이션 헬퍼.
+ private static void AnimateScale(FrameworkElement el, double from, double to, int ms, IEasingFunction ease)
+ {
+ if (el.RenderTransform is TransformGroup tg)
+ {
+ var st = tg.Children.OfType().FirstOrDefault();
+ if (st != null)
+ {
+ var anim = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) { EasingFunction = ease };
+ st.BeginAnimation(ScaleTransform.ScaleXProperty, anim);
+ st.BeginAnimation(ScaleTransform.ScaleYProperty, anim);
+ return;
+ }
+ }
+ if (el.RenderTransform is ScaleTransform scale)
+ {
+ var anim = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) { EasingFunction = ease };
+ scale.BeginAnimation(ScaleTransform.ScaleXProperty, anim);
+ scale.BeginAnimation(ScaleTransform.ScaleYProperty, anim);
+ }
+ }
+
+ /// 마우스 오버 시 살짝 확대 + 복귀하는 호버 애니메이션을 적용합니다.
+ ///
+ /// 마우스 오버 시 살짝 확대하는 호버 애니메이션.
+ /// 주의: 인접 요소(탭 버튼, 가로 나열 메뉴 등)에는 사용 금지 — 확대 시 이웃 요소를 가립니다.
+ /// 독립적 공간이 있는 버튼에만 적용하세요.
+ ///
+ private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08)
+ {
+ // Loaded 이벤트에서 실행해야 XAML Style의 봉인된 Transform을 안전하게 교체 가능
+ void EnsureTransform()
+ {
+ element.RenderTransformOrigin = new Point(0.5, 0.5);
+ // 봉인(frozen)된 Transform이면 새로 생성하여 교체
+ if (element.RenderTransform is not ScaleTransform || element.RenderTransform.IsFrozen)
+ element.RenderTransform = new ScaleTransform(1, 1);
+ }
+
+ element.Loaded += (_, _) => EnsureTransform();
+
+ element.MouseEnter += (_, _) =>
+ {
+ EnsureTransform();
+ var st = (ScaleTransform)element.RenderTransform;
+ var grow = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150))
+ { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
+ st.BeginAnimation(ScaleTransform.ScaleXProperty, grow);
+ st.BeginAnimation(ScaleTransform.ScaleYProperty, grow);
+ };
+ element.MouseLeave += (_, _) =>
+ {
+ EnsureTransform();
+ var st = (ScaleTransform)element.RenderTransform;
+ var shrink = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200))
+ { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
+ st.BeginAnimation(ScaleTransform.ScaleXProperty, shrink);
+ st.BeginAnimation(ScaleTransform.ScaleYProperty, shrink);
+ };
+ }
+
+ /// 마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션을 적용합니다.
+ ///
+ /// 마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션.
+ /// Scale과 달리 크기가 변하지 않아 인접 요소를 가리지 않습니다.
+ ///
+ private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5)
+ {
+ void EnsureTransform()
+ {
+ if (element.RenderTransform is not TranslateTransform || element.RenderTransform.IsFrozen)
+ element.RenderTransform = new TranslateTransform(0, 0);
+ }
+
+ element.Loaded += (_, _) => EnsureTransform();
+
+ element.MouseEnter += (_, _) =>
+ {
+ EnsureTransform();
+ var tt = (TranslateTransform)element.RenderTransform;
+ tt.BeginAnimation(TranslateTransform.YProperty,
+ new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200))
+ { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } });
+ };
+ element.MouseLeave += (_, _) =>
+ {
+ EnsureTransform();
+ var tt = (TranslateTransform)element.RenderTransform;
+ tt.BeginAnimation(TranslateTransform.YProperty,
+ new DoubleAnimation(0, TimeSpan.FromMilliseconds(250))
+ { EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 10 } });
+ };
+ }
+
+ /// 심플한 V 체크 아이콘을 생성합니다 (디자인 통일용).
+ private static FrameworkElement CreateSimpleCheck(Brush color, double size = 14)
+ {
+ return new System.Windows.Shapes.Path
+ {
+ Data = Geometry.Parse($"M {size * 0.15} {size * 0.5} L {size * 0.4} {size * 0.75} L {size * 0.85} {size * 0.28}"),
+ Stroke = color,
+ StrokeThickness = 2,
+ StrokeStartLineCap = PenLineCap.Round,
+ StrokeEndLineCap = PenLineCap.Round,
+ StrokeLineJoin = PenLineJoin.Round,
+ Width = size,
+ Height = size,
+ Margin = new Thickness(0, 0, 10, 0),
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ }
+
+ /// 팝업 메뉴 항목에 호버 배경색 + 미세 확대 효과를 적용합니다.
+ private static void ApplyMenuItemHover(Border item)
+ {
+ var originalBg = item.Background?.Clone() ?? Brushes.Transparent;
+ if (originalBg.CanFreeze) originalBg.Freeze();
+ item.RenderTransformOrigin = new Point(0.5, 0.5);
+ item.RenderTransform = new ScaleTransform(1, 1);
+ item.MouseEnter += (s, _) =>
+ {
+ if (s is Border b)
+ {
+ // 원래 배경이 투명이면 반투명 흰색, 아니면 밝기 변경
+ if (originalBg is SolidColorBrush scb && scb.Color.A > 0x20)
+ b.Opacity = 0.85;
+ else
+ b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
+ }
+ var st = item.RenderTransform as ScaleTransform;
+ st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
+ st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
+ };
+ item.MouseLeave += (s, _) =>
+ {
+ if (s is Border b)
+ {
+ b.Opacity = 1.0;
+ b.Background = originalBg;
+ }
+ var st = item.RenderTransform as ScaleTransform;
+ st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
+ st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
+ };
+ }
+}
diff --git a/src/AxCopilot/Views/ChatWindow.FeedbackButtons.cs b/src/AxCopilot/Views/ChatWindow.FeedbackButtons.cs
new file mode 100644
index 0000000..c16e937
--- /dev/null
+++ b/src/AxCopilot/Views/ChatWindow.FeedbackButtons.cs
@@ -0,0 +1,130 @@
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using AxCopilot.Models;
+using AxCopilot.Services;
+
+namespace AxCopilot.Views;
+
+public partial class ChatWindow
+{
+ // ─── 액션·피드백 버튼 ───────────────────────────────────────────────
+
+ private Button CreateActionButton(string symbol, string tooltip, Brush foreground, Action onClick)
+ {
+ var hoverBrush = ThemeResourceHelper.Primary(this);
+ var icon = new TextBlock
+ {
+ Text = symbol,
+ FontFamily = ThemeResourceHelper.SegoeMdl2,
+ FontSize = 12,
+ Foreground = foreground,
+ VerticalAlignment = VerticalAlignment.Center
+ };
+ var btn = new Button
+ {
+ Content = icon,
+ Background = Brushes.Transparent,
+ BorderThickness = new Thickness(0),
+ Cursor = Cursors.Hand,
+ Padding = new Thickness(6, 4, 6, 4),
+ Margin = new Thickness(0, 0, 4, 0),
+ ToolTip = tooltip
+ };
+ btn.MouseEnter += (_, _) => icon.Foreground = hoverBrush;
+ btn.MouseLeave += (_, _) => icon.Foreground = foreground;
+ btn.Click += (_, _) => onClick();
+ ApplyHoverScaleAnimation(btn, 1.15);
+ return btn;
+ }
+
+ /// 좋아요/싫어요 토글 피드백 버튼 (상태 영구 저장)
+ private Button CreateFeedbackButton(string outline, string filled, string tooltip,
+ Brush normalColor, Brush activeColor, ChatMessage? message = null, string feedbackType = "",
+ Action? resetSibling = null, Action? registerReset = null)
+ {
+ var hoverBrush = ThemeResourceHelper.Primary(this);
+ var isActive = message?.Feedback == feedbackType;
+ var icon = new TextBlock
+ {
+ Text = isActive ? filled : outline,
+ FontFamily = ThemeResourceHelper.SegoeMdl2,
+ FontSize = 12,
+ Foreground = isActive ? activeColor : normalColor,
+ VerticalAlignment = VerticalAlignment.Center,
+ RenderTransformOrigin = new Point(0.5, 0.5),
+ RenderTransform = new ScaleTransform(1, 1)
+ };
+ var btn = new Button
+ {
+ Content = icon,
+ Background = Brushes.Transparent,
+ BorderThickness = new Thickness(0),
+ Cursor = Cursors.Hand,
+ Padding = new Thickness(6, 4, 6, 4),
+ Margin = new Thickness(0, 0, 4, 0),
+ ToolTip = tooltip
+ };
+ // 상대 버튼이 리셋할 수 있도록 등록
+ registerReset?.Invoke(() =>
+ {
+ isActive = false;
+ icon.Text = outline;
+ icon.Foreground = normalColor;
+ });
+ btn.MouseEnter += (_, _) => { if (!isActive) icon.Foreground = hoverBrush; };
+ btn.MouseLeave += (_, _) => { if (!isActive) icon.Foreground = normalColor; };
+ btn.Click += (_, _) =>
+ {
+ isActive = !isActive;
+ icon.Text = isActive ? filled : outline;
+ icon.Foreground = isActive ? activeColor : normalColor;
+
+ // 상호 배타: 활성화 시 반대쪽 리셋
+ if (isActive) resetSibling?.Invoke();
+
+ // 피드백 상태 저장
+ if (message != null)
+ {
+ message.Feedback = isActive ? feedbackType : null;
+ try
+ {
+ ChatConversation? conv;
+ lock (_convLock) conv = _currentConversation;
+ if (conv != null) _storage.Save(conv);
+ }
+ catch (Exception) { /* 비핵심 작업 실패 — UI 차단 방지 */ }
+ }
+
+ // 바운스 애니메이션
+ var scale = (ScaleTransform)icon.RenderTransform;
+ var bounce = new DoubleAnimation(1.3, 1.0, TimeSpan.FromMilliseconds(250))
+ { EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 5 } };
+ scale.BeginAnimation(ScaleTransform.ScaleXProperty, bounce);
+ scale.BeginAnimation(ScaleTransform.ScaleYProperty, bounce);
+ };
+ return btn;
+ }
+
+ /// 좋아요/싫어요 버튼을 상호 배타로 연결하여 추가
+ private void AddLinkedFeedbackButtons(StackPanel actionBar, Brush btnColor, ChatMessage? message)
+ {
+ // resetSibling는 나중에 설정되므로 Action 래퍼로 간접 참조
+ Action? resetLikeAction = null;
+ Action? resetDislikeAction = null;
+
+ var likeBtn = CreateFeedbackButton("\uE8E1", "\uEB51", "좋아요", btnColor,
+ new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), message, "like",
+ resetSibling: () => resetDislikeAction?.Invoke(),
+ registerReset: reset => resetLikeAction = reset);
+ var dislikeBtn = CreateFeedbackButton("\uE8E0", "\uEB50", "싫어요", btnColor,
+ new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E)), message, "dislike",
+ resetSibling: () => resetLikeAction?.Invoke(),
+ registerReset: reset => resetDislikeAction = reset);
+
+ actionBar.Children.Add(likeBtn);
+ actionBar.Children.Add(dislikeBtn);
+ }
+}
diff --git a/src/AxCopilot/Views/ChatWindow.MessageRendering.cs b/src/AxCopilot/Views/ChatWindow.MessageRendering.cs
index 8763320..c42a2cb 100644
--- a/src/AxCopilot/Views/ChatWindow.MessageRendering.cs
+++ b/src/AxCopilot/Views/ChatWindow.MessageRendering.cs
@@ -217,306 +217,4 @@ public partial class ChatWindow
}
}
- // ─── 커스텀 체크 아이콘 (모든 팝업 메뉴 공통) ─────────────────────────
-
- /// 커스텀 체크/미선택 아이콘을 생성합니다. Path 도형 기반, 선택 시 스케일 바운스 애니메이션.
- /// 현재 테마의 체크 스타일을 반환합니다.
- private string GetCheckStyle()
- {
- var theme = (_settings.Settings.Launcher.Theme ?? "system").ToLowerInvariant();
- return theme switch
- {
- "dark" or "system" => "circle", // 원 + 체크마크, 바운스
- "oled" => "glow", // 네온 글로우 원, 페이드인
- "light" => "roundrect", // 둥근 사각형, 슬라이드인
- "nord" => "diamond", // 다이아몬드(마름모), 스무스 스케일
- "catppuccin" => "pill", // 필 모양, 스프링 바운스
- "monokai" => "square", // 정사각형, 퀵 팝
- "sepia" => "stamp", // 도장 스타일 원, 회전 등장
- "alfred" => "minimal", // 미니멀 원, 우아한 페이드
- "alfredlight" => "minimal", // 미니멀 원, 우아한 페이드
- _ => "circle",
- };
- }
-
- private FrameworkElement CreateCheckIcon(bool isChecked, Brush? accentBrush = null)
- {
- var accent = accentBrush ?? ThemeResourceHelper.Accent(this);
-
- // 심플 V 체크 — 선택 시 컬러 V, 미선택 시 빈 공간
- if (isChecked)
- {
- return CreateSimpleCheck(accent, 14);
- }
-
- // 미선택: 동일 크기 빈 공간 (정렬 유지)
- return new System.Windows.Shapes.Rectangle
- {
- Width = 14, Height = 14,
- Fill = Brushes.Transparent,
- Margin = new Thickness(0, 0, 10, 0),
- };
- }
-
- /// ScaleTransform 바운스/스케일 애니메이션 헬퍼.
- private static void AnimateScale(FrameworkElement el, double from, double to, int ms, IEasingFunction ease)
- {
- if (el.RenderTransform is TransformGroup tg)
- {
- var st = tg.Children.OfType().FirstOrDefault();
- if (st != null)
- {
- var anim = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) { EasingFunction = ease };
- st.BeginAnimation(ScaleTransform.ScaleXProperty, anim);
- st.BeginAnimation(ScaleTransform.ScaleYProperty, anim);
- return;
- }
- }
- if (el.RenderTransform is ScaleTransform scale)
- {
- var anim = new DoubleAnimation(from, to, TimeSpan.FromMilliseconds(ms)) { EasingFunction = ease };
- scale.BeginAnimation(ScaleTransform.ScaleXProperty, anim);
- scale.BeginAnimation(ScaleTransform.ScaleYProperty, anim);
- }
- }
-
- /// 마우스 오버 시 살짝 확대 + 복귀하는 호버 애니메이션을 적용합니다.
- ///
- /// 마우스 오버 시 살짝 확대하는 호버 애니메이션.
- /// 주의: 인접 요소(탭 버튼, 가로 나열 메뉴 등)에는 사용 금지 — 확대 시 이웃 요소를 가립니다.
- /// 독립적 공간이 있는 버튼에만 적용하세요.
- ///
- private static void ApplyHoverScaleAnimation(FrameworkElement element, double hoverScale = 1.08)
- {
- // Loaded 이벤트에서 실행해야 XAML Style의 봉인된 Transform을 안전하게 교체 가능
- void EnsureTransform()
- {
- element.RenderTransformOrigin = new Point(0.5, 0.5);
- // 봉인(frozen)된 Transform이면 새로 생성하여 교체
- if (element.RenderTransform is not ScaleTransform || element.RenderTransform.IsFrozen)
- element.RenderTransform = new ScaleTransform(1, 1);
- }
-
- element.Loaded += (_, _) => EnsureTransform();
-
- element.MouseEnter += (_, _) =>
- {
- EnsureTransform();
- var st = (ScaleTransform)element.RenderTransform;
- var grow = new DoubleAnimation(hoverScale, TimeSpan.FromMilliseconds(150))
- { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
- st.BeginAnimation(ScaleTransform.ScaleXProperty, grow);
- st.BeginAnimation(ScaleTransform.ScaleYProperty, grow);
- };
- element.MouseLeave += (_, _) =>
- {
- EnsureTransform();
- var st = (ScaleTransform)element.RenderTransform;
- var shrink = new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(200))
- { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
- st.BeginAnimation(ScaleTransform.ScaleXProperty, shrink);
- st.BeginAnimation(ScaleTransform.ScaleYProperty, shrink);
- };
- }
-
- /// 마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션을 적용합니다.
- ///
- /// 마우스 오버 시 텍스트가 살짝 튀어오르는 바운스 애니메이션.
- /// Scale과 달리 크기가 변하지 않아 인접 요소를 가리지 않습니다.
- ///
- private static void ApplyHoverBounceAnimation(FrameworkElement element, double bounceY = -2.5)
- {
- void EnsureTransform()
- {
- if (element.RenderTransform is not TranslateTransform || element.RenderTransform.IsFrozen)
- element.RenderTransform = new TranslateTransform(0, 0);
- }
-
- element.Loaded += (_, _) => EnsureTransform();
-
- element.MouseEnter += (_, _) =>
- {
- EnsureTransform();
- var tt = (TranslateTransform)element.RenderTransform;
- tt.BeginAnimation(TranslateTransform.YProperty,
- new DoubleAnimation(bounceY, TimeSpan.FromMilliseconds(200))
- { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } });
- };
- element.MouseLeave += (_, _) =>
- {
- EnsureTransform();
- var tt = (TranslateTransform)element.RenderTransform;
- tt.BeginAnimation(TranslateTransform.YProperty,
- new DoubleAnimation(0, TimeSpan.FromMilliseconds(250))
- { EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 10 } });
- };
- }
-
- /// 심플한 V 체크 아이콘을 생성합니다 (디자인 통일용).
- private static FrameworkElement CreateSimpleCheck(Brush color, double size = 14)
- {
- return new System.Windows.Shapes.Path
- {
- Data = Geometry.Parse($"M {size * 0.15} {size * 0.5} L {size * 0.4} {size * 0.75} L {size * 0.85} {size * 0.28}"),
- Stroke = color,
- StrokeThickness = 2,
- StrokeStartLineCap = PenLineCap.Round,
- StrokeEndLineCap = PenLineCap.Round,
- StrokeLineJoin = PenLineJoin.Round,
- Width = size,
- Height = size,
- Margin = new Thickness(0, 0, 10, 0),
- VerticalAlignment = VerticalAlignment.Center,
- };
- }
-
- /// 팝업 메뉴 항목에 호버 배경색 + 미세 확대 효과를 적용합니다.
- private static void ApplyMenuItemHover(Border item)
- {
- var originalBg = item.Background?.Clone() ?? Brushes.Transparent;
- if (originalBg.CanFreeze) originalBg.Freeze();
- item.RenderTransformOrigin = new Point(0.5, 0.5);
- item.RenderTransform = new ScaleTransform(1, 1);
- item.MouseEnter += (s, _) =>
- {
- if (s is Border b)
- {
- // 원래 배경이 투명이면 반투명 흰색, 아니면 밝기 변경
- if (originalBg is SolidColorBrush scb && scb.Color.A > 0x20)
- b.Opacity = 0.85;
- else
- b.Background = new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
- }
- var st = item.RenderTransform as ScaleTransform;
- st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
- st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.02, TimeSpan.FromMilliseconds(120)));
- };
- item.MouseLeave += (s, _) =>
- {
- if (s is Border b)
- {
- b.Opacity = 1.0;
- b.Background = originalBg;
- }
- var st = item.RenderTransform as ScaleTransform;
- st?.BeginAnimation(ScaleTransform.ScaleXProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
- st?.BeginAnimation(ScaleTransform.ScaleYProperty, new DoubleAnimation(1.0, TimeSpan.FromMilliseconds(150)));
- };
- }
-
- private Button CreateActionButton(string symbol, string tooltip, Brush foreground, Action onClick)
- {
- var hoverBrush = ThemeResourceHelper.Primary(this);
- var icon = new TextBlock
- {
- Text = symbol,
- FontFamily = ThemeResourceHelper.SegoeMdl2,
- FontSize = 12,
- Foreground = foreground,
- VerticalAlignment = VerticalAlignment.Center
- };
- var btn = new Button
- {
- Content = icon,
- Background = Brushes.Transparent,
- BorderThickness = new Thickness(0),
- Cursor = Cursors.Hand,
- Padding = new Thickness(6, 4, 6, 4),
- Margin = new Thickness(0, 0, 4, 0),
- ToolTip = tooltip
- };
- btn.MouseEnter += (_, _) => icon.Foreground = hoverBrush;
- btn.MouseLeave += (_, _) => icon.Foreground = foreground;
- btn.Click += (_, _) => onClick();
- ApplyHoverScaleAnimation(btn, 1.15);
- return btn;
- }
-
- /// 좋아요/싫어요 토글 피드백 버튼 (상태 영구 저장)
- private Button CreateFeedbackButton(string outline, string filled, string tooltip,
- Brush normalColor, Brush activeColor, ChatMessage? message = null, string feedbackType = "",
- Action? resetSibling = null, Action? registerReset = null)
- {
- var hoverBrush = ThemeResourceHelper.Primary(this);
- var isActive = message?.Feedback == feedbackType;
- var icon = new TextBlock
- {
- Text = isActive ? filled : outline,
- FontFamily = ThemeResourceHelper.SegoeMdl2,
- FontSize = 12,
- Foreground = isActive ? activeColor : normalColor,
- VerticalAlignment = VerticalAlignment.Center,
- RenderTransformOrigin = new Point(0.5, 0.5),
- RenderTransform = new ScaleTransform(1, 1)
- };
- var btn = new Button
- {
- Content = icon,
- Background = Brushes.Transparent,
- BorderThickness = new Thickness(0),
- Cursor = Cursors.Hand,
- Padding = new Thickness(6, 4, 6, 4),
- Margin = new Thickness(0, 0, 4, 0),
- ToolTip = tooltip
- };
- // 상대 버튼이 리셋할 수 있도록 등록
- registerReset?.Invoke(() =>
- {
- isActive = false;
- icon.Text = outline;
- icon.Foreground = normalColor;
- });
- btn.MouseEnter += (_, _) => { if (!isActive) icon.Foreground = hoverBrush; };
- btn.MouseLeave += (_, _) => { if (!isActive) icon.Foreground = normalColor; };
- btn.Click += (_, _) =>
- {
- isActive = !isActive;
- icon.Text = isActive ? filled : outline;
- icon.Foreground = isActive ? activeColor : normalColor;
-
- // 상호 배타: 활성화 시 반대쪽 리셋
- if (isActive) resetSibling?.Invoke();
-
- // 피드백 상태 저장
- if (message != null)
- {
- message.Feedback = isActive ? feedbackType : null;
- try
- {
- ChatConversation? conv;
- lock (_convLock) conv = _currentConversation;
- if (conv != null) _storage.Save(conv);
- }
- catch (Exception) { /* 비핵심 작업 실패 — UI 차단 방지 */ }
- }
-
- // 바운스 애니메이션
- var scale = (ScaleTransform)icon.RenderTransform;
- var bounce = new DoubleAnimation(1.3, 1.0, TimeSpan.FromMilliseconds(250))
- { EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 1, Springiness = 5 } };
- scale.BeginAnimation(ScaleTransform.ScaleXProperty, bounce);
- scale.BeginAnimation(ScaleTransform.ScaleYProperty, bounce);
- };
- return btn;
- }
-
- /// 좋아요/싫어요 버튼을 상호 배타로 연결하여 추가
- private void AddLinkedFeedbackButtons(StackPanel actionBar, Brush btnColor, ChatMessage? message)
- {
- // resetSibling는 나중에 설정되므로 Action 래퍼로 간접 참조
- Action? resetLikeAction = null;
- Action? resetDislikeAction = null;
-
- var likeBtn = CreateFeedbackButton("\uE8E1", "\uEB51", "좋아요", btnColor,
- new SolidColorBrush(Color.FromRgb(0x38, 0xA1, 0x69)), message, "like",
- resetSibling: () => resetDislikeAction?.Invoke(),
- registerReset: reset => resetLikeAction = reset);
- var dislikeBtn = CreateFeedbackButton("\uE8E0", "\uEB50", "싫어요", btnColor,
- new SolidColorBrush(Color.FromRgb(0xE5, 0x3E, 0x3E)), message, "dislike",
- resetSibling: () => resetLikeAction?.Invoke(),
- registerReset: reset => resetDislikeAction = reset);
-
- actionBar.Children.Add(likeBtn);
- actionBar.Children.Add(dislikeBtn);
- }
}