Files
AX-Copilot-Codex/src/AxCopilot/Views/ChatWindow.FileBrowserPresentation.cs
lacvet 3c3faab528
Some checks failed
Release Gate / gate (push) Has been cancelled
AX Agent surface visual language 공통화 및 preview/file browser 정리
- ChatWindow에 공통 popup container/menu item/separator/file tree header helper를 추가
- Preview와 FileBrowser presentation이 같은 surface 스타일을 사용하도록 정리
- README와 DEVELOPMENT 문서에 2026-04-06 11:20 (KST) 기준 visual polish 1차 반영
- dotnet build 검증 경고 0, 오류 0 확인
2026-04-06 12:09:18 +09:00

322 lines
10 KiB
C#

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<string> _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();
}
}