- ChatWindow.FileBrowserPresentation.cs를 추가해 파일 탐색기 열기/닫기, 폴더 트리 구성, 파일 헤더·아이콘·크기 표시, 우클릭 메뉴, 디바운스 새로고침 흐름을 메인 창 코드에서 분리함 - ChatWindow.xaml.cs는 transcript·runtime orchestration 중심으로 더 정리해 claw-code 기준 sidebar/file surface 품질 개선을 이어가기 쉬운 구조로 개선함 - README.md와 docs/DEVELOPMENT.md에 2026-04-06 08:55 (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:
407
src/AxCopilot/Views/ChatWindow.FileBrowserPresentation.cs
Normal file
407
src/AxCopilot/Views/ChatWindow.FileBrowserPresentation.cs
Normal file
@@ -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<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 = 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user