[Phase 44] LauncherViewModel·SettingsWindow.Tools·MarkdownRenderer 파셜 분할
LauncherViewModel (805줄 → 402줄, 50% 감소): - LauncherViewModel.FileAction.cs (154줄): 파일 액션 서브메뉴, EnterActionMode, ExitActionMode - LauncherViewModel.Commands.cs (273줄): CopySelectedPath, Favorite, Terminal, 클립보드 병합, INotifyPropertyChanged - 오류 수정: FileAction.cs에 using AxCopilot.Themes 누락 → 추가 SettingsWindow.Tools (875줄 → 605줄): - SettingsWindow.ToolCards.cs (283줄): AX Agent 서브탭 전환 + 도구 관리 카드 UI MarkdownRenderer (825줄 → 621줄): - MarkdownRenderer.Highlighting.cs (215줄): 구문 하이라이팅 전체 분리 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
273
src/AxCopilot/ViewModels/LauncherViewModel.Commands.cs
Normal file
273
src/AxCopilot/ViewModels/LauncherViewModel.Commands.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
|
||||
namespace AxCopilot.ViewModels;
|
||||
|
||||
public partial class LauncherViewModel
|
||||
{
|
||||
// ─── 단축키 지원 메서드 ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>선택된 항목의 경로를 클립보드에 복사</summary>
|
||||
public bool CopySelectedPath()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Path.GetFileName(Environment.ExpandEnvironmentVariables(entry.Path));
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목의 전체 경로를 클립보드에 복사</summary>
|
||||
public bool CopySelectedFullPath()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목을 탐색기에서 열기</summary>
|
||||
public bool OpenSelectedInExplorer()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
if (File.Exists(path))
|
||||
Process.Start("explorer.exe", $"/select,\"{path}\"");
|
||||
else if (Directory.Exists(path))
|
||||
Process.Start("explorer.exe", $"\"{path}\"");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목을 관리자 권한으로 실행</summary>
|
||||
public bool RunSelectedAsAdmin()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path)
|
||||
{ UseShellExecute = true, Verb = "runas" });
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) { LogService.Warn($"관리자 실행 취소: {ex.Message}"); }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목의 속성 창 열기</summary>
|
||||
public bool ShowSelectedProperties()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path)
|
||||
{ UseShellExecute = true, Verb = "properties" });
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// properties verb 미지원 시 탐색기에서 선택
|
||||
Process.Start("explorer.exe", $"/select,\"{path}\"");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>최근 기록에서 항목 삭제 (Delete 키용)</summary>
|
||||
public bool RemoveSelectedFromRecent()
|
||||
{
|
||||
if (SelectedItem == null || Results.Count == 0) return false;
|
||||
|
||||
var idx = Results.IndexOf(SelectedItem);
|
||||
Results.Remove(SelectedItem);
|
||||
|
||||
if (Results.Count > 0)
|
||||
SelectedItem = Results[Math.Min(idx, Results.Count - 1)];
|
||||
else
|
||||
SelectedItem = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>입력창 초기화</summary>
|
||||
public void ClearInput()
|
||||
{
|
||||
InputText = "";
|
||||
}
|
||||
|
||||
/// <summary>첫 번째 결과 항목 선택</summary>
|
||||
public void SelectFirst()
|
||||
{
|
||||
if (Results.Count > 0) SelectedItem = Results[0];
|
||||
}
|
||||
|
||||
/// <summary>마지막 결과 항목 선택</summary>
|
||||
public void SelectLast()
|
||||
{
|
||||
if (Results.Count > 0) SelectedItem = Results[^1];
|
||||
}
|
||||
|
||||
/// <summary>현재 선택된 파일/폴더 항목을 즐겨찾기에 추가하거나 제거합니다.</summary>
|
||||
/// <returns>(추가됐으면 true, 제거됐으면 false, 대상 없으면 null)</returns>
|
||||
public bool? ToggleFavorite()
|
||||
{
|
||||
if (SelectedItem?.Data is not IndexEntry entry) return null;
|
||||
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
if (string.IsNullOrWhiteSpace(name)) name = Path.GetFileName(path);
|
||||
|
||||
var favFile = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AxCopilot", "favorites.json");
|
||||
|
||||
try
|
||||
{
|
||||
var opts = new System.Text.Json.JsonSerializerOptions
|
||||
{ WriteIndented = true, PropertyNameCaseInsensitive = true };
|
||||
|
||||
List<FavJson> list = new();
|
||||
if (File.Exists(favFile))
|
||||
list = System.Text.Json.JsonSerializer.Deserialize<List<FavJson>>(
|
||||
File.ReadAllText(favFile), opts) ?? new();
|
||||
|
||||
var existing = list.FirstOrDefault(f =>
|
||||
f.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
list.Remove(existing);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(favFile)!);
|
||||
File.WriteAllText(favFile, System.Text.Json.JsonSerializer.Serialize(list, opts));
|
||||
return false; // 제거됨
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Insert(0, new FavJson { Name = name, Path = path });
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(favFile)!);
|
||||
File.WriteAllText(favFile, System.Text.Json.JsonSerializer.Serialize(list, opts));
|
||||
return true; // 추가됨
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"즐겨찾기 토글 실패: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>선택 항목의 디렉터리에서 터미널을 열기.</summary>
|
||||
/// <returns>성공 여부</returns>
|
||||
public bool OpenSelectedInTerminal()
|
||||
{
|
||||
string dir;
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
dir = Directory.Exists(path) ? path : Path.GetDirectoryName(path) ?? path;
|
||||
}
|
||||
else
|
||||
{
|
||||
dir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
}
|
||||
|
||||
try { Process.Start("wt.exe", $"-d \"{dir}\""); return true; }
|
||||
catch (Exception)
|
||||
{
|
||||
try { Process.Start("cmd.exe", $"/k cd /d \"{dir}\""); return true; }
|
||||
catch (Exception ex) { LogService.Warn($"터미널 열기 실패: {ex.Message}"); return false; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>다운로드 폴더를 cd 프리픽스로 탐색합니다.</summary>
|
||||
public void NavigateToDownloads()
|
||||
{
|
||||
var downloads = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
|
||||
InputText = $"cd {downloads}";
|
||||
}
|
||||
|
||||
// 즐겨찾기 직렬화용 내부 레코드
|
||||
private sealed class FavJson
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
[System.Text.Json.Serialization.JsonPropertyName("path")]
|
||||
public string Path { get; set; } = "";
|
||||
}
|
||||
|
||||
// ─── 클립보드 병합 ────────────────────────────────────────────────────────
|
||||
|
||||
public void ToggleMergeItem(LauncherItem? item)
|
||||
{
|
||||
if (item?.Data is not ClipboardEntry entry || !entry.IsText) return;
|
||||
|
||||
if (!_mergeQueue.Remove(entry))
|
||||
_mergeQueue.Add(entry);
|
||||
|
||||
OnPropertyChanged(nameof(MergeCount));
|
||||
OnPropertyChanged(nameof(ShowMergeHint));
|
||||
OnPropertyChanged(nameof(MergeHintText));
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목들을 줄바꿈으로 합쳐 클립보드에 복사</summary>
|
||||
public void ExecuteMerge()
|
||||
{
|
||||
if (_mergeQueue.Count == 0) return;
|
||||
|
||||
// 선택 순서 보존: Results에서 보이는 순서 기준
|
||||
var ordered = Results
|
||||
.Where(r => r.Data is ClipboardEntry e && _mergeQueue.Contains(e))
|
||||
.Select(r => ((ClipboardEntry)r.Data!).Text)
|
||||
.ToList();
|
||||
|
||||
if (ordered.Count == 0)
|
||||
ordered = _mergeQueue.Select(e => e.Text).ToList();
|
||||
|
||||
var merged = string.Join("\n", ordered);
|
||||
try { Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(merged)); }
|
||||
catch (Exception ex) { LogService.Warn($"병합 클립보드 실패: {ex.Message}"); }
|
||||
|
||||
ClearMerge();
|
||||
CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||
LogService.Info($"클립보드 병합: {ordered.Count}개 항목");
|
||||
}
|
||||
|
||||
public void ClearMerge()
|
||||
{
|
||||
_mergeQueue.Clear();
|
||||
OnPropertyChanged(nameof(MergeCount));
|
||||
OnPropertyChanged(nameof(ShowMergeHint));
|
||||
OnPropertyChanged(nameof(MergeHintText));
|
||||
}
|
||||
|
||||
// ─── INotifyPropertyChanged ───────────────────────────────────────────────
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
// ─── 파일 액션 데이터 타입 ────────────────────────────────────────────────────
|
||||
|
||||
public enum FileAction { CopyPath, CopyFullPath, OpenExplorer, RunAsAdmin, OpenTerminal, ShowProperties, Rename, DeleteToRecycleBin }
|
||||
|
||||
public record FileActionData(string Path, FileAction Action);
|
||||
155
src/AxCopilot/ViewModels/LauncherViewModel.FileAction.cs
Normal file
155
src/AxCopilot/ViewModels/LauncherViewModel.FileAction.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using AxCopilot.Models;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.ViewModels;
|
||||
|
||||
public partial class LauncherViewModel
|
||||
{
|
||||
// ─── 파일 액션 서브메뉴 ───────────────────────────────────────────────────
|
||||
|
||||
public void EnterActionMode(LauncherItem item)
|
||||
{
|
||||
if (!_settings.Settings.Launcher.EnableActionMode) return;
|
||||
if (item.Data is not IndexEntry entry) return;
|
||||
|
||||
_actionSourceItem = item;
|
||||
_savedQuery = _inputText;
|
||||
IsActionMode = true;
|
||||
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
var isDir = Directory.Exists(path);
|
||||
var name = Path.GetFileName(path);
|
||||
|
||||
Results.Clear();
|
||||
Results.Add(MakeAction("경로 복사",
|
||||
path, FileAction.CopyPath, Symbols.Clipboard, "#8764B8"));
|
||||
|
||||
Results.Add(MakeAction("전체 경로 복사",
|
||||
path, FileAction.CopyFullPath, Symbols.Clipboard, "#C55A11"));
|
||||
|
||||
Results.Add(MakeAction("파일 탐색기에서 열기",
|
||||
"Explorer에서 위치 선택됨으로 표시", FileAction.OpenExplorer, Symbols.Folder, "#107C10"));
|
||||
|
||||
if (!isDir)
|
||||
Results.Add(MakeAction("관리자 권한으로 실행",
|
||||
"UAC 권한 상승 후 실행", FileAction.RunAsAdmin, Symbols.Lock, "#C50F1F"));
|
||||
|
||||
Results.Add(MakeAction("터미널에서 열기",
|
||||
isDir ? path : Path.GetDirectoryName(path) ?? path,
|
||||
FileAction.OpenTerminal, Symbols.Terminal, "#323130"));
|
||||
|
||||
if (!isDir)
|
||||
Results.Add(MakeAction("파일 속성 보기",
|
||||
"Windows 속성 대화 상자 열기", FileAction.ShowProperties, Symbols.Info, "#6B2C91"));
|
||||
|
||||
Results.Add(MakeAction("이름 바꾸기",
|
||||
name, FileAction.Rename, Symbols.Rename, "#D97706"));
|
||||
|
||||
Results.Add(MakeAction("휴지통으로 삭제",
|
||||
"복구 가능한 삭제 · 확인 후 실행", FileAction.DeleteToRecycleBin, Symbols.Delete, "#C50F1F"));
|
||||
|
||||
SelectedItem = Results.FirstOrDefault();
|
||||
|
||||
static LauncherItem MakeAction(string title, string subtitle,
|
||||
FileAction action, string symbol, string colorHex)
|
||||
{
|
||||
var data = new FileActionData(subtitle, action);
|
||||
return new LauncherItem(title, subtitle, null, data, Symbol: symbol);
|
||||
}
|
||||
}
|
||||
|
||||
public void ExitActionMode()
|
||||
{
|
||||
IsActionMode = false;
|
||||
_actionSourceItem = null;
|
||||
// 이전 검색 쿼리 복원
|
||||
var q = _savedQuery;
|
||||
_savedQuery = "";
|
||||
_ = SearchAsync(q);
|
||||
}
|
||||
|
||||
private static void ExecuteFileAction(FileActionData data)
|
||||
{
|
||||
var path = data.Path;
|
||||
switch (data.Action)
|
||||
{
|
||||
case FileAction.CopyPath:
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path));
|
||||
break;
|
||||
|
||||
case FileAction.OpenExplorer:
|
||||
if (File.Exists(path))
|
||||
Process.Start("explorer.exe", $"/select,\"{path}\"");
|
||||
else
|
||||
Process.Start("explorer.exe", $"\"{path}\"");
|
||||
break;
|
||||
|
||||
case FileAction.RunAsAdmin:
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path)
|
||||
{ UseShellExecute = true, Verb = "runas" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"관리자 실행 취소: {ex.Message}");
|
||||
}
|
||||
break;
|
||||
|
||||
case FileAction.CopyFullPath:
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path));
|
||||
break;
|
||||
|
||||
case FileAction.ShowProperties:
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("explorer.exe")
|
||||
{
|
||||
Arguments = $"/select,\"{path}\"",
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(psi);
|
||||
// Shell property dialog
|
||||
var propInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "rundll32.exe",
|
||||
Arguments = $"shell32.dll,ShellExec_RunDLL \"properties\" \"{path}\"",
|
||||
UseShellExecute = false
|
||||
};
|
||||
// 대안: Shell verb "properties"
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path)
|
||||
{ UseShellExecute = true, Verb = "properties" });
|
||||
}
|
||||
catch (Exception) { /* 일부 파일 형식에서 지원 안됨 */ }
|
||||
}
|
||||
catch (Exception ex) { LogService.Warn($"속성 열기 실패: {ex.Message}"); }
|
||||
break;
|
||||
|
||||
case FileAction.Rename:
|
||||
// 런처에서 rename 핸들러로 전달
|
||||
// ExitActionMode 후 InputText가 rename 프리픽스로 설정됨
|
||||
break;
|
||||
|
||||
case FileAction.DeleteToRecycleBin:
|
||||
// LauncherWindow.xaml.cs의 ExecuteSelected에서 확인 다이얼로그 처리
|
||||
break;
|
||||
|
||||
case FileAction.OpenTerminal:
|
||||
var dir = File.Exists(path) ? Path.GetDirectoryName(path) ?? path : path;
|
||||
try { Process.Start("wt.exe", $"-d \"{dir}\""); }
|
||||
catch (Exception)
|
||||
{
|
||||
try { Process.Start("cmd.exe", $"/k cd /d \"{dir}\""); }
|
||||
catch (Exception ex) { LogService.Warn($"터미널 열기 실패: {ex.Message}"); }
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.ViewModels;
|
||||
|
||||
public class LauncherViewModel : INotifyPropertyChanged
|
||||
public partial class LauncherViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private static App? CurrentApp => System.Windows.Application.Current as App;
|
||||
|
||||
@@ -399,407 +399,4 @@ public class LauncherViewModel : INotifyPropertyChanged
|
||||
// 기본: 제목
|
||||
return SelectedItem.Title;
|
||||
}
|
||||
|
||||
// ─── 파일 액션 서브메뉴 ───────────────────────────────────────────────────
|
||||
|
||||
public void EnterActionMode(LauncherItem item)
|
||||
{
|
||||
if (!_settings.Settings.Launcher.EnableActionMode) return;
|
||||
if (item.Data is not IndexEntry entry) return;
|
||||
|
||||
_actionSourceItem = item;
|
||||
_savedQuery = _inputText;
|
||||
IsActionMode = true;
|
||||
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
var isDir = Directory.Exists(path);
|
||||
var name = Path.GetFileName(path);
|
||||
|
||||
Results.Clear();
|
||||
Results.Add(MakeAction("경로 복사",
|
||||
path, FileAction.CopyPath, Symbols.Clipboard, "#8764B8"));
|
||||
|
||||
Results.Add(MakeAction("전체 경로 복사",
|
||||
path, FileAction.CopyFullPath, Symbols.Clipboard, "#C55A11"));
|
||||
|
||||
Results.Add(MakeAction("파일 탐색기에서 열기",
|
||||
"Explorer에서 위치 선택됨으로 표시", FileAction.OpenExplorer, Symbols.Folder, "#107C10"));
|
||||
|
||||
if (!isDir)
|
||||
Results.Add(MakeAction("관리자 권한으로 실행",
|
||||
"UAC 권한 상승 후 실행", FileAction.RunAsAdmin, Symbols.Lock, "#C50F1F"));
|
||||
|
||||
Results.Add(MakeAction("터미널에서 열기",
|
||||
isDir ? path : Path.GetDirectoryName(path) ?? path,
|
||||
FileAction.OpenTerminal, Symbols.Terminal, "#323130"));
|
||||
|
||||
if (!isDir)
|
||||
Results.Add(MakeAction("파일 속성 보기",
|
||||
"Windows 속성 대화 상자 열기", FileAction.ShowProperties, Symbols.Info, "#6B2C91"));
|
||||
|
||||
Results.Add(MakeAction("이름 바꾸기",
|
||||
name, FileAction.Rename, Symbols.Rename, "#D97706"));
|
||||
|
||||
Results.Add(MakeAction("휴지통으로 삭제",
|
||||
"복구 가능한 삭제 · 확인 후 실행", FileAction.DeleteToRecycleBin, Symbols.Delete, "#C50F1F"));
|
||||
|
||||
SelectedItem = Results.FirstOrDefault();
|
||||
|
||||
static LauncherItem MakeAction(string title, string subtitle,
|
||||
FileAction action, string symbol, string colorHex)
|
||||
{
|
||||
var data = new FileActionData(subtitle, action);
|
||||
return new LauncherItem(title, subtitle, null, data, Symbol: symbol);
|
||||
}
|
||||
}
|
||||
|
||||
public void ExitActionMode()
|
||||
{
|
||||
IsActionMode = false;
|
||||
_actionSourceItem = null;
|
||||
// 이전 검색 쿼리 복원
|
||||
var q = _savedQuery;
|
||||
_savedQuery = "";
|
||||
_ = SearchAsync(q);
|
||||
}
|
||||
|
||||
private static void ExecuteFileAction(FileActionData data)
|
||||
{
|
||||
var path = data.Path;
|
||||
switch (data.Action)
|
||||
{
|
||||
case FileAction.CopyPath:
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path));
|
||||
break;
|
||||
|
||||
case FileAction.OpenExplorer:
|
||||
if (File.Exists(path))
|
||||
Process.Start("explorer.exe", $"/select,\"{path}\"");
|
||||
else
|
||||
Process.Start("explorer.exe", $"\"{path}\"");
|
||||
break;
|
||||
|
||||
case FileAction.RunAsAdmin:
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path)
|
||||
{ UseShellExecute = true, Verb = "runas" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"관리자 실행 취소: {ex.Message}");
|
||||
}
|
||||
break;
|
||||
|
||||
case FileAction.CopyFullPath:
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path));
|
||||
break;
|
||||
|
||||
case FileAction.ShowProperties:
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("explorer.exe")
|
||||
{
|
||||
Arguments = $"/select,\"{path}\"",
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(psi);
|
||||
// Shell property dialog
|
||||
var propInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "rundll32.exe",
|
||||
Arguments = $"shell32.dll,ShellExec_RunDLL \"properties\" \"{path}\"",
|
||||
UseShellExecute = false
|
||||
};
|
||||
// 대안: Shell verb "properties"
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path)
|
||||
{ UseShellExecute = true, Verb = "properties" });
|
||||
}
|
||||
catch (Exception) { /* 일부 파일 형식에서 지원 안됨 */ }
|
||||
}
|
||||
catch (Exception ex) { LogService.Warn($"속성 열기 실패: {ex.Message}"); }
|
||||
break;
|
||||
|
||||
case FileAction.Rename:
|
||||
// 런처에서 rename 핸들러로 전달
|
||||
// ExitActionMode 후 InputText가 rename 프리픽스로 설정됨
|
||||
break;
|
||||
|
||||
case FileAction.DeleteToRecycleBin:
|
||||
// LauncherWindow.xaml.cs의 ExecuteSelected에서 확인 다이얼로그 처리
|
||||
break;
|
||||
|
||||
case FileAction.OpenTerminal:
|
||||
var dir = File.Exists(path) ? Path.GetDirectoryName(path) ?? path : path;
|
||||
try { Process.Start("wt.exe", $"-d \"{dir}\""); }
|
||||
catch (Exception)
|
||||
{
|
||||
try { Process.Start("cmd.exe", $"/k cd /d \"{dir}\""); }
|
||||
catch (Exception ex) { LogService.Warn($"터미널 열기 실패: {ex.Message}"); }
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 단축키 지원 메서드 ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>선택된 항목의 경로를 클립보드에 복사</summary>
|
||||
public bool CopySelectedPath()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Path.GetFileName(Environment.ExpandEnvironmentVariables(entry.Path));
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목의 전체 경로를 클립보드에 복사</summary>
|
||||
public bool CopySelectedFullPath()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(path));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목을 탐색기에서 열기</summary>
|
||||
public bool OpenSelectedInExplorer()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
if (File.Exists(path))
|
||||
Process.Start("explorer.exe", $"/select,\"{path}\"");
|
||||
else if (Directory.Exists(path))
|
||||
Process.Start("explorer.exe", $"\"{path}\"");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목을 관리자 권한으로 실행</summary>
|
||||
public bool RunSelectedAsAdmin()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path)
|
||||
{ UseShellExecute = true, Verb = "runas" });
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) { LogService.Warn($"관리자 실행 취소: {ex.Message}"); }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목의 속성 창 열기</summary>
|
||||
public bool ShowSelectedProperties()
|
||||
{
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path)
|
||||
{ UseShellExecute = true, Verb = "properties" });
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// properties verb 미지원 시 탐색기에서 선택
|
||||
Process.Start("explorer.exe", $"/select,\"{path}\"");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>최근 기록에서 항목 삭제 (Delete 키용)</summary>
|
||||
public bool RemoveSelectedFromRecent()
|
||||
{
|
||||
if (SelectedItem == null || Results.Count == 0) return false;
|
||||
|
||||
var idx = Results.IndexOf(SelectedItem);
|
||||
Results.Remove(SelectedItem);
|
||||
|
||||
if (Results.Count > 0)
|
||||
SelectedItem = Results[Math.Min(idx, Results.Count - 1)];
|
||||
else
|
||||
SelectedItem = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>입력창 초기화</summary>
|
||||
public void ClearInput()
|
||||
{
|
||||
InputText = "";
|
||||
}
|
||||
|
||||
/// <summary>첫 번째 결과 항목 선택</summary>
|
||||
public void SelectFirst()
|
||||
{
|
||||
if (Results.Count > 0) SelectedItem = Results[0];
|
||||
}
|
||||
|
||||
/// <summary>마지막 결과 항목 선택</summary>
|
||||
public void SelectLast()
|
||||
{
|
||||
if (Results.Count > 0) SelectedItem = Results[^1];
|
||||
}
|
||||
|
||||
/// <summary>현재 선택된 파일/폴더 항목을 즐겨찾기에 추가하거나 제거합니다.</summary>
|
||||
/// <returns>(추가됐으면 true, 제거됐으면 false, 대상 없으면 null)</returns>
|
||||
public bool? ToggleFavorite()
|
||||
{
|
||||
if (SelectedItem?.Data is not IndexEntry entry) return null;
|
||||
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
var name = Path.GetFileNameWithoutExtension(path);
|
||||
if (string.IsNullOrWhiteSpace(name)) name = Path.GetFileName(path);
|
||||
|
||||
var favFile = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AxCopilot", "favorites.json");
|
||||
|
||||
try
|
||||
{
|
||||
var opts = new System.Text.Json.JsonSerializerOptions
|
||||
{ WriteIndented = true, PropertyNameCaseInsensitive = true };
|
||||
|
||||
List<FavJson> list = new();
|
||||
if (File.Exists(favFile))
|
||||
list = System.Text.Json.JsonSerializer.Deserialize<List<FavJson>>(
|
||||
File.ReadAllText(favFile), opts) ?? new();
|
||||
|
||||
var existing = list.FirstOrDefault(f =>
|
||||
f.Path.Equals(path, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
list.Remove(existing);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(favFile)!);
|
||||
File.WriteAllText(favFile, System.Text.Json.JsonSerializer.Serialize(list, opts));
|
||||
return false; // 제거됨
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Insert(0, new FavJson { Name = name, Path = path });
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(favFile)!);
|
||||
File.WriteAllText(favFile, System.Text.Json.JsonSerializer.Serialize(list, opts));
|
||||
return true; // 추가됨
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"즐겨찾기 토글 실패: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>선택 항목의 디렉터리에서 터미널을 열기.</summary>
|
||||
/// <returns>성공 여부</returns>
|
||||
public bool OpenSelectedInTerminal()
|
||||
{
|
||||
string dir;
|
||||
if (SelectedItem?.Data is IndexEntry entry)
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(entry.Path);
|
||||
dir = Directory.Exists(path) ? path : Path.GetDirectoryName(path) ?? path;
|
||||
}
|
||||
else
|
||||
{
|
||||
dir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
}
|
||||
|
||||
try { Process.Start("wt.exe", $"-d \"{dir}\""); return true; }
|
||||
catch (Exception)
|
||||
{
|
||||
try { Process.Start("cmd.exe", $"/k cd /d \"{dir}\""); return true; }
|
||||
catch (Exception ex) { LogService.Warn($"터미널 열기 실패: {ex.Message}"); return false; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>다운로드 폴더를 cd 프리픽스로 탐색합니다.</summary>
|
||||
public void NavigateToDownloads()
|
||||
{
|
||||
var downloads = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
|
||||
InputText = $"cd {downloads}";
|
||||
}
|
||||
|
||||
// 즐겨찾기 직렬화용 내부 레코드
|
||||
private sealed class FavJson
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
[System.Text.Json.Serialization.JsonPropertyName("path")]
|
||||
public string Path { get; set; } = "";
|
||||
}
|
||||
|
||||
// ─── 클립보드 병합 ────────────────────────────────────────────────────────
|
||||
|
||||
public void ToggleMergeItem(LauncherItem? item)
|
||||
{
|
||||
if (item?.Data is not ClipboardEntry entry || !entry.IsText) return;
|
||||
|
||||
if (!_mergeQueue.Remove(entry))
|
||||
_mergeQueue.Add(entry);
|
||||
|
||||
OnPropertyChanged(nameof(MergeCount));
|
||||
OnPropertyChanged(nameof(ShowMergeHint));
|
||||
OnPropertyChanged(nameof(MergeHintText));
|
||||
}
|
||||
|
||||
/// <summary>선택된 항목들을 줄바꿈으로 합쳐 클립보드에 복사</summary>
|
||||
public void ExecuteMerge()
|
||||
{
|
||||
if (_mergeQueue.Count == 0) return;
|
||||
|
||||
// 선택 순서 보존: Results에서 보이는 순서 기준
|
||||
var ordered = Results
|
||||
.Where(r => r.Data is ClipboardEntry e && _mergeQueue.Contains(e))
|
||||
.Select(r => ((ClipboardEntry)r.Data!).Text)
|
||||
.ToList();
|
||||
|
||||
if (ordered.Count == 0)
|
||||
ordered = _mergeQueue.Select(e => e.Text).ToList();
|
||||
|
||||
var merged = string.Join("\n", ordered);
|
||||
try { Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(merged)); }
|
||||
catch (Exception ex) { LogService.Warn($"병합 클립보드 실패: {ex.Message}"); }
|
||||
|
||||
ClearMerge();
|
||||
CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||
LogService.Info($"클립보드 병합: {ordered.Count}개 항목");
|
||||
}
|
||||
|
||||
public void ClearMerge()
|
||||
{
|
||||
_mergeQueue.Clear();
|
||||
OnPropertyChanged(nameof(MergeCount));
|
||||
OnPropertyChanged(nameof(ShowMergeHint));
|
||||
OnPropertyChanged(nameof(MergeHintText));
|
||||
}
|
||||
|
||||
// ─── INotifyPropertyChanged ───────────────────────────────────────────────
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
// ─── 파일 액션 데이터 타입 ────────────────────────────────────────────────────
|
||||
|
||||
public enum FileAction { CopyPath, CopyFullPath, OpenExplorer, RunAsAdmin, OpenTerminal, ShowProperties, Rename, DeleteToRecycleBin }
|
||||
|
||||
public record FileActionData(string Path, FileAction Action);
|
||||
|
||||
Reference in New Issue
Block a user