diff --git a/src/AxCopilot/Views/ChatWindow.xaml b/src/AxCopilot/Views/ChatWindow.xaml index d4170e9..08bfba0 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml +++ b/src/AxCopilot/Views/ChatWindow.xaml @@ -38,7 +38,7 @@ - + @@ -72,7 +72,7 @@ - + @@ -102,7 +102,7 @@ - + @@ -395,11 +395,12 @@ + CornerRadius="8" + Background="{DynamicResource HintBackground}" + BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"> @@ -467,7 +468,7 @@ VerticalAlignment="Center" TextTrimming="CharacterEllipsis" MaxWidth="300" Margin="0,0,12,0"/> - @@ -783,14 +784,16 @@ Panel.ZIndex="20" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,16" - Background="#E0202030" CornerRadius="20" + Background="{DynamicResource ItemBackground}" + BorderBrush="{DynamicResource BorderColor}" BorderThickness="1" + CornerRadius="20" Padding="16,8,16,8" Opacity="0" IsHitTestVisible="False"> + Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,6,0"/> + Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/> @@ -1092,21 +1095,19 @@ HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10,7,0,0" Visibility="Collapsed" CornerRadius="7" Padding="8,3,4,3" + Background="{DynamicResource ItemHoverBackground}" IsHitTestVisible="True"> - - - + FontSize="8" Foreground="{DynamicResource AccentColor}"/> @@ -1135,13 +1136,13 @@ @@ -1167,7 +1168,7 @@ VerticalAlignment="Bottom"> - - + + Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,4,0"/> @@ -1427,24 +1428,24 @@ + Background="{DynamicResource HintBackground}" ToolTip="실행 중인 서브에이전트"> + Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center" Margin="0,0,4,0"/> - + @@ -1478,7 +1479,7 @@ BorderThickness="1,0,0,0"> - @@ -1518,9 +1519,9 @@ 테마에 맞는 ContextMenu를 생성합니다. - private ContextMenu CreateThemedContextMenu() + private Popup? _sharedContextPopup; + + private (Popup Popup, StackPanel Panel) CreateThemedPopupMenu( + UIElement? placementTarget = null, + PlacementMode placement = PlacementMode.MousePoint, + double minWidth = 200) { - var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E)); + _sharedContextPopup?.SetCurrentValue(Popup.IsOpenProperty, false); + + var bg = TryFindResource("LauncherBackground") as Brush ?? Brushes.White; var border = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; - return new ContextMenu + var panel = new StackPanel { Margin = new Thickness(2) }; + var container = new Border { Background = bg, BorderBrush = border, BorderThickness = new Thickness(1), - Padding = new Thickness(4), + CornerRadius = new CornerRadius(10), + Padding = new Thickness(6), + MinWidth = minWidth, + Child = panel, + Effect = new System.Windows.Media.Effects.DropShadowEffect + { + BlurRadius = 16, + ShadowDepth = 3, + Opacity = 0.18, + Color = Colors.Black, + Direction = 270, + }, }; + + var popup = new Popup + { + Child = container, + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade, + Placement = placement, + PlacementTarget = placementTarget, + }; + + _sharedContextPopup = popup; + return (popup, panel); + } + + private Border CreatePopupMenuItem( + Popup popup, + string icon, + string label, + Brush iconBrush, + Brush labelBrush, + Brush hoverBrush, + Action action) + { + var sp = new StackPanel { Orientation = Orientation.Horizontal }; + sp.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 12.5, + Foreground = iconBrush, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 9, 0), + }); + sp.Children.Add(new TextBlock + { + Text = label, + FontSize = 12.5, + Foreground = labelBrush, + VerticalAlignment = VerticalAlignment.Center, + }); + + var item = new Border + { + Child = sp, + Background = Brushes.Transparent, + CornerRadius = new CornerRadius(8), + Cursor = Cursors.Hand, + Padding = new Thickness(10, 7, 12, 7), + Margin = new Thickness(0, 1, 0, 1), + }; + item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBrush; }; + item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; + item.MouseLeftButtonUp += (_, _) => + { + popup.SetCurrentValue(Popup.IsOpenProperty, false); + action(); + }; + + return item; + } + + private static void AddPopupMenuSeparator(Panel panel, Brush brush) + { + panel.Children.Add(new Border + { + Height = 1, + Margin = new Thickness(10, 4, 10, 4), + Background = brush, + Opacity = 0.35, + }); } /// 최근 폴더 항목 우클릭 컨텍스트 메뉴를 표시합니다. private void ShowRecentFolderContextMenu(string folderPath) { - var menu = CreateThemedContextMenu(); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; + var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; + var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent; + var warningBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)); + var (popup, panel) = CreateThemedPopupMenu(); - void AddItem(string icon, string label, Action action) - { - var sp = new StackPanel { Orientation = Orientation.Horizontal }; - sp.Children.Add(new TextBlock - { - Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 12, Foreground = secondaryText, - VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), - }); - sp.Children.Add(new TextBlock - { - Text = label, FontSize = 12, Foreground = primaryText, - VerticalAlignment = VerticalAlignment.Center, - }); - var mi = new MenuItem { Header = sp, Padding = new Thickness(8, 6, 16, 6) }; - mi.Click += (_, _) => action(); - menu.Items.Add(mi); - } - - AddItem("\uED25", "폴더 열기", () => + panel.Children.Add(CreatePopupMenuItem(popup, "\uED25", "폴더 열기", secondaryText, primaryText, hoverBg, () => { try { @@ -1136,16 +1209,16 @@ public partial class ChatWindow : Window }); } catch { } - }); + })); - AddItem("\uE8C8", "경로 복사", () => + panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "경로 복사", secondaryText, primaryText, hoverBg, () => { try { Clipboard.SetText(folderPath); } catch { } - }); + })); - menu.Items.Add(new Separator()); + AddPopupMenuSeparator(panel, borderBrush); - AddItem("\uE74D", "목록에서 삭제", () => + panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "목록에서 삭제", warningBrush, warningBrush, hoverBg, () => { _settings.Settings.Llm.RecentWorkFolders.RemoveAll( p => p.Equals(folderPath, StringComparison.OrdinalIgnoreCase)); @@ -1153,9 +1226,9 @@ public partial class ChatWindow : Window // 메뉴 새로고침 if (FolderMenuPopup.IsOpen) ShowFolderMenu(); - }); + })); - menu.IsOpen = true; + Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input); } private void BtnFolderClear_Click(object sender, RoutedEventArgs e) @@ -8582,43 +8655,27 @@ public partial class ChatWindow : Window private void ShowMessageContextMenu(string content, string role) { - var menu = CreateThemedContextMenu(); var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; - - void AddItem(string icon, string label, Action action) - { - var sp = new StackPanel { Orientation = Orientation.Horizontal }; - sp.Children.Add(new TextBlock - { - Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 12, Foreground = secondaryText, - VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 8, 0), - }); - sp.Children.Add(new TextBlock - { - Text = label, FontSize = 12, Foreground = primaryText, - VerticalAlignment = VerticalAlignment.Center, - }); - var mi = new MenuItem { Header = sp, Padding = new Thickness(8, 6, 16, 6) }; - mi.Click += (_, _) => action(); - menu.Items.Add(mi); - } + var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; + var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.Transparent; + var dangerBrush = new SolidColorBrush(Color.FromRgb(0xEF, 0x44, 0x44)); + var (popup, panel) = CreateThemedPopupMenu(); // 복사 - AddItem("\uE8C8", "텍스트 복사", () => + panel.Children.Add(CreatePopupMenuItem(popup, "\uE8C8", "텍스트 복사", secondaryText, primaryText, hoverBg, () => { try { Clipboard.SetText(content); ShowToast("복사되었습니다"); } catch { } - }); + })); // 마크다운 복사 - AddItem("\uE943", "마크다운 복사", () => + panel.Children.Add(CreatePopupMenuItem(popup, "\uE943", "마크다운 복사", secondaryText, primaryText, hoverBg, () => { try { Clipboard.SetText(content); ShowToast("마크다운으로 복사됨"); } catch { } - }); + })); // 인용하여 답장 - AddItem("\uE97A", "인용하여 답장", () => + panel.Children.Add(CreatePopupMenuItem(popup, "\uE97A", "인용하여 답장", secondaryText, primaryText, hoverBg, () => { var quote = content.Length > 200 ? content[..200] + "..." : content; var lines = quote.Split('\n'); @@ -8626,18 +8683,18 @@ public partial class ChatWindow : Window InputBox.Text = quoted + "\n\n"; InputBox.Focus(); InputBox.CaretIndex = InputBox.Text.Length; - }); + })); - menu.Items.Add(new Separator()); + AddPopupMenuSeparator(panel, borderBrush); // 재생성 (AI 응답만) if (role == "assistant") { - AddItem("\uE72C", "응답 재생성", () => _ = RegenerateLastAsync()); + panel.Children.Add(CreatePopupMenuItem(popup, "\uE72C", "응답 재생성", secondaryText, primaryText, hoverBg, () => _ = RegenerateLastAsync())); } // 대화 분기 (Fork) - AddItem("\uE8A5", "여기서 분기", () => + panel.Children.Add(CreatePopupMenuItem(popup, "\uE8A5", "여기서 분기", secondaryText, primaryText, hoverBg, () => { ChatConversation? conv; lock (_convLock) conv = _currentConversation; @@ -8647,14 +8704,14 @@ public partial class ChatWindow : Window if (idx < 0) return; ForkConversation(conv, idx); - }); + })); - menu.Items.Add(new Separator()); + AddPopupMenuSeparator(panel, borderBrush); // 이후 메시지 모두 삭제 var msgContent = content; var msgRole = role; - AddItem("\uE74D", "이후 메시지 모두 삭제", () => + panel.Children.Add(CreatePopupMenuItem(popup, "\uE74D", "이후 메시지 모두 삭제", dangerBrush, dangerBrush, hoverBg, () => { ChatConversation? conv; lock (_convLock) conv = _currentConversation; @@ -8664,7 +8721,7 @@ public partial class ChatWindow : Window if (idx < 0) return; var removeCount = conv.Messages.Count - idx; - if (MessageBox.Show($"이 메시지 포함 {removeCount}개 메시지를 삭제하시겠습니까?", + if (CustomMessageBox.Show($"이 메시지 포함 {removeCount}개 메시지를 삭제하시겠습니까?", "메시지 삭제", MessageBoxButton.YesNo, MessageBoxImage.Warning) != MessageBoxResult.Yes) return; @@ -8672,9 +8729,9 @@ public partial class ChatWindow : Window try { _storage.Save(conv); } catch (Exception ex) { Services.LogService.Debug($"대화 저장 실패: {ex.Message}"); } RenderMessages(); ShowToast($"{removeCount}개 메시지 삭제됨"); - }); + })); - menu.IsOpen = true; + Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input); } // ─── 팁 알림 ────────────────────────────────────────────────────── @@ -11667,7 +11724,7 @@ public partial class ChatWindow : Window // 삭제 AddItem("\uE74D", "삭제", () => { - var result = MessageBox.Show( + var result = CustomMessageBox.Show( $"파일을 삭제하시겠습니까?\n{System.IO.Path.GetFileName(filePath)}", "파일 삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning); if (result == MessageBoxResult.Yes)