Some checks failed
Release Gate / gate (push) Has been cancelled
- 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 확인
322 lines
10 KiB
C#
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();
|
|
}
|
|
}
|