diff --git a/README.md b/README.md index f5a601f..10ff94b 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저 개발 참고: Claw Code 동등성 작업 추적 문서 `docs/claw-code-parity-plan.md` +- 업데이트: 2026-04-06 08:55 (KST) +- AX Agent 파일 브라우저 렌더를 `ChatWindow.FileBrowserPresentation.cs`로 분리했습니다. 파일 탐색기 열기/닫기, 폴더 트리 구성, 파일 헤더/아이콘/크기 표시, 우클릭 메뉴, 디바운스 새로고침 흐름이 메인 창 코드 밖으로 이동했습니다. +- `ChatWindow.xaml.cs`는 transcript·runtime orchestration 중심으로 더 정리됐고, claw-code 기준 사이드 surface 품질 작업을 이어가기 쉬운 구조로 맞췄습니다. + - 업데이트: 2026-04-06 08:47 (KST) - AX Agent 우측 프리뷰 패널 렌더를 `ChatWindow.PreviewPresentation.cs`로 분리했습니다. 프리뷰 탭 목록, 헤더, 파일 로드, CSV/텍스트/마크다운/HTML 표시, 숨김/열기, 우클릭 메뉴, 별도 창 미리보기 흐름이 메인 창 코드 밖으로 이동했습니다. - `ChatWindow.xaml.cs`는 transcript 및 런타임 orchestration 중심으로 더 정리됐고, claw-code 기준 preview surface 품질 작업을 이어가기 쉬운 구조로 맞췄습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 98cd864..6162af7 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -4901,3 +4901,5 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - Document update: 2026-04-06 08:39 (KST) - This pass reduces direct status-line branching in the main chat window file and keeps AX Agent closer to the `claw-code` model where runtime/footer presentation is separated from session orchestration. - Document update: 2026-04-06 08:47 (KST) - Split preview surface rendering out of `ChatWindow.xaml.cs` into `ChatWindow.PreviewPresentation.cs`. Preview tab tracking, panel open/close, tab bar rebuilding, preview header state, CSV/text/markdown/HTML loaders, context menu actions, and external popup preview are now grouped in a dedicated partial. - Document update: 2026-04-06 08:47 (KST) - This keeps `ChatWindow.xaml.cs` focused on transcript/runtime orchestration and aligns AX Agent more closely with the `claw-code` model where preview/session surfaces are treated as separate presentation layers. +- Document update: 2026-04-06 08:55 (KST) - Split file browser presentation out of `ChatWindow.xaml.cs` into `ChatWindow.FileBrowserPresentation.cs`. File browser open/close handlers, folder tree population, file item header/icon/size formatting, context menu actions, and debounced refresh logic now live in a dedicated partial. +- Document update: 2026-04-06 08:55 (KST) - This keeps `ChatWindow.xaml.cs` more orchestration-focused and aligns AX Agent more closely with the `claw-code` model where sidebar/file surfaces are separated from transcript and runtime flow. diff --git a/src/AxCopilot/Views/ChatWindow.FileBrowserPresentation.cs b/src/AxCopilot/Views/ChatWindow.FileBrowserPresentation.cs new file mode 100644 index 0000000..a1c82dc --- /dev/null +++ b/src/AxCopilot/Views/ChatWindow.FileBrowserPresentation.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; + +namespace AxCopilot.Views; + +public partial class ChatWindow +{ + private static readonly HashSet _ignoredDirs = new(StringComparer.OrdinalIgnoreCase) + { + "bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode", + "__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build", + ".cache", ".next", ".nuxt", "coverage", ".terraform", + }; + + private DispatcherTimer? _fileBrowserRefreshTimer; + + private void ToggleFileBrowser() + { + if (FileBrowserPanel.Visibility == Visibility.Visible) + { + FileBrowserPanel.Visibility = Visibility.Collapsed; + _settings.Settings.Llm.ShowFileBrowser = false; + } + else + { + FileBrowserPanel.Visibility = Visibility.Visible; + _settings.Settings.Llm.ShowFileBrowser = true; + BuildFileTree(); + } + + _settings.Save(); + } + + private void BtnFileBrowserRefresh_Click(object sender, RoutedEventArgs e) => BuildFileTree(); + + private void BtnFileBrowserOpenFolder_Click(object sender, RoutedEventArgs e) + { + var folder = GetCurrentWorkFolder(); + if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder)) + return; + + try + { + Process.Start(new ProcessStartInfo { FileName = folder, UseShellExecute = true }); + } + catch + { + } + } + + private void BtnFileBrowserClose_Click(object sender, RoutedEventArgs e) + { + FileBrowserPanel.Visibility = Visibility.Collapsed; + } + + private void BuildFileTree() + { + FileTreeView.Items.Clear(); + var folder = GetCurrentWorkFolder(); + if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder)) + { + FileTreeView.Items.Add(new TreeViewItem { Header = "작업 폴더를 선택하세요", IsEnabled = false }); + return; + } + + FileBrowserTitle.Text = $"파일 탐색기 — {Path.GetFileName(folder)}"; + var count = 0; + PopulateDirectory(new DirectoryInfo(folder), FileTreeView.Items, 0, ref count); + } + + private void PopulateDirectory(DirectoryInfo dir, ItemCollection items, int depth, ref int count) + { + if (depth > 4 || count > 200) + return; + + try + { + foreach (var subDir in dir.GetDirectories().OrderBy(d => d.Name)) + { + if (count > 200) + break; + if (_ignoredDirs.Contains(subDir.Name) || subDir.Name.StartsWith('.')) + continue; + + count++; + var dirItem = new TreeViewItem + { + Header = CreateFileTreeHeader("\uED25", subDir.Name, null), + Tag = subDir.FullName, + IsExpanded = depth < 1, + }; + + if (depth < 3) + { + dirItem.Items.Add(new TreeViewItem { Header = "로딩 중..." }); + var capturedDir = subDir; + var capturedDepth = depth; + dirItem.Expanded += (s, _) => + { + if (s is TreeViewItem ti && ti.Items.Count == 1 && ti.Items[0] is TreeViewItem d && d.Header?.ToString() == "로딩 중...") + { + ti.Items.Clear(); + var c = 0; + PopulateDirectory(capturedDir, ti.Items, capturedDepth + 1, ref c); + } + }; + } + else + { + PopulateDirectory(subDir, dirItem.Items, depth + 1, ref count); + } + + items.Add(dirItem); + } + } + catch + { + } + + try + { + foreach (var file in dir.GetFiles().OrderBy(f => f.Name)) + { + if (count > 200) + break; + count++; + + var ext = file.Extension.ToLowerInvariant(); + var icon = GetFileIcon(ext); + var size = FormatFileSize(file.Length); + + var fileItem = new TreeViewItem + { + Header = CreateFileTreeHeader(icon, file.Name, size), + Tag = file.FullName, + }; + + var capturedPath = file.FullName; + fileItem.MouseDoubleClick += (_, e) => + { + e.Handled = true; + TryShowPreview(capturedPath); + }; + + fileItem.MouseRightButtonUp += (s, e) => + { + e.Handled = true; + if (s is TreeViewItem ti) + ti.IsSelected = true; + ShowFileTreeContextMenu(capturedPath); + }; + + items.Add(fileItem); + } + } + catch + { + } + } + + private static StackPanel CreateFileTreeHeader(string icon, string name, string? sizeText) + { + var sp = new StackPanel { Orientation = Orientation.Horizontal }; + sp.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 11, + Foreground = new SolidColorBrush(Color.FromRgb(0x9C, 0xA3, 0xAF)), + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 5, 0), + }); + sp.Children.Add(new TextBlock + { + Text = name, + FontSize = 11.5, + VerticalAlignment = VerticalAlignment.Center, + }); + if (sizeText != null) + { + sp.Children.Add(new TextBlock + { + Text = $" {sizeText}", + FontSize = 10, + Foreground = new SolidColorBrush(Color.FromRgb(0x6B, 0x72, 0x80)), + VerticalAlignment = VerticalAlignment.Center, + }); + } + + return sp; + } + + private static string GetFileIcon(string ext) => ext switch + { + ".html" or ".htm" => "\uEB41", + ".xlsx" or ".xls" => "\uE9F9", + ".docx" or ".doc" => "\uE8A5", + ".pdf" => "\uEA90", + ".csv" => "\uE80A", + ".md" => "\uE70B", + ".json" or ".xml" => "\uE943", + ".png" or ".jpg" or ".jpeg" or ".gif" or ".svg" or ".webp" => "\uEB9F", + ".cs" or ".py" or ".js" or ".ts" or ".java" or ".cpp" => "\uE943", + ".bat" or ".cmd" or ".ps1" or ".sh" => "\uE756", + ".txt" or ".log" => "\uE8A5", + _ => "\uE7C3", + }; + + private static string FormatFileSize(long bytes) => bytes switch + { + < 1024 => $"{bytes} B", + < 1024 * 1024 => $"{bytes / 1024.0:F1} KB", + _ => $"{bytes / (1024.0 * 1024.0):F1} MB", + }; + + private void ShowFileTreeContextMenu(string filePath) + { + 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("HintBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); + var dangerBrush = new SolidColorBrush(Color.FromRgb(0xE7, 0x4C, 0x3C)); + + var popup = new Popup + { + StaysOpen = false, + AllowsTransparency = true, + PopupAnimation = PopupAnimation.Fade, + Placement = PlacementMode.MousePoint, + }; + var panel = new StackPanel { Margin = new Thickness(2) }; + var container = new Border + { + Background = bg, + BorderBrush = borderBrush, + BorderThickness = new Thickness(1), + CornerRadius = new CornerRadius(10), + Padding = new Thickness(6), + MinWidth = 200, + Effect = new System.Windows.Media.Effects.DropShadowEffect + { + BlurRadius = 16, + ShadowDepth = 4, + Opacity = 0.3, + Color = Colors.Black, + Direction = 270, + }, + Child = panel, + }; + popup.Child = container; + + void AddItem(string icon, string label, Action action, Brush? labelColor = null, Brush? iconColor = null) + { + var sp = new StackPanel { Orientation = Orientation.Horizontal }; + sp.Children.Add(new TextBlock + { + Text = icon, + FontFamily = new FontFamily("Segoe MDL2 Assets"), + FontSize = 13, + Foreground = iconColor ?? secondaryText, + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(0, 0, 10, 0), + }); + sp.Children.Add(new TextBlock + { + Text = label, + FontSize = 12.5, + Foreground = labelColor ?? primaryText, + VerticalAlignment = VerticalAlignment.Center, + }); + var item = new Border + { + Child = sp, + Background = Brushes.Transparent, + CornerRadius = new CornerRadius(7), + Cursor = Cursors.Hand, + Padding = new Thickness(10, 8, 14, 8), + Margin = new Thickness(0, 1, 0, 1), + }; + item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; + item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; + item.MouseLeftButtonUp += (_, _) => + { + popup.IsOpen = false; + action(); + }; + panel.Children.Add(item); + } + + void AddSep() + { + panel.Children.Add(new Border + { + Height = 1, + Margin = new Thickness(10, 4, 10, 4), + Background = borderBrush, + Opacity = 0.3, + }); + } + + var ext = Path.GetExtension(filePath).ToLowerInvariant(); + if (_previewableExtensions.Contains(ext)) + AddItem("\uE8A1", "미리보기", () => ShowPreviewPanel(filePath)); + + AddItem("\uE8A7", "외부 프로그램으로 열기", () => + { + try + { + Process.Start(new ProcessStartInfo { FileName = filePath, UseShellExecute = true }); + } + catch + { + } + }); + AddItem("\uED25", "폴더에서 보기", () => + { + try + { + Process.Start("explorer.exe", $"/select,\"{filePath}\""); + } + catch + { + } + }); + AddItem("\uE8C8", "경로 복사", () => + { + try + { + Clipboard.SetText(filePath); + ShowToast("경로 복사됨"); + } + catch + { + } + }); + + AddSep(); + + AddItem("\uE8AC", "이름 변경", () => + { + var dir = Path.GetDirectoryName(filePath) ?? ""; + var oldName = Path.GetFileName(filePath); + var dlg = new InputDialog("이름 변경", "새 파일 이름:", oldName) { Owner = this }; + if (dlg.ShowDialog() == true && !string.IsNullOrWhiteSpace(dlg.ResponseText)) + { + var newPath = Path.Combine(dir, dlg.ResponseText.Trim()); + try + { + File.Move(filePath, newPath); + BuildFileTree(); + ShowToast($"이름 변경: {dlg.ResponseText.Trim()}"); + } + catch (Exception ex) + { + ShowToast($"이름 변경 실패: {ex.Message}", "\uE783"); + } + } + }); + + AddItem("\uE74D", "삭제", () => + { + var result = CustomMessageBox.Show( + $"파일을 삭제하시겠습니까?\n{Path.GetFileName(filePath)}", + "파일 삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning); + if (result == MessageBoxResult.Yes) + { + try + { + File.Delete(filePath); + BuildFileTree(); + ShowToast("파일 삭제됨"); + } + catch (Exception ex) + { + ShowToast($"삭제 실패: {ex.Message}", "\uE783"); + } + } + }, dangerBrush, dangerBrush); + + Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, DispatcherPriority.Input); + } + + private void RefreshFileTreeIfVisible() + { + if (FileBrowserPanel.Visibility != Visibility.Visible) + return; + + _fileBrowserRefreshTimer?.Stop(); + _fileBrowserRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; + _fileBrowserRefreshTimer.Tick += (_, _) => + { + _fileBrowserRefreshTimer.Stop(); + BuildFileTree(); + }; + _fileBrowserRefreshTimer.Start(); + } +} diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs index 905c89d..9bafc11 100644 --- a/src/AxCopilot/Views/ChatWindow.xaml.cs +++ b/src/AxCopilot/Views/ChatWindow.xaml.cs @@ -15827,342 +15827,6 @@ public partial class ChatWindow : Window hideTimer.Start(); } - // ─── 파일 탐색기 ────────────────────────────────────────────────────── - - private static readonly HashSet _ignoredDirs = new(StringComparer.OrdinalIgnoreCase) - { - "bin", "obj", "node_modules", ".git", ".vs", ".idea", ".vscode", - "__pycache__", ".mypy_cache", ".pytest_cache", "dist", "build", - ".cache", ".next", ".nuxt", "coverage", ".terraform", - }; - - private DispatcherTimer? _fileBrowserRefreshTimer; - - private void ToggleFileBrowser() - { - if (FileBrowserPanel.Visibility == Visibility.Visible) - { - FileBrowserPanel.Visibility = Visibility.Collapsed; - _settings.Settings.Llm.ShowFileBrowser = false; - } - else - { - FileBrowserPanel.Visibility = Visibility.Visible; - _settings.Settings.Llm.ShowFileBrowser = true; - BuildFileTree(); - } - _settings.Save(); - } - - private void BtnFileBrowserRefresh_Click(object sender, RoutedEventArgs e) => BuildFileTree(); - - private void BtnFileBrowserOpenFolder_Click(object sender, RoutedEventArgs e) - { - var folder = GetCurrentWorkFolder(); - if (string.IsNullOrEmpty(folder) || !System.IO.Directory.Exists(folder)) return; - try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = folder, UseShellExecute = true }); } catch { } - } - - private void BtnFileBrowserClose_Click(object sender, RoutedEventArgs e) - { - FileBrowserPanel.Visibility = Visibility.Collapsed; - } - - private void BuildFileTree() - { - FileTreeView.Items.Clear(); - var folder = GetCurrentWorkFolder(); - if (string.IsNullOrEmpty(folder) || !System.IO.Directory.Exists(folder)) - { - FileTreeView.Items.Add(new TreeViewItem { Header = "작업 폴더를 선택하세요", IsEnabled = false }); - return; - } - - FileBrowserTitle.Text = $"파일 탐색기 — {System.IO.Path.GetFileName(folder)}"; - var count = 0; - PopulateDirectory(new System.IO.DirectoryInfo(folder), FileTreeView.Items, 0, ref count); - } - - private void PopulateDirectory(System.IO.DirectoryInfo dir, ItemCollection items, int depth, ref int count) - { - if (depth > 4 || count > 200) return; - - // 디렉터리 - try - { - foreach (var subDir in dir.GetDirectories().OrderBy(d => d.Name)) - { - if (count > 200) break; - if (_ignoredDirs.Contains(subDir.Name) || subDir.Name.StartsWith('.')) continue; - - count++; - var dirItem = new TreeViewItem - { - Header = CreateFileTreeHeader("\uED25", subDir.Name, null), - Tag = subDir.FullName, - IsExpanded = depth < 1, - }; - - // 지연 로딩: 더미 자식 → 펼칠 때 실제 로드 - if (depth < 3) - { - dirItem.Items.Add(new TreeViewItem { Header = "로딩 중..." }); // 더미 - var capturedDir = subDir; - var capturedDepth = depth; - dirItem.Expanded += (s, _) => - { - if (s is TreeViewItem ti && ti.Items.Count == 1 && ti.Items[0] is TreeViewItem d && d.Header?.ToString() == "로딩 중...") - { - ti.Items.Clear(); - int c = 0; - PopulateDirectory(capturedDir, ti.Items, capturedDepth + 1, ref c); - } - }; - } - else - { - PopulateDirectory(subDir, dirItem.Items, depth + 1, ref count); - } - - items.Add(dirItem); - } - } - catch { } - - // 파일 - try - { - foreach (var file in dir.GetFiles().OrderBy(f => f.Name)) - { - if (count > 200) break; - count++; - - var ext = file.Extension.ToLowerInvariant(); - var icon = GetFileIcon(ext); - var size = FormatFileSize(file.Length); - - var fileItem = new TreeViewItem - { - Header = CreateFileTreeHeader(icon, file.Name, size), - Tag = file.FullName, - }; - - // 더블클릭 → 프리뷰 - var capturedPath = file.FullName; - fileItem.MouseDoubleClick += (s, e) => - { - e.Handled = true; - TryShowPreview(capturedPath); - }; - - // 우클릭 → 컨텍스트 메뉴 (MouseRightButtonUp에서 열어야 Popup이 바로 닫히지 않음) - fileItem.MouseRightButtonUp += (s, e) => - { - e.Handled = true; - if (s is TreeViewItem ti) ti.IsSelected = true; - ShowFileTreeContextMenu(capturedPath); - }; - - items.Add(fileItem); - } - } - catch { } - } - - private static StackPanel CreateFileTreeHeader(string icon, string name, string? sizeText) - { - var sp = new StackPanel { Orientation = Orientation.Horizontal }; - sp.Children.Add(new TextBlock - { - Text = icon, - FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 11, - Foreground = new SolidColorBrush(Color.FromRgb(0x9C, 0xA3, 0xAF)), - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(0, 0, 5, 0), - }); - sp.Children.Add(new TextBlock - { - Text = name, - FontSize = 11.5, - VerticalAlignment = VerticalAlignment.Center, - }); - if (sizeText != null) - { - sp.Children.Add(new TextBlock - { - Text = $" {sizeText}", - FontSize = 10, - Foreground = new SolidColorBrush(Color.FromRgb(0x6B, 0x72, 0x80)), - VerticalAlignment = VerticalAlignment.Center, - }); - } - return sp; - } - - private static string GetFileIcon(string ext) => ext switch - { - ".html" or ".htm" => "\uEB41", - ".xlsx" or ".xls" => "\uE9F9", - ".docx" or ".doc" => "\uE8A5", - ".pdf" => "\uEA90", - ".csv" => "\uE80A", - ".md" => "\uE70B", - ".json" or ".xml" => "\uE943", - ".png" or ".jpg" or ".jpeg" or ".gif" or ".svg" or ".webp" => "\uEB9F", - ".cs" or ".py" or ".js" or ".ts" or ".java" or ".cpp" => "\uE943", - ".bat" or ".cmd" or ".ps1" or ".sh" => "\uE756", - ".txt" or ".log" => "\uE8A5", - _ => "\uE7C3", - }; - - private static string FormatFileSize(long bytes) => bytes switch - { - < 1024 => $"{bytes} B", - < 1024 * 1024 => $"{bytes / 1024.0:F1} KB", - _ => $"{bytes / (1024.0 * 1024.0):F1} MB", - }; - - private void ShowFileTreeContextMenu(string filePath) - { - 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("HintBackground") as Brush ?? new SolidColorBrush(Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); - var dangerBrush = new SolidColorBrush(Color.FromRgb(0xE7, 0x4C, 0x3C)); - - var popup = new Popup - { - StaysOpen = false, AllowsTransparency = true, PopupAnimation = PopupAnimation.Fade, - Placement = PlacementMode.MousePoint, - }; - var panel = new StackPanel { Margin = new Thickness(2) }; - var container = new Border - { - Background = bg, BorderBrush = borderBrush, BorderThickness = new Thickness(1), - CornerRadius = new CornerRadius(10), Padding = new Thickness(6), MinWidth = 200, - Effect = new System.Windows.Media.Effects.DropShadowEffect - { - BlurRadius = 16, ShadowDepth = 4, Opacity = 0.3, - Color = Colors.Black, Direction = 270, - }, - Child = panel, - }; - popup.Child = container; - - void AddItem(string icon, string label, Action action, Brush? labelColor = null, Brush? iconColor = null) - { - var sp = new StackPanel { Orientation = Orientation.Horizontal }; - sp.Children.Add(new TextBlock - { - Text = icon, FontFamily = new FontFamily("Segoe MDL2 Assets"), - FontSize = 13, Foreground = iconColor ?? secondaryText, - VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 10, 0), - }); - sp.Children.Add(new TextBlock - { - Text = label, FontSize = 12.5, Foreground = labelColor ?? primaryText, - VerticalAlignment = VerticalAlignment.Center, - }); - var item = new Border - { - Child = sp, Background = Brushes.Transparent, - CornerRadius = new CornerRadius(7), Cursor = Cursors.Hand, - Padding = new Thickness(10, 8, 14, 8), Margin = new Thickness(0, 1, 0, 1), - }; - item.MouseEnter += (s, _) => { if (s is Border b) b.Background = hoverBg; }; - item.MouseLeave += (s, _) => { if (s is Border b) b.Background = Brushes.Transparent; }; - item.MouseLeftButtonUp += (_, _) => { popup.IsOpen = false; action(); }; - panel.Children.Add(item); - } - - void AddSep() - { - panel.Children.Add(new Border - { - Height = 1, Margin = new Thickness(10, 4, 10, 4), - Background = borderBrush, Opacity = 0.3, - }); - } - - var ext = System.IO.Path.GetExtension(filePath).ToLowerInvariant(); - if (_previewableExtensions.Contains(ext)) - AddItem("\uE8A1", "미리보기", () => ShowPreviewPanel(filePath)); - - AddItem("\uE8A7", "외부 프로그램으로 열기", () => - { - try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = filePath, UseShellExecute = true }); } catch { } - }); - AddItem("\uED25", "폴더에서 보기", () => - { - try { System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{filePath}\""); } catch { } - }); - AddItem("\uE8C8", "경로 복사", () => - { - try { Clipboard.SetText(filePath); ShowToast("경로 복사됨"); } catch { } - }); - - AddSep(); - - // 이름 변경 - AddItem("\uE8AC", "이름 변경", () => - { - var dir = System.IO.Path.GetDirectoryName(filePath) ?? ""; - var oldName = System.IO.Path.GetFileName(filePath); - var dlg = new Views.InputDialog("이름 변경", "새 파일 이름:", oldName) { Owner = this }; - if (dlg.ShowDialog() == true && !string.IsNullOrWhiteSpace(dlg.ResponseText)) - { - var newPath = System.IO.Path.Combine(dir, dlg.ResponseText.Trim()); - try - { - System.IO.File.Move(filePath, newPath); - BuildFileTree(); - ShowToast($"이름 변경: {dlg.ResponseText.Trim()}"); - } - catch (Exception ex) { ShowToast($"이름 변경 실패: {ex.Message}", "\uE783"); } - } - }); - - // 삭제 - AddItem("\uE74D", "삭제", () => - { - var result = CustomMessageBox.Show( - $"파일을 삭제하시겠습니까?\n{System.IO.Path.GetFileName(filePath)}", - "파일 삭제 확인", MessageBoxButton.YesNo, MessageBoxImage.Warning); - if (result == MessageBoxResult.Yes) - { - try - { - System.IO.File.Delete(filePath); - BuildFileTree(); - ShowToast("파일 삭제됨"); - } - catch (Exception ex) { ShowToast($"삭제 실패: {ex.Message}", "\uE783"); } - } - }, dangerBrush, dangerBrush); - - // Dispatcher로 열어야 MouseRightButtonUp 후 바로 닫히지 않음 - Dispatcher.BeginInvoke(() => { popup.IsOpen = true; }, - System.Windows.Threading.DispatcherPriority.Input); - } - - /// 에이전트가 파일 생성 시 파일 탐색기를 자동 새로고침합니다. - private void RefreshFileTreeIfVisible() - { - if (FileBrowserPanel.Visibility != Visibility.Visible) return; - - // 디바운스: 500ms 내 중복 호출 방지 - _fileBrowserRefreshTimer?.Stop(); - _fileBrowserRefreshTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) }; - _fileBrowserRefreshTimer.Tick += (_, _) => - { - _fileBrowserRefreshTimer.Stop(); - BuildFileTree(); - }; - _fileBrowserRefreshTimer.Start(); - } - // ─── 하단 상태바 ────────────────────────────────────────────────────── private void BtnCompactNow_Click(object sender, RoutedEventArgs e)