diff --git a/src/AxCopilot/Models/QuickActionChip.cs b/src/AxCopilot/Models/QuickActionChip.cs new file mode 100644 index 0000000..0c3cac9 --- /dev/null +++ b/src/AxCopilot/Models/QuickActionChip.cs @@ -0,0 +1,14 @@ +using System.Windows.Media; + +namespace AxCopilot.Models; + +/// +/// 퀵 액션 바에 표시되는 최근 실행 항목 칩 모델. +/// 입력창 아래에 가로 칩 형태로 표시되며 클릭 시 즉시 실행됩니다. +/// +public record QuickActionChip( + string Title, + string Symbol, + string Path, + Brush Background +); diff --git a/src/AxCopilot/Services/UsageRankingService.cs b/src/AxCopilot/Services/UsageRankingService.cs index 5bdaffe..3965d3f 100644 --- a/src/AxCopilot/Services/UsageRankingService.cs +++ b/src/AxCopilot/Services/UsageRankingService.cs @@ -46,6 +46,21 @@ internal static class UsageRankingService } } + /// + /// 실행 횟수 기준 상위 N개의 (경로, 횟수) 목록을 반환합니다. + /// 퀵 액션 바 표시에 활용됩니다. + /// + public static IReadOnlyList<(string Path, int Count)> GetTopItems(int n) + { + EnsureLoaded(); + lock (_lock) + return _counts + .OrderByDescending(kv => kv.Value) + .Take(n) + .Select(kv => (kv.Key, kv.Value)) + .ToList(); + } + /// /// 실행 횟수 기준으로 내림차순 정렬하는 컴파러를 반환합니다. /// 동점이면 원래 순서 유지 (stable sort). diff --git a/src/AxCopilot/ViewModels/LauncherViewModel.cs b/src/AxCopilot/ViewModels/LauncherViewModel.cs index 553f3f8..42e5e48 100644 --- a/src/AxCopilot/ViewModels/LauncherViewModel.cs +++ b/src/AxCopilot/ViewModels/LauncherViewModel.cs @@ -48,6 +48,15 @@ public partial class LauncherViewModel : INotifyPropertyChanged /// public ICollectionView GroupedResults { get; } + /// + /// 퀵 액션 바 — 가장 많이 실행한 항목을 입력창 아래 칩으로 표시합니다. + /// 입력창이 비어 있을 때만 표시됩니다. + /// + public ObservableCollection QuickActionItems { get; } = new(); + + /// 퀵 액션 칩 표시 조건: 입력이 없고 칩이 하나 이상 있을 때 + public bool ShowQuickActions => string.IsNullOrEmpty(_inputText) && QuickActionItems.Count > 0; + // ─── 기본 프로퍼티 ──────────────────────────────────────────────────────── public string InputText @@ -66,6 +75,8 @@ public partial class LauncherViewModel : INotifyPropertyChanged OnPropertyChanged(nameof(ShowMergeHint)); OnPropertyChanged(nameof(MergeHintText)); + OnPropertyChanged(nameof(ShowQuickActions)); + // 연속 입력 시 이전 검색 즉시 취소 + 50ms 디바운스 후 실제 검색 시작 _searchCts?.Cancel(); _debounceTimer?.Dispose(); @@ -266,6 +277,47 @@ public partial class LauncherViewModel : INotifyPropertyChanged Results.Clear(); _lastSearchQuery = ""; ClearMerge(); + LoadQuickActions(); + } + + /// + /// UsageRankingService 상위 항목에서 퀵 액션 칩을 생성합니다. + /// 실제로 존재하는 파일/폴더만 표시하며 최대 8개로 제한합니다. + /// + public void LoadQuickActions() + { + QuickActionItems.Clear(); + var topItems = UsageRankingService.GetTopItems(16); // 여유분 확보 (일부 경로가 없을 수 있음) + var added = 0; + foreach (var (path, _) in topItems) + { + if (added >= 8) break; + var expanded = Environment.ExpandEnvironmentVariables(path); + var isFolder = Directory.Exists(expanded); + var isFile = !isFolder && File.Exists(expanded); + if (!isFolder && !isFile) continue; // 삭제된 항목 건너뜀 + + var ext = Path.GetExtension(expanded).ToLowerInvariant(); + var title = Path.GetFileNameWithoutExtension(expanded); + if (string.IsNullOrEmpty(title)) title = Path.GetFileName(expanded); + + var symbol = isFolder ? Symbols.Folder + : ext == ".exe" ? Symbols.App + : ext is ".lnk" + or ".url" ? Symbols.App + : Symbols.File; + + var color = isFolder ? Color.FromRgb(0x10, 0x7C, 0x10) + : ext == ".exe" ? Color.FromRgb(0x4B, 0x5E, 0xFC) + : ext is ".lnk" + or ".url" ? Color.FromRgb(0x4B, 0x5E, 0xFC) + : Color.FromRgb(0x5B, 0x4E, 0x7E); + + var bg = new SolidColorBrush(Color.FromArgb(0x26, color.R, color.G, color.B)); + QuickActionItems.Add(new QuickActionChip(title, symbol, path, bg)); + added++; + } + OnPropertyChanged(nameof(ShowQuickActions)); } // ─── 검색 ──────────────────────────────────────────────────────────────── diff --git a/src/AxCopilot/Views/LauncherWindow.Shell.cs b/src/AxCopilot/Views/LauncherWindow.Shell.cs index 08dbc80..358f63d 100644 --- a/src/AxCopilot/Views/LauncherWindow.Shell.cs +++ b/src/AxCopilot/Views/LauncherWindow.Shell.cs @@ -163,6 +163,53 @@ public partial class LauncherWindow _ = _vm.ExecuteSelectedAsync(); } + // ─── F3 QuickLook 토글 ──────────────────────────────────────────────── + + /// 현재 열린 QuickLook 창 참조 (토글/닫기 추적용) + private QuickLookWindow? _quickLookWindow; + + /// + /// F3 빠른 미리보기 토글. + /// 이미 열려 있으면 닫고, 없으면 선택된 파일/폴더로 미리보기 창을 엽니다. + /// + internal void ToggleQuickLook() + { + // 이미 열린 창이 있으면 닫기 (토글) + if (_quickLookWindow != null) + { + _quickLookWindow.Close(); + _quickLookWindow = null; + return; + } + + var selected = _vm.SelectedItem; + string? path = null; + + if (selected?.Data is Services.IndexEntry indexEntry) + path = Environment.ExpandEnvironmentVariables(indexEntry.Path); + + if (string.IsNullOrEmpty(path)) return; + if (!System.IO.File.Exists(path) && !System.IO.Directory.Exists(path)) return; + + // 런처 창 오른쪽에 배치 (화면 경계 고려) + var qlLeft = Left + ActualWidth + 8; + var qlTop = Top; + + // 화면 오른쪽 여백 확인 (화면 밖으로 나가면 왼쪽에 배치) + var screen = System.Windows.Forms.Screen.FromHandle( + new System.Windows.Interop.WindowInteropHelper(this).Handle); + if (qlLeft + 400 > screen.WorkingArea.Right) + qlLeft = Left - 408; + + _quickLookWindow = new QuickLookWindow(path, this) + { + Left = qlLeft, + Top = qlTop + }; + _quickLookWindow.Closed += (_, _) => _quickLookWindow = null; + _quickLookWindow.Show(); + } + // ─── 창 이벤트 / 스크롤 / 알림 ───────────────────────────────────────── private void Window_Deactivated(object sender, EventArgs e) @@ -171,6 +218,16 @@ public partial class LauncherWindow if (_vm.CloseOnFocusLost) Hide(); } + private void Window_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) + { + // 런처 숨김 시 QuickLook도 함께 닫기 + if (!(bool)e.NewValue) + { + _quickLookWindow?.Close(); + _quickLookWindow = null; + } + } + private void ScrollToSelected() { if (_vm.SelectedItem != null) @@ -182,4 +239,33 @@ public partial class LauncherWindow // 시스템 트레이 토스트 알림 표시 // App.xaml.cs의 TrayIcon을 통해 처리 } + + // ─── 퀵 액션 칩 클릭 ────────────────────────────────────────────────────── + + /// + /// 입력창 아래 최근 실행 칩 클릭 시 — 경로를 즉시 실행하고 런처를 닫습니다. + /// + private async void QuickActionChip_Click(object sender, MouseButtonEventArgs e) + { + if (((System.Windows.FrameworkElement)sender).DataContext + is not Models.QuickActionChip chip) return; + + var expanded = Environment.ExpandEnvironmentVariables(chip.Path); + Hide(); // 창 먼저 숨겨 체감 속도 확보 + + try + { + await Task.Run(() => + System.Diagnostics.Process.Start( + new System.Diagnostics.ProcessStartInfo(expanded) + { UseShellExecute = true })); + } + catch (Exception ex) + { + Services.LogService.Error($"퀵 액션 실행 실패: {expanded} - {ex.Message}"); + } + + // 사용 통계 기록 (비동기) + _ = Task.Run(() => Services.UsageRankingService.RecordExecution(chip.Path)); + } } diff --git a/src/AxCopilot/Views/LauncherWindow.xaml b/src/AxCopilot/Views/LauncherWindow.xaml index 70e31c1..1b3ea24 100644 --- a/src/AxCopilot/Views/LauncherWindow.xaml +++ b/src/AxCopilot/Views/LauncherWindow.xaml @@ -207,6 +207,10 @@ + + + + @@ -417,6 +421,55 @@ VerticalAlignment="Center"/> + + + + + + + + + + + + + + + + + + + + + +