using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Threading; using AxCopilot.Services.Agent; namespace AxCopilot.Views; public partial class ChatWindow { // ─── 미리보기 패널 (탭 기반) ───────────────────────────────────────────── private static readonly HashSet _previewableExtensions = new(StringComparer.OrdinalIgnoreCase) { ".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log", }; /// 열려 있는 프리뷰 탭 목록 (파일 경로 기준). private readonly List _previewTabs = new(); private string? _activePreviewTab; private void TryShowPreview(string filePath) { if (string.IsNullOrEmpty(filePath) || !System.IO.File.Exists(filePath)) return; // 별도 커스텀 창으로 미리보기 (WebView2 HWND airspace 문제 근본 해결) PreviewWindow.ShowPreview(filePath, _selectedMood); } private void ShowPreviewPanel(string filePath) { // 탭에 없으면 추가 if (!_previewTabs.Contains(filePath, StringComparer.OrdinalIgnoreCase)) _previewTabs.Add(filePath); _activePreviewTab = filePath; // 패널 열기 if (PreviewColumn.Width.Value < 100) { PreviewColumn.Width = new GridLength(420); SplitterColumn.Width = new GridLength(5); } PreviewPanel.Visibility = Visibility.Visible; PreviewSplitter.Visibility = Visibility.Visible; BtnPreviewToggle.Visibility = Visibility.Visible; RebuildPreviewTabs(); LoadPreviewContent(filePath); } /// 탭 바 UI를 다시 구성합니다. private void RebuildPreviewTabs() { PreviewTabPanel.Children.Clear(); var accentBrush = ThemeResourceHelper.Accent(this); var secondaryText = ThemeResourceHelper.Secondary(this); var primaryText = ThemeResourceHelper.Primary(this); var borderBrush = ThemeResourceHelper.Border(this); foreach (var tabPath in _previewTabs) { var fileName = System.IO.Path.GetFileName(tabPath); var isActive = string.Equals(tabPath, _activePreviewTab, StringComparison.OrdinalIgnoreCase); var tabBorder = new Border { Background = isActive ? new SolidColorBrush(Color.FromArgb(0x15, 0xFF, 0xFF, 0xFF)) : Brushes.Transparent, BorderBrush = isActive ? accentBrush : Brushes.Transparent, BorderThickness = new Thickness(0, 0, 0, isActive ? 2 : 0), Padding = new Thickness(8, 6, 4, 6), Cursor = Cursors.Hand, MaxWidth = _previewTabs.Count <= 3 ? 200 : (_previewTabs.Count <= 5 ? 140 : 100), }; var tabContent = new StackPanel { Orientation = Orientation.Horizontal }; // 파일명 tabContent.Children.Add(new TextBlock { Text = fileName, FontSize = 11, Foreground = isActive ? primaryText : secondaryText, FontWeight = isActive ? FontWeights.SemiBold : FontWeights.Normal, VerticalAlignment = VerticalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis, MaxWidth = tabBorder.MaxWidth - 30, ToolTip = tabPath, }); // 닫기 버튼 (x) — 활성 탭은 항상 표시, 비활성 탭은 호버 시에만 표시 var closeFg = isActive ? primaryText : secondaryText; var closeBtnText = new TextBlock { Text = "\uE711", FontFamily = ThemeResourceHelper.SegoeMdl2, FontSize = 10, Foreground = closeFg, VerticalAlignment = VerticalAlignment.Center, }; var closeBtn = new Border { Background = Brushes.Transparent, CornerRadius = new CornerRadius(3), Padding = new Thickness(3, 2, 3, 2), Margin = new Thickness(5, 0, 0, 0), Cursor = Cursors.Hand, VerticalAlignment = VerticalAlignment.Center, // 비활성 탭은 초기에 숨김, 활성 탭은 항상 표시 Visibility = isActive ? Visibility.Visible : Visibility.Hidden, Child = closeBtnText, }; var closePath = tabPath; closeBtn.MouseEnter += (s, _) => { if (s is Border b) { b.Background = new SolidColorBrush(Color.FromArgb(0x40, 0xFF, 0x50, 0x50)); if (b.Child is TextBlock tb) tb.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0x60, 0x60)); } }; closeBtn.MouseLeave += (s, _) => { if (s is Border b) { b.Background = Brushes.Transparent; if (b.Child is TextBlock tb) tb.Foreground = closeFg; } }; closeBtn.Tag = "close"; // 닫기 버튼 식별용 closeBtn.MouseLeftButtonUp += (_, e) => { e.Handled = true; // 부모 탭 클릭 이벤트 차단 ClosePreviewTab(closePath); }; tabContent.Children.Add(closeBtn); tabBorder.Child = tabContent; // 탭 클릭 → 활성화 (MouseLeftButtonUp 사용: 닫기 버튼의 PreviewMouseLeftButtonDown보다 늦게 실행되어 충돌 방지) var clickPath = tabPath; tabBorder.MouseLeftButtonUp += (_, e) => { if (e.Handled) return; e.Handled = true; _activePreviewTab = clickPath; RebuildPreviewTabs(); LoadPreviewContent(clickPath); }; // 우클릭 → 컨텍스트 메뉴 var ctxPath = tabPath; tabBorder.MouseRightButtonUp += (_, e) => { e.Handled = true; ShowPreviewTabContextMenu(ctxPath); }; // 더블클릭 → 별도 창에서 보기 var dblPath = tabPath; tabBorder.MouseLeftButtonDown += (_, e) => { if (e.Handled) return; if (e.ClickCount == 2) { e.Handled = true; OpenPreviewPopupWindow(dblPath); } }; // 호버 효과 — 비활성 탭에서 배경 강조 + 닫기 버튼 표시 var capturedIsActive = isActive; var capturedCloseBtn = closeBtn; tabBorder.MouseEnter += (s, _) => { if (s is Border b && !capturedIsActive) b.Background = new SolidColorBrush(Color.FromArgb(0x10, 0xFF, 0xFF, 0xFF)); // 비활성 탭도 호버 시 닫기 버튼 표시 if (!capturedIsActive) capturedCloseBtn.Visibility = Visibility.Visible; }; tabBorder.MouseLeave += (s, _) => { if (s is Border b && !capturedIsActive) b.Background = Brushes.Transparent; // 비활성 탭 호버 해제 시 닫기 버튼 숨김 if (!capturedIsActive) capturedCloseBtn.Visibility = Visibility.Hidden; }; PreviewTabPanel.Children.Add(tabBorder); // 탭 사이 구분선 if (tabPath != _previewTabs[^1]) { PreviewTabPanel.Children.Add(new Border { Width = 1, Height = 14, Background = borderBrush, Margin = new Thickness(0, 4, 0, 4), VerticalAlignment = VerticalAlignment.Center, }); } } } private void ClosePreviewTab(string filePath) { _previewTabs.Remove(filePath); if (_previewTabs.Count == 0) { HidePreviewPanel(); return; } // 닫힌 탭이 활성 탭이면 마지막 탭으로 전환 if (string.Equals(filePath, _activePreviewTab, StringComparison.OrdinalIgnoreCase)) { _activePreviewTab = _previewTabs[^1]; LoadPreviewContent(_activePreviewTab); } RebuildPreviewTabs(); } private async void LoadPreviewContent(string filePath) { var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); // 모든 콘텐츠 숨기기 PreviewWebView.Visibility = Visibility.Collapsed; PreviewTextScroll.Visibility = Visibility.Collapsed; PreviewDataGrid.Visibility = Visibility.Collapsed; PreviewEmpty.Visibility = Visibility.Collapsed; if (!System.IO.File.Exists(filePath)) { PreviewEmpty.Text = "파일을 찾을 수 없습니다"; PreviewEmpty.Visibility = Visibility.Visible; return; } try { switch (ext) { case ".html": case ".htm": await EnsureWebViewInitializedAsync(); PreviewWebView.Source = new Uri(filePath); PreviewWebView.Visibility = Visibility.Visible; break; case ".csv": LoadCsvPreview(filePath); PreviewDataGrid.Visibility = Visibility.Visible; break; case ".md": await EnsureWebViewInitializedAsync(); var mdText = System.IO.File.ReadAllText(filePath); if (mdText.Length > 50000) mdText = mdText[..50000]; var mdHtml = Services.Agent.TemplateService.RenderMarkdownToHtml(mdText, _selectedMood); PreviewWebView.NavigateToString(mdHtml); PreviewWebView.Visibility = Visibility.Visible; break; case ".txt": case ".json": case ".xml": case ".log": var text = System.IO.File.ReadAllText(filePath); if (text.Length > 50000) text = text[..50000] + "\n\n... (이후 생략)"; PreviewTextBlock.Text = text; PreviewTextScroll.Visibility = Visibility.Visible; break; default: PreviewEmpty.Text = "미리보기할 수 없는 파일 형식입니다"; PreviewEmpty.Visibility = Visibility.Visible; break; } } catch (Exception ex) { PreviewTextBlock.Text = $"미리보기 오류: {ex.Message}"; PreviewTextScroll.Visibility = Visibility.Visible; } } private bool _webViewInitialized; private static readonly string WebView2DataFolder = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "WebView2"); private async Task EnsureWebViewInitializedAsync() { if (_webViewInitialized) return; try { var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync( userDataFolder: WebView2DataFolder); await PreviewWebView.EnsureCoreWebView2Async(env); _webViewInitialized = true; } catch (Exception ex) { Services.LogService.Warn($"WebView2 초기화 실패: {ex.Message}"); } } private void LoadCsvPreview(string filePath) { try { var lines = System.IO.File.ReadAllLines(filePath); if (lines.Length == 0) return; var dt = new System.Data.DataTable(); var headers = ParseCsvLine(lines[0]); foreach (var h in headers) dt.Columns.Add(h); var maxRows = Math.Min(lines.Length, 501); for (int i = 1; i < maxRows; i++) { var vals = ParseCsvLine(lines[i]); var row = dt.NewRow(); for (int j = 0; j < Math.Min(vals.Length, headers.Length); j++) row[j] = vals[j]; dt.Rows.Add(row); } PreviewDataGrid.ItemsSource = dt.DefaultView; } catch (Exception ex) { PreviewTextBlock.Text = $"CSV 로드 오류: {ex.Message}"; PreviewTextScroll.Visibility = Visibility.Visible; PreviewDataGrid.Visibility = Visibility.Collapsed; } } private static string[] ParseCsvLine(string line) { var fields = new System.Collections.Generic.List(); var current = new System.Text.StringBuilder(); bool inQuotes = false; for (int i = 0; i < line.Length; i++) { char c = line[i]; if (inQuotes) { if (c == '"' && i + 1 < line.Length && line[i + 1] == '"') { current.Append('"'); i++; } else if (c == '"') inQuotes = false; else current.Append(c); } else { if (c == '"') inQuotes = true; else if (c == ',') { fields.Add(current.ToString()); current.Clear(); } else current.Append(c); } } fields.Add(current.ToString()); return fields.ToArray(); } private void HidePreviewPanel() { _previewTabs.Clear(); _activePreviewTab = null; PreviewColumn.Width = new GridLength(0); SplitterColumn.Width = new GridLength(0); PreviewPanel.Visibility = Visibility.Collapsed; PreviewSplitter.Visibility = Visibility.Collapsed; PreviewWebView.Visibility = Visibility.Collapsed; PreviewTextScroll.Visibility = Visibility.Collapsed; PreviewDataGrid.Visibility = Visibility.Collapsed; try { if (_webViewInitialized) PreviewWebView.CoreWebView2?.NavigateToString(""); } catch (Exception) { /* WebView 초기화 실패 */ } } /// 프리뷰 탭 바 클릭 시 WebView2에서 포커스를 회수 (HWND airspace 문제 방지). private void PreviewTabBar_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { // WebView2가 포커스를 잡고 있으면 WPF 버튼 클릭이 무시될 수 있으므로 포커스를 강제 이동 if (PreviewWebView.IsFocused || PreviewWebView.IsKeyboardFocusWithin) { var border = sender as Border; border?.Focus(); } } private void BtnClosePreview_Click(object sender, RoutedEventArgs e) { HidePreviewPanel(); BtnPreviewToggle.Visibility = Visibility.Collapsed; } private void BtnPreviewToggle_Click(object sender, RoutedEventArgs e) { if (PreviewPanel.Visibility == Visibility.Visible) { // 숨기기 (탭은 유지) PreviewPanel.Visibility = Visibility.Collapsed; PreviewSplitter.Visibility = Visibility.Collapsed; PreviewColumn.Width = new GridLength(0); SplitterColumn.Width = new GridLength(0); } else if (_previewTabs.Count > 0) { // 다시 열기 PreviewPanel.Visibility = Visibility.Visible; PreviewSplitter.Visibility = Visibility.Visible; PreviewColumn.Width = new GridLength(420); SplitterColumn.Width = new GridLength(5); RebuildPreviewTabs(); if (_activePreviewTab != null) LoadPreviewContent(_activePreviewTab); } } private void BtnOpenExternal_Click(object sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(_activePreviewTab) || !System.IO.File.Exists(_activePreviewTab)) return; try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = _activePreviewTab, UseShellExecute = true, }); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"외부 프로그램 실행 오류: {ex.Message}"); } } }