using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace AxCopilot.Views; public partial class ChatWindow { private void UpdateGitBranchUi(string? branchName, string filesText, string addedText, string deletedText, string tooltip, Visibility visibility) { Dispatcher.Invoke(() => { _currentGitBranchName = branchName; _currentGitTooltip = tooltip; if (BtnGitBranch != null) { BtnGitBranch.Visibility = visibility; BtnGitBranch.ToolTip = string.IsNullOrWhiteSpace(tooltip) ? "현재 Git 브랜치 상태" : tooltip; } if (GitBranchLabel != null) GitBranchLabel.Text = string.IsNullOrWhiteSpace(branchName) ? "브랜치 없음" : branchName; if (GitBranchFilesText != null) GitBranchFilesText.Text = filesText; if (GitBranchAddedText != null) GitBranchAddedText.Text = addedText; if (GitBranchDeletedText != null) GitBranchDeletedText.Text = deletedText; if (GitBranchSeparator != null) GitBranchSeparator.Visibility = visibility; }); } private void BuildGitBranchPopup() { if (GitBranchItems == null) return; GitBranchItems.Children.Clear(); var gitRoot = _currentGitRoot ?? ResolveGitRoot(GetCurrentWorkFolder()); var branchName = _currentGitBranchName ?? "detached"; var tooltip = _currentGitTooltip ?? ""; var fileText = GitBranchFilesText?.Text ?? ""; var addedText = GitBranchAddedText?.Text ?? ""; var deletedText = GitBranchDeletedText?.Text ?? ""; var query = (_gitBranchSearchText ?? "").Trim(); var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue; var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; GitBranchItems.Children.Add(CreatePopupSummaryStrip(new[] { ("브랜치", string.IsNullOrWhiteSpace(branchName) ? "없음" : branchName, "#F8FAFC", "#E2E8F0", "#475569"), ("파일", string.IsNullOrWhiteSpace(fileText) ? "0" : fileText, "#EFF6FF", "#BFDBFE", "#1D4ED8"), ("최근", _recentGitBranches.Count.ToString(), "#F5F3FF", "#DDD6FE", "#6D28D9"), })); GitBranchItems.Children.Add(CreatePopupSectionLabel("현재 브랜치", new Thickness(8, 6, 8, 4))); GitBranchItems.Children.Add(CreatePopupMenuRow( "\uE943", branchName, string.IsNullOrWhiteSpace(fileText) ? "현재 브랜치" : fileText, true, accentBrush, secondaryText, primaryText, () => { })); if (!string.IsNullOrWhiteSpace(addedText) || !string.IsNullOrWhiteSpace(deletedText)) { var stats = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(8, 2, 8, 8), }; if (!string.IsNullOrWhiteSpace(addedText)) stats.Children.Add(CreateMetricPill(addedText, "#16A34A")); if (!string.IsNullOrWhiteSpace(deletedText)) stats.Children.Add(CreateMetricPill(deletedText, "#DC2626")); GitBranchItems.Children.Add(stats); } if (!string.IsNullOrWhiteSpace(gitRoot)) { GitBranchItems.Children.Add(CreatePopupSectionLabel("저장소", new Thickness(8, 6, 8, 4))); GitBranchItems.Children.Add(CreatePopupMenuRow( "\uED25", System.IO.Path.GetFileName(gitRoot.TrimEnd('\\', '/')), gitRoot, false, accentBrush, secondaryText, primaryText, () => { })); } if (!string.IsNullOrWhiteSpace(_currentGitUpstreamStatus)) { GitBranchItems.Children.Add(CreatePopupMenuRow( "\uE8AB", "업스트림", _currentGitUpstreamStatus!, false, accentBrush, secondaryText, primaryText, () => { })); } GitBranchItems.Children.Add(CreatePopupSectionLabel("빠른 작업", new Thickness(8, 10, 8, 4))); GitBranchItems.Children.Add(CreatePopupMenuRow( "\uE8C8", "상태 요약 복사", "브랜치 변경 파일, 추가/삭제 라인 복사", false, accentBrush, secondaryText, primaryText, () => { try { Clipboard.SetText(tooltip); } catch { } GitBranchPopup.IsOpen = false; })); GitBranchItems.Children.Add(CreatePopupMenuRow( "\uE72B", "새로고침", "Git 상태를 다시 조회합니다.", false, accentBrush, secondaryText, primaryText, async () => { await RefreshGitBranchStatusAsync(); BuildGitBranchPopup(); })); var filteredBranches = _currentGitBranches .Where(branch => string.IsNullOrWhiteSpace(query) || branch.Contains(query, StringComparison.OrdinalIgnoreCase)) .Take(20) .ToList(); var recentBranches = _recentGitBranches .Where(branch => _currentGitBranches.Any(current => string.Equals(current, branch, StringComparison.OrdinalIgnoreCase))) .Where(branch => string.IsNullOrWhiteSpace(query) || branch.Contains(query, StringComparison.OrdinalIgnoreCase)) .Take(5) .ToList(); if (recentBranches.Count > 0) { GitBranchItems.Children.Add(CreatePopupSectionLabel($"최근 전환 · {recentBranches.Count}", new Thickness(8, 10, 8, 4))); foreach (var branch in recentBranches) { var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase); GitBranchItems.Children.Add(CreatePopupMenuRow( isCurrent ? "\uE73E" : "\uE8FD", branch, isCurrent ? "현재 브랜치" : "최근 사용 브랜치", isCurrent, accentBrush, secondaryText, primaryText, isCurrent ? null : () => _ = SwitchGitBranchAsync(branch))); } } if (_currentGitBranches.Count > 0) { var branchSectionLabel = string.IsNullOrWhiteSpace(query) ? $"브랜치 전환 · {_currentGitBranches.Count}" : $"브랜치 전환 · {filteredBranches.Count}/{_currentGitBranches.Count}"; GitBranchItems.Children.Add(CreatePopupSectionLabel(branchSectionLabel, new Thickness(8, 10, 8, 4))); foreach (var branch in filteredBranches) { if (recentBranches.Any(recent => string.Equals(recent, branch, StringComparison.OrdinalIgnoreCase))) continue; var isCurrent = string.Equals(branch, branchName, StringComparison.OrdinalIgnoreCase); GitBranchItems.Children.Add(CreatePopupMenuRow( isCurrent ? "\uE73E" : "\uE943", branch, isCurrent ? "현재 브랜치" : "이 브랜치로 전환", isCurrent, accentBrush, secondaryText, primaryText, isCurrent ? null : () => _ = SwitchGitBranchAsync(branch))); } if (!string.IsNullOrWhiteSpace(query) && filteredBranches.Count == 0) { GitBranchItems.Children.Add(new TextBlock { Text = "검색 결과가 없습니다.", FontSize = 11.5, Foreground = secondaryText, Margin = new Thickness(10, 6, 10, 10), }); } } GitBranchItems.Children.Add(CreatePopupSectionLabel("브랜치 작업", new Thickness(8, 10, 8, 4))); GitBranchItems.Children.Add(CreatePopupMenuRow( "\uE710", "새 브랜치 생성", "현재 작업 기준으로 새 브랜치를 만들고 전환합니다.", false, accentBrush, secondaryText, primaryText, () => _ = CreateGitBranchAsync())); } private TextBlock CreatePopupSectionLabel(string text, Thickness? margin = null) { return new TextBlock { Text = text, FontSize = 10.5, FontWeight = FontWeights.SemiBold, Foreground = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray, Margin = margin ?? new Thickness(8, 8, 8, 4), }; } private UIElement CreatePopupSummaryStrip(IEnumerable<(string Label, string Value, string BgHex, string BorderHex, string FgHex)> items) { var wrap = new WrapPanel { Margin = new Thickness(8, 6, 8, 6), }; foreach (var item in items) wrap.Children.Add(CreateMetricPill($"{item.Label} {item.Value}", item.FgHex, item.BgHex, item.BorderHex)); return wrap; } private Border CreateMetricPill(string text, string colorHex) => CreateMetricPill(text, colorHex, $"{colorHex}18", $"{colorHex}44"); private Border CreateMetricPill(string text, string colorHex, string bgHex, string borderHex) { return new Border { Background = BrushFromHex(bgHex), BorderBrush = BrushFromHex(borderHex), BorderThickness = new Thickness(1), CornerRadius = new CornerRadius(999), Padding = new Thickness(8, 3, 8, 3), Margin = new Thickness(0, 0, 6, 0), Child = new TextBlock { Text = text, FontSize = 10.5, FontWeight = FontWeights.SemiBold, Foreground = BrushFromHex(colorHex), } }; } private Border CreateFlatPopupRow(string icon, string title, string description, string colorHex, bool clickable, Action? onClick) { var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; var hoverBrush = TryFindResource("ItemHoverBackground") as Brush ?? Brushes.LightGray; var borderColor = TryFindResource("BorderColor") as Brush ?? Brushes.Gray; var border = new Border { Background = Brushes.Transparent, BorderBrush = borderColor, BorderThickness = new Thickness(0, 0, 0, 1), Padding = new Thickness(8, 9, 8, 9), Cursor = clickable ? Cursors.Hand : Cursors.Arrow, Focusable = clickable, }; KeyboardNavigation.SetIsTabStop(border, clickable); var grid = new Grid(); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto }); grid.Children.Add(new TextBlock { Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 11, Foreground = BrushFromHex(colorHex), VerticalAlignment = VerticalAlignment.Top, Margin = new Thickness(0, 1, 10, 0), }); var textStack = new StackPanel(); textStack.Children.Add(new TextBlock { Text = title, FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = primaryText, }); if (!string.IsNullOrWhiteSpace(description)) { textStack.Children.Add(new TextBlock { Text = description, FontSize = 10.5, Foreground = secondaryText, Margin = new Thickness(0, 2, 0, 0), TextWrapping = TextWrapping.Wrap, }); } Grid.SetColumn(textStack, 1); grid.Children.Add(textStack); if (clickable) { var chevron = new TextBlock { Text = "\uE76C", FontFamily = new FontFamily("Segoe MDL2 Assets"), FontSize = 10, Foreground = secondaryText, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(8, 0, 0, 0), }; Grid.SetColumn(chevron, 2); grid.Children.Add(chevron); } border.Child = grid; if (clickable && onClick != null) { border.MouseEnter += (_, _) => border.Background = hoverBrush; border.MouseLeave += (_, _) => border.Background = Brushes.Transparent; border.MouseLeftButtonUp += (_, _) => onClick(); border.KeyDown += (_, keyEvent) => { if (keyEvent.Key is Key.Enter or Key.Space) { keyEvent.Handled = true; onClick(); } }; } return border; } }