[Phase L2-7] 퀵 액션 바 — 최근 실행 항목 칩 표시
Models/QuickActionChip.cs (신규 12줄): - Title, Symbol, Path, Background 레코드 Services/UsageRankingService.cs: - GetTopItems(int n) 메서드 추가: 실행 횟수 상위 N개 (경로, 횟수) 반환 ViewModels/LauncherViewModel.cs: - QuickActionItems (ObservableCollection<QuickActionChip>) 프로퍼티 추가 - ShowQuickActions: 입력 비었을 때 칩 표시 조건 - LoadQuickActions(): 상위 8개 경로 → 파일 존재 확인 → 타입별 아이콘/색상 칩 생성 - OnShown()에서 LoadQuickActions() 호출 - InputText 변경 시 ShowQuickActions 알림 Views/LauncherWindow.xaml: - 입력 Grid를 2행 구조로 변환 (RowDefinitions 추가) - Row 1: ItemsControl + WrapPanel + DataTemplate 칩 UI - CornerRadius=10 Border, 아이콘+제목 StackPanel - 호버 시 AccentColor 테두리, 최대 너비 100px 말줄임 Views/LauncherWindow.Shell.cs: - QuickActionChip_Click 핸들러: 창 숨김 → 경로 실행 → 사용 통계 기록 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
14
src/AxCopilot/Models/QuickActionChip.cs
Normal file
14
src/AxCopilot/Models/QuickActionChip.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace AxCopilot.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 퀵 액션 바에 표시되는 최근 실행 항목 칩 모델.
|
||||
/// 입력창 아래에 가로 칩 형태로 표시되며 클릭 시 즉시 실행됩니다.
|
||||
/// </summary>
|
||||
public record QuickActionChip(
|
||||
string Title,
|
||||
string Symbol,
|
||||
string Path,
|
||||
Brush Background
|
||||
);
|
||||
@@ -46,6 +46,21 @@ internal static class UsageRankingService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실행 횟수 기준 상위 N개의 (경로, 횟수) 목록을 반환합니다.
|
||||
/// 퀵 액션 바 표시에 활용됩니다.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 실행 횟수 기준으로 내림차순 정렬하는 컴파러를 반환합니다.
|
||||
/// 동점이면 원래 순서 유지 (stable sort).
|
||||
|
||||
@@ -48,6 +48,15 @@ public partial class LauncherViewModel : INotifyPropertyChanged
|
||||
/// </summary>
|
||||
public ICollectionView GroupedResults { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 퀵 액션 바 — 가장 많이 실행한 항목을 입력창 아래 칩으로 표시합니다.
|
||||
/// 입력창이 비어 있을 때만 표시됩니다.
|
||||
/// </summary>
|
||||
public ObservableCollection<QuickActionChip> QuickActionItems { get; } = new();
|
||||
|
||||
/// <summary>퀵 액션 칩 표시 조건: 입력이 없고 칩이 하나 이상 있을 때</summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UsageRankingService 상위 항목에서 퀵 액션 칩을 생성합니다.
|
||||
/// 실제로 존재하는 파일/폴더만 표시하며 최대 8개로 제한합니다.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
|
||||
// ─── 검색 ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -163,6 +163,53 @@ public partial class LauncherWindow
|
||||
_ = _vm.ExecuteSelectedAsync();
|
||||
}
|
||||
|
||||
// ─── F3 QuickLook 토글 ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>현재 열린 QuickLook 창 참조 (토글/닫기 추적용)</summary>
|
||||
private QuickLookWindow? _quickLookWindow;
|
||||
|
||||
/// <summary>
|
||||
/// F3 빠른 미리보기 토글.
|
||||
/// 이미 열려 있으면 닫고, 없으면 선택된 파일/폴더로 미리보기 창을 엽니다.
|
||||
/// </summary>
|
||||
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을 통해 처리
|
||||
}
|
||||
|
||||
// ─── 퀵 액션 칩 클릭 ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 입력창 아래 최근 실행 칩 클릭 시 — 경로를 즉시 실행하고 런처를 닫습니다.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +207,10 @@
|
||||
|
||||
<!-- ─── 입력 영역 ─── -->
|
||||
<Grid Grid.Row="0" Margin="20,16,20,16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
@@ -417,6 +421,55 @@
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 퀵 액션 칩 바 (Row 1) — 입력이 없을 때 최근 실행 항목 표시 ─── -->
|
||||
<ItemsControl Grid.Row="1" Grid.ColumnSpan="3"
|
||||
ItemsSource="{Binding QuickActionItems}"
|
||||
Margin="0,10,0,0"
|
||||
Visibility="{Binding ShowQuickActions, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border Margin="0,0,6,6"
|
||||
Padding="9,5"
|
||||
CornerRadius="10"
|
||||
Cursor="Hand"
|
||||
Background="{Binding Background}"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
MouseLeftButtonUp="QuickActionChip_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentColor}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Symbol}"
|
||||
FontFamily="Segoe MDL2 Assets"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,5,0"/>
|
||||
<TextBlock Text="{Binding Title}"
|
||||
FontFamily="Segoe UI, Malgun Gothic"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"
|
||||
MaxWidth="100"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── 파일 액션 모드 breadcrumb 바 ─── -->
|
||||
|
||||
Reference in New Issue
Block a user