AX Agent UI/UX 동등 품질 정비: ChatWindow 테마 일관성 강화 및 컨텍스트 메뉴 커스텀 팝업 전환

- ChatWindow.xaml에서 hover/selected/상태 배지/토스트/입력 칩/파일 미리보기 영역의 하드코딩 색상을 DynamicResource 기반으로 정리해 Codex/claude-code 지향 테마 일관성 강화

- 상단/하단 상태 배지와 데이터그리드 시각 요소를 PrimaryText/AccentColor/HintBackground/BorderColor로 통일해 라이트·다크 전환 시 가독성 확보

- ChatWindow.xaml.cs에서 ContextMenu/MenuItem 기반 구현 제거, 공통 커스텀 Popup 메뉴 빌더(CreateThemedPopupMenu/CreatePopupMenuItem)로 최근 폴더/메시지 우클릭 메뉴를 통합

- 기본 MessageBox.Show 사용 경로를 CustomMessageBox.Show로 교체하여 지침 준수(메시지 삭제/파일 삭제 확인)

- 검증: dotnet build 경고 0 오류 0, dotnet test(371/371 통과)
This commit is contained in:
2026-04-03 20:35:04 +09:00
parent 2c047d062d
commit 905e1835a0
2 changed files with 158 additions and 100 deletions

View File

@@ -1085,47 +1085,120 @@ public partial class ChatWindow : Window
SkillService.ActivateConditionalSkillsForPaths(_attachedFiles, cwd);
}
/// <summary>테마에 맞는 ContextMenu를 생성합니다.</summary>
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,
});
}
/// <summary>최근 폴더 항목 우클릭 컨텍스트 메뉴를 표시합니다.</summary>
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)