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 = CreateSurfaceFileTreeHeader("\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 = CreateSurfaceFileTreeHeader(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 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 primaryText = TryFindResource("PrimaryText") as Brush ?? Brushes.White; var secondaryText = TryFindResource("SecondaryText") as Brush ?? Brushes.Gray; 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 = CreateSurfacePopupContainer(panel, 200, new Thickness(6)); popup.Child = container; void AddItem(string icon, string label, Action action, Brush? labelColor = null, Brush? iconColor = null) { var item = CreateSurfacePopupMenuItem(icon, iconColor ?? secondaryText, label, () => { popup.IsOpen = false; action(); }, labelColor ?? primaryText); panel.Children.Add(item); } void AddSep() { panel.Children.Add(CreateSurfacePopupSeparator()); } 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(); } }