- ChatWindow.PreviewPresentation.cs를 추가해 프리뷰 탭 목록, 헤더, 파일 로드, CSV·텍스트·마크다운·HTML 표시, 컨텍스트 메뉴, 별도 창 미리보기 흐름을 메인 창 코드에서 분리함 - ChatWindow.xaml.cs는 transcript 및 런타임 orchestration 중심으로 더 정리해 claw-code 기준 preview surface 품질 개선을 이어가기 쉬운 구조로 개선함 - README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:47 (KST) 기준 변경 이력을 반영함 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 기준 경고 0 / 오류 0 확인
This commit is contained in:
786
src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs
Normal file
786
src/AxCopilot/Views/ChatWindow.PreviewPresentation.cs
Normal file
@@ -0,0 +1,786 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.Views;
|
||||
|
||||
public partial class ChatWindow
|
||||
{
|
||||
private static readonly HashSet<string> _previewableExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".html", ".htm", ".md", ".txt", ".csv", ".json", ".xml", ".log",
|
||||
};
|
||||
|
||||
/// <summary>열려 있는 프리뷰 탭 목록 (파일 경로 기준).</summary>
|
||||
private readonly List<string> _previewTabs = new();
|
||||
private string? _activePreviewTab;
|
||||
private bool _webViewInitialized;
|
||||
private Popup? _previewTabPopup;
|
||||
|
||||
private static readonly string WebView2DataFolder =
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AxCopilot", "WebView2");
|
||||
|
||||
private void TryShowPreview(string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath))
|
||||
return;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void RebuildPreviewTabs()
|
||||
{
|
||||
PreviewTabPanel.Children.Clear();
|
||||
|
||||
var accentBrush = TryFindResource("AccentColor") as Brush ?? Brushes.CornflowerBlue;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
|
||||
foreach (var tabPath in _previewTabs)
|
||||
{
|
||||
var fileName = 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,
|
||||
});
|
||||
|
||||
var closeFg = isActive ? primaryText : secondaryText;
|
||||
var closeBtnText = new TextBlock
|
||||
{
|
||||
Text = "\uE711",
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
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,
|
||||
Tag = "close",
|
||||
};
|
||||
|
||||
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.MouseLeftButtonUp += (_, e) =>
|
||||
{
|
||||
e.Handled = true;
|
||||
ClosePreviewTab(closePath);
|
||||
};
|
||||
|
||||
tabContent.Children.Add(closeBtn);
|
||||
tabBorder.Child = tabContent;
|
||||
|
||||
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 (!string.Equals(tabPath, _previewTabs[^1], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
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 = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
SetPreviewHeader(filePath);
|
||||
|
||||
PreviewWebView.Visibility = Visibility.Collapsed;
|
||||
PreviewTextScroll.Visibility = Visibility.Collapsed;
|
||||
PreviewDataGrid.Visibility = Visibility.Collapsed;
|
||||
PreviewEmpty.Visibility = Visibility.Collapsed;
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
SetPreviewHeaderState("파일을 찾을 수 없습니다");
|
||||
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 = 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 = File.ReadAllText(filePath);
|
||||
if (text.Length > 50000)
|
||||
text = text[..50000] + "\n\n... (이후 생략)";
|
||||
PreviewTextBlock.Text = text;
|
||||
PreviewTextScroll.Visibility = Visibility.Visible;
|
||||
break;
|
||||
|
||||
default:
|
||||
SetPreviewHeaderState("지원되지 않는 형식");
|
||||
PreviewEmpty.Text = "미리보기할 수 없는 파일 형식입니다";
|
||||
PreviewEmpty.Visibility = Visibility.Visible;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetPreviewHeaderState("미리보기 오류");
|
||||
PreviewTextBlock.Text = $"미리보기 오류: {ex.Message}";
|
||||
PreviewTextScroll.Visibility = Visibility.Visible;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPreviewHeader(string filePath)
|
||||
{
|
||||
if (PreviewHeaderTitle == null || PreviewHeaderSubtitle == null || PreviewHeaderMeta == null)
|
||||
return;
|
||||
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var extension = Path.GetExtension(filePath).TrimStart('.').ToUpperInvariant();
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
var sizeText = fileInfo.Exists
|
||||
? fileInfo.Length >= 1024 * 1024
|
||||
? $"{fileInfo.Length / 1024d / 1024d:F1} MB"
|
||||
: $"{Math.Max(1, fileInfo.Length / 1024d):F0} KB"
|
||||
: "파일 없음";
|
||||
|
||||
PreviewHeaderTitle.Text = string.IsNullOrWhiteSpace(fileName) ? "미리보기" : fileName;
|
||||
PreviewHeaderSubtitle.Text = filePath;
|
||||
PreviewHeaderMeta.Text = string.IsNullOrWhiteSpace(extension)
|
||||
? sizeText
|
||||
: $"{extension} · {sizeText}";
|
||||
}
|
||||
|
||||
private void SetPreviewHeaderState(string state)
|
||||
{
|
||||
if (PreviewHeaderMeta != null && !string.IsNullOrWhiteSpace(state))
|
||||
PreviewHeaderMeta.Text = state;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
LogService.Warn($"WebView2 초기화 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadCsvPreview(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var lines = File.ReadAllLines(filePath);
|
||||
if (lines.Length == 0)
|
||||
return;
|
||||
|
||||
var dt = new DataTable();
|
||||
var headers = ParseCsvLine(lines[0]);
|
||||
foreach (var h in headers)
|
||||
dt.Columns.Add(h);
|
||||
|
||||
var maxRows = Math.Min(lines.Length, 501);
|
||||
for (var i = 1; i < maxRows; i++)
|
||||
{
|
||||
var vals = ParseCsvLine(lines[i]);
|
||||
var row = dt.NewRow();
|
||||
for (var 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 List<string>();
|
||||
var current = new StringBuilder();
|
||||
var inQuotes = false;
|
||||
|
||||
for (var i = 0; i < line.Length; i++)
|
||||
{
|
||||
var 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;
|
||||
if (PreviewHeaderTitle != null) PreviewHeaderTitle.Text = "미리보기";
|
||||
if (PreviewHeaderSubtitle != null) PreviewHeaderSubtitle.Text = "선택한 파일이 여기에 표시됩니다";
|
||||
if (PreviewHeaderMeta != null) PreviewHeaderMeta.Text = "파일 메타";
|
||||
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("<html></html>");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewTabBar_PreviewMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
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) || !File.Exists(_activePreviewTab))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = _activePreviewTab,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"외부 프로그램 실행 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowPreviewTabContextMenu(string filePath)
|
||||
{
|
||||
if (_previewTabPopup != null)
|
||||
_previewTabPopup.IsOpen = false;
|
||||
|
||||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||||
var borderBrush = TryFindResource("BorderColor") as Brush ?? Brushes.Gray;
|
||||
var primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray;
|
||||
var hoverBg = TryFindResource("ItemHoverBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF));
|
||||
|
||||
var stack = new StackPanel();
|
||||
|
||||
void AddItem(string icon, string iconColor, string label, Action action)
|
||||
{
|
||||
var itemBorder = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
CornerRadius = new CornerRadius(6),
|
||||
Padding = new Thickness(10, 7, 16, 7),
|
||||
Cursor = Cursors.Hand,
|
||||
};
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = icon,
|
||||
FontFamily = new FontFamily("Segoe MDL2 Assets"),
|
||||
FontSize = 12,
|
||||
Foreground = string.IsNullOrEmpty(iconColor)
|
||||
? secondaryText
|
||||
: new SolidColorBrush((Color)ColorConverter.ConvertFromString(iconColor)),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
});
|
||||
sp.Children.Add(new TextBlock
|
||||
{
|
||||
Text = label,
|
||||
FontSize = 13,
|
||||
Foreground = primaryText,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
});
|
||||
itemBorder.Child = sp;
|
||||
itemBorder.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; };
|
||||
itemBorder.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; };
|
||||
itemBorder.MouseLeftButtonUp += (_, _) =>
|
||||
{
|
||||
_previewTabPopup!.IsOpen = false;
|
||||
action();
|
||||
};
|
||||
stack.Children.Add(itemBorder);
|
||||
}
|
||||
|
||||
void AddSeparator()
|
||||
{
|
||||
stack.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Background = borderBrush,
|
||||
Margin = new Thickness(8, 3, 8, 3),
|
||||
});
|
||||
}
|
||||
|
||||
AddItem("\uE8A7", "#64B5F6", "외부 프로그램으로 열기", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = filePath,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
AddItem("\uE838", "#FFB74D", "파일 위치 열기", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start("explorer.exe", $"/select,\"{filePath}\"");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
AddItem("\uE8A7", "#81C784", "별도 창에서 보기", () => OpenPreviewPopupWindow(filePath));
|
||||
|
||||
AddSeparator();
|
||||
|
||||
AddItem("\uE8C8", "", "경로 복사", () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
|
||||
AddSeparator();
|
||||
|
||||
AddItem("\uE711", "#EF5350", "이 탭 닫기", () => ClosePreviewTab(filePath));
|
||||
|
||||
if (_previewTabs.Count > 1)
|
||||
{
|
||||
AddItem("\uE8BB", "#EF5350", "다른 탭 모두 닫기", () =>
|
||||
{
|
||||
var keep = filePath;
|
||||
_previewTabs.RemoveAll(p => !string.Equals(p, keep, StringComparison.OrdinalIgnoreCase));
|
||||
_activePreviewTab = keep;
|
||||
RebuildPreviewTabs();
|
||||
LoadPreviewContent(keep);
|
||||
});
|
||||
}
|
||||
|
||||
var popupBorder = new Border
|
||||
{
|
||||
Background = bg,
|
||||
BorderBrush = borderBrush,
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(12),
|
||||
Padding = new Thickness(4, 6, 4, 6),
|
||||
MinWidth = 180,
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect
|
||||
{
|
||||
BlurRadius = 16,
|
||||
Opacity = 0.4,
|
||||
ShadowDepth = 4,
|
||||
Color = Colors.Black,
|
||||
},
|
||||
Child = stack,
|
||||
};
|
||||
|
||||
_previewTabPopup = new Popup
|
||||
{
|
||||
Child = popupBorder,
|
||||
Placement = PlacementMode.MousePoint,
|
||||
StaysOpen = false,
|
||||
AllowsTransparency = true,
|
||||
PopupAnimation = PopupAnimation.Fade,
|
||||
};
|
||||
_previewTabPopup.IsOpen = true;
|
||||
}
|
||||
|
||||
private void OpenPreviewPopupWindow(string filePath)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
return;
|
||||
|
||||
var ext = Path.GetExtension(filePath).ToLowerInvariant();
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
var bg = TryFindResource("LauncherBackground") as Brush ?? new SolidColorBrush(Color.FromRgb(0x1E, 0x1E, 0x2E));
|
||||
var fg = TryFindResource("PrimaryText") as Brush ?? Brushes.White;
|
||||
|
||||
var win = new Window
|
||||
{
|
||||
Title = $"미리보기 — {fileName}",
|
||||
Width = 900,
|
||||
Height = 700,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Background = bg,
|
||||
};
|
||||
|
||||
FrameworkElement content;
|
||||
|
||||
switch (ext)
|
||||
{
|
||||
case ".html":
|
||||
case ".htm":
|
||||
var wv = new Microsoft.Web.WebView2.Wpf.WebView2();
|
||||
wv.Loaded += async (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
|
||||
userDataFolder: WebView2DataFolder);
|
||||
await wv.EnsureCoreWebView2Async(env);
|
||||
wv.Source = new Uri(filePath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
};
|
||||
content = wv;
|
||||
break;
|
||||
|
||||
case ".md":
|
||||
var mdWv = new Microsoft.Web.WebView2.Wpf.WebView2();
|
||||
var mdMood = _selectedMood;
|
||||
mdWv.Loaded += async (_, _) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var env = await Microsoft.Web.WebView2.Core.CoreWebView2Environment.CreateAsync(
|
||||
userDataFolder: WebView2DataFolder);
|
||||
await mdWv.EnsureCoreWebView2Async(env);
|
||||
var mdSrc = File.ReadAllText(filePath);
|
||||
if (mdSrc.Length > 100000)
|
||||
mdSrc = mdSrc[..100000];
|
||||
var html = Services.Agent.TemplateService.RenderMarkdownToHtml(mdSrc, mdMood);
|
||||
mdWv.NavigateToString(html);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
};
|
||||
content = mdWv;
|
||||
break;
|
||||
|
||||
case ".csv":
|
||||
var dg = new DataGrid
|
||||
{
|
||||
AutoGenerateColumns = true,
|
||||
IsReadOnly = true,
|
||||
Background = Brushes.Transparent,
|
||||
Foreground = Brushes.White,
|
||||
BorderThickness = new Thickness(0),
|
||||
FontSize = 12,
|
||||
};
|
||||
try
|
||||
{
|
||||
var lines = File.ReadAllLines(filePath);
|
||||
if (lines.Length > 0)
|
||||
{
|
||||
var dt = new DataTable();
|
||||
var headers = ParseCsvLine(lines[0]);
|
||||
foreach (var h in headers)
|
||||
dt.Columns.Add(h);
|
||||
for (var i = 1; i < Math.Min(lines.Length, 1001); i++)
|
||||
{
|
||||
var vals = ParseCsvLine(lines[i]);
|
||||
var row = dt.NewRow();
|
||||
for (var j = 0; j < Math.Min(vals.Length, dt.Columns.Count); j++)
|
||||
row[j] = vals[j];
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
|
||||
dg.ItemsSource = dt.DefaultView;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
content = dg;
|
||||
break;
|
||||
|
||||
default:
|
||||
var text = File.ReadAllText(filePath);
|
||||
if (text.Length > 100000)
|
||||
text = text[..100000] + "\n\n... (이후 생략)";
|
||||
var sv = new ScrollViewer
|
||||
{
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
Padding = new Thickness(20),
|
||||
Content = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontSize = 13,
|
||||
Foreground = fg,
|
||||
},
|
||||
};
|
||||
content = sv;
|
||||
break;
|
||||
}
|
||||
|
||||
win.Content = content;
|
||||
win.Show();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user