[Phase L4-4/L4-6] 검색 히스토리 + 계산기 단위변환 단축 문법
L4-4 검색 히스토리 (↑/↓ 키 탐색): - Services/SearchHistoryService.cs (신규 100줄): 50개 FIFO JSON 저장 Add() · GetAll() · Clear(). 2자 미만/중복 최상단 무시 - ViewModels/LauncherViewModel.cs: _historyIndex·_isHistoryNavigation 필드 추가 NavigateHistoryPrev() / NavigateHistoryNext() / SetInputFromHistory() InputText setter: 직접 입력 시 _historyIndex 초기화 ExecuteSelectedAsync: 실행 전 히스토리 저장 (2자 이상) OnShown: _historyIndex = -1 초기화 - Views/LauncherWindow.Keyboard.cs: Key.Up/Down — Results.Count==0 분기: 히스토리 탐색 / 목록 탐색 분기 L4-3 클립보드 핀/카테고리: 기존 완전 구현 확인 (IsPinned, Category, TogglePin, Ctrl+P, #pin/#url/#코드/#경로 필터) L4-6 계산기 단위 변환 단축 문법: - Handlers/UnitConverter.cs: AutoSuggest(): "20km", "100f", "5lb" 등 목표 없이 주요 단위 자동 제안 _suggestions 테이블: 길이/무게/속도/데이터/온도 40개 단위 매핑 DateShortcut: "today+30d", "today-7w" → = 접두어에서 날짜 계산 - Handlers/CalculatorHandler.cs: DateShortcut.TryMatch 분기 추가 (통화 감지 전) UnitConverter.AutoSuggest 분기 추가 (명시 변환 후) - 힌트 텍스트: "20km · 100°F · today+30d" 추가 docs/LAUNCHER_ROADMAP.md: Phase L4 계획 테이블 추가 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,21 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Phase L4 — 검색/탐색 UX 혁신 + 생산성 확장 (v1.7.x)
|
||||||
|
|
||||||
|
> **방향**: Raycast/Alfred 기능 갭 해소 + 기존 L1-7 미완 기능 이행. 런처 단독 완결형 생산성 강화.
|
||||||
|
|
||||||
|
| # | 기능 | 설명 | 우선순위 |
|
||||||
|
|---|------|------|----------|
|
||||||
|
| L4-1 | **파일 탐색기 인라인 패널** | `cd` + `→` 키로 인라인 탐색기 진입. 경로 브레드크럼, ←/→ 폴더 이동, Ctrl+D 즐겨찾기 핀 | 높음 |
|
||||||
|
| L4-2 | **QuickLook F3 강화** | PDF(WinRT PdfDocument), 코드 구문강조(확장자별 배경색+라인 번호), Office 텍스트 추출 | 높음 |
|
||||||
|
| L4-3 | **클립보드 핀 & 카테고리** | Ctrl+P 핀 고정, `#pin/#url/#code` 필터, 자동 분류(URL/코드/경로). L1-7 이행 | 높음 |
|
||||||
|
| L4-4 | **검색 히스토리** | ↑/↓ 키로 이전 검색어 탐색. 50개 FIFO 로컬 저장. 런처 재시작 후에도 유지 | 중간 |
|
||||||
|
| L4-5 | **고급 검색 필터 문법** | `ext:.pdf size:>1mb modified:week in:documents` 인라인 필터. FuzzyEngine 파서 레이어 | 중간 |
|
||||||
|
| L4-6 | **계산기 단위 변환 확장** | `=20km`, `=100°F`, `=today+30d`. 로컬 변환 테이블. `=` 핸들러 확장 | 중간 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 기술 부채 (v1.5.0 해결)
|
## 기술 부채 (v1.5.0 해결)
|
||||||
|
|
||||||
| 항목 | 상태 |
|
| 항목 | 상태 |
|
||||||
|
|||||||
@@ -40,13 +40,17 @@ public class CalculatorHandler : IActionHandler
|
|||||||
|
|
||||||
var trimmed = query.Trim();
|
var trimmed = query.Trim();
|
||||||
|
|
||||||
|
// ─── today+Nd / today-Nd 날짜 단축 계산 (= 접두어에서 date 핸들러 위임) ─
|
||||||
|
if (DateShortcut.TryMatch(trimmed, out var dateItems))
|
||||||
|
return dateItems!;
|
||||||
|
|
||||||
// ─── 통화 변환 우선 감지 ──────────────────────────────────────────────
|
// ─── 통화 변환 우선 감지 ──────────────────────────────────────────────
|
||||||
if (CurrencyConverter.IsCurrencyQuery(trimmed))
|
if (CurrencyConverter.IsCurrencyQuery(trimmed))
|
||||||
{
|
{
|
||||||
return await CurrencyConverter.ConvertAsync(trimmed, ct);
|
return await CurrencyConverter.ConvertAsync(trimmed, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── 단위 변환 우선 감지 ──────────────────────────────────────────────
|
// ─── 단위 변환 (명시적 목표: 100km in miles) ─────────────────────────
|
||||||
if (UnitConverter.TryConvert(trimmed, out var convertResult))
|
if (UnitConverter.TryConvert(trimmed, out var convertResult))
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
@@ -58,6 +62,11 @@ public class CalculatorHandler : IActionHandler
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 단위 단축 자동 제안 (20km, 100f, 5lb …) ──────────────────────────
|
||||||
|
var autoSuggest = UnitConverter.AutoSuggest(trimmed);
|
||||||
|
if (autoSuggest.Count > 0)
|
||||||
|
return autoSuggest;
|
||||||
|
|
||||||
// ─── 수식 계산 ────────────────────────────────────────────────────────
|
// ─── 수식 계산 ────────────────────────────────────────────────────────
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -151,4 +151,131 @@ internal static class UnitConverter
|
|||||||
return ((long)v).ToString("N0", System.Globalization.CultureInfo.CurrentCulture);
|
return ((long)v).ToString("N0", System.Globalization.CultureInfo.CurrentCulture);
|
||||||
return v.ToString("G6", System.Globalization.CultureInfo.InvariantCulture);
|
return v.ToString("G6", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 자동 제안 (목표 단위 없이 입력 시) ──────────────────────────────────
|
||||||
|
|
||||||
|
// 패턴: <숫자><단위> (공백 없어도 OK)
|
||||||
|
private static readonly Regex _autoPattern = new(
|
||||||
|
@"^(-?\d+(?:\.\d+)?)\s*([a-z°/²³µ]+)$",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
// 단위 → 주요 변환 대상 목록 (사용자에게 자동 제안)
|
||||||
|
private static readonly Dictionary<string, string[]> _suggestions = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
// 길이
|
||||||
|
["km"] = ["miles", "m", "ft"],
|
||||||
|
["miles"] = ["km", "m", "ft"],
|
||||||
|
["m"] = ["km", "ft", "inches"],
|
||||||
|
["cm"] = ["inches", "m"],
|
||||||
|
["mm"] = ["inches", "cm"],
|
||||||
|
["ft"] = ["m", "km", "inches"],
|
||||||
|
["feet"] = ["m", "km", "inches"],
|
||||||
|
["in"] = ["cm", "m"],
|
||||||
|
["inches"]= ["cm", "m"],
|
||||||
|
// 무게
|
||||||
|
["kg"] = ["lb", "g"],
|
||||||
|
["lb"] = ["kg", "g"],
|
||||||
|
["lbs"] = ["kg", "g"],
|
||||||
|
["g"] = ["kg", "oz"],
|
||||||
|
["oz"] = ["g", "kg"],
|
||||||
|
// 속도
|
||||||
|
["km/h"] = ["mph", "m/s"],
|
||||||
|
["kmh"] = ["mph", "m/s"],
|
||||||
|
["kph"] = ["mph", "m/s"],
|
||||||
|
["mph"] = ["km/h", "m/s"],
|
||||||
|
["m/s"] = ["km/h", "mph"],
|
||||||
|
// 데이터
|
||||||
|
["mb"] = ["gb", "kb"],
|
||||||
|
["gb"] = ["mb", "tb"],
|
||||||
|
["tb"] = ["gb", "mb"],
|
||||||
|
["kb"] = ["mb", "b"],
|
||||||
|
// 온도 — 양방향
|
||||||
|
["c"] = ["f", "k"],
|
||||||
|
["°c"] = ["°f", "k"],
|
||||||
|
["celsius"]= ["fahrenheit", "kelvin"],
|
||||||
|
["f"] = ["c", "k"],
|
||||||
|
["°f"] = ["°c", "k"],
|
||||||
|
["fahrenheit"] = ["celsius", "kelvin"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "20km", "100f" 처럼 목표 단위 없이 입력하면 주요 변환 대상을 자동 제안합니다.
|
||||||
|
/// </summary>
|
||||||
|
public static List<LauncherItem> AutoSuggest(string input)
|
||||||
|
{
|
||||||
|
var m = _autoPattern.Match(input.Trim());
|
||||||
|
if (!m.Success) return new();
|
||||||
|
|
||||||
|
if (!double.TryParse(m.Groups[1].Value,
|
||||||
|
System.Globalization.NumberStyles.Float,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out var value))
|
||||||
|
return new();
|
||||||
|
|
||||||
|
var fromUnit = m.Groups[2].Value;
|
||||||
|
if (!_suggestions.TryGetValue(fromUnit, out var targets)) return new();
|
||||||
|
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
foreach (var to in targets)
|
||||||
|
{
|
||||||
|
if (TryConvert($"{value} {fromUnit} in {to}", out var r) && r != null)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
r,
|
||||||
|
$"{value} {fromUnit} → {to} · Enter로 복사",
|
||||||
|
null, r, Symbol: Symbols.Calculator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── = 접두어에서 today 날짜 단축 계산 ────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "today+30d", "today-7d", "today+2w" 등 날짜 단축 패턴을 = 핸들러에서 처리합니다.
|
||||||
|
/// DateCalcHandler의 로직을 재사용합니다.
|
||||||
|
/// </summary>
|
||||||
|
internal static class DateShortcut
|
||||||
|
{
|
||||||
|
private static readonly Regex _pattern = new(
|
||||||
|
@"^today\s*([+-])(\d+)([dDwWmMyY])$",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static bool TryMatch(string input, out IEnumerable<LauncherItem>? items)
|
||||||
|
{
|
||||||
|
items = null;
|
||||||
|
var m = _pattern.Match(input.Trim());
|
||||||
|
if (!m.Success) return false;
|
||||||
|
|
||||||
|
var sign = m.Groups[1].Value == "+" ? 1 : -1;
|
||||||
|
var val = int.Parse(m.Groups[2].Value) * sign;
|
||||||
|
var unit = m.Groups[3].Value.ToLowerInvariant();
|
||||||
|
var now = DateTime.Now;
|
||||||
|
|
||||||
|
var target = unit switch
|
||||||
|
{
|
||||||
|
"d" => now.AddDays(val),
|
||||||
|
"w" => now.AddDays(val * 7),
|
||||||
|
"m" => now.AddMonths(val),
|
||||||
|
"y" => now.AddYears(val),
|
||||||
|
_ => now.AddDays(val),
|
||||||
|
};
|
||||||
|
|
||||||
|
var diff = (target.Date - now.Date).Days;
|
||||||
|
var dayName = target.ToString("dddd", new System.Globalization.CultureInfo("ko-KR"));
|
||||||
|
var dateStr = target.ToString("yyyy-MM-dd");
|
||||||
|
var diffText = diff == 0 ? "오늘"
|
||||||
|
: diff > 0 ? $"+{diff}일 후"
|
||||||
|
: $"{-diff}일 전";
|
||||||
|
|
||||||
|
items = new List<LauncherItem>
|
||||||
|
{
|
||||||
|
new LauncherItem(
|
||||||
|
$"{dateStr} ({dayName})",
|
||||||
|
$"{input} → {diffText} · Enter로 복사",
|
||||||
|
null, dateStr, Symbol: Symbols.Calculator),
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
src/AxCopilot/Services/SearchHistoryService.cs
Normal file
101
src/AxCopilot/Services/SearchHistoryService.cs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace AxCopilot.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 런처 검색 히스토리를 로컬 파일로 관리합니다.
|
||||||
|
/// ↑/↓ 키로 이전 검색어를 탐색할 수 있습니다.
|
||||||
|
/// 최대 50개 FIFO — 가장 최근 항목이 index 0.
|
||||||
|
/// 저장 위치: %APPDATA%\AxCopilot\search_history.json
|
||||||
|
/// </summary>
|
||||||
|
internal static class SearchHistoryService
|
||||||
|
{
|
||||||
|
private const int MaxItems = 50;
|
||||||
|
|
||||||
|
private static readonly string _file = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"AxCopilot", "search_history.json");
|
||||||
|
|
||||||
|
private static List<string> _history = new();
|
||||||
|
private static bool _loaded;
|
||||||
|
private static readonly object _lock = new();
|
||||||
|
|
||||||
|
// ─── 공개 API ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 검색어를 히스토리에 추가합니다.
|
||||||
|
/// 공백·짧은 쿼리·중복(최근 항목)은 추가하지 않습니다.
|
||||||
|
/// </summary>
|
||||||
|
public static void Add(string query)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(query) || query.Trim().Length < 2) return;
|
||||||
|
var q = query.Trim();
|
||||||
|
EnsureLoaded();
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
// 이미 최상단이면 중복 추가 안 함
|
||||||
|
if (_history.Count > 0 && _history[0] == q) return;
|
||||||
|
// 기존 동일 항목 제거 후 맨 앞에 삽입
|
||||||
|
_history.Remove(q);
|
||||||
|
_history.Insert(0, q);
|
||||||
|
if (_history.Count > MaxItems) _history.RemoveAt(_history.Count - 1);
|
||||||
|
}
|
||||||
|
_ = SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>전체 히스토리 목록 반환 (최신 → 오래된 순)</summary>
|
||||||
|
public static IReadOnlyList<string> GetAll()
|
||||||
|
{
|
||||||
|
EnsureLoaded();
|
||||||
|
lock (_lock) return _history.AsReadOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>히스토리 전체 삭제</summary>
|
||||||
|
public static void Clear()
|
||||||
|
{
|
||||||
|
lock (_lock) _history.Clear();
|
||||||
|
_ = SaveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 내부 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static void EnsureLoaded()
|
||||||
|
{
|
||||||
|
if (_loaded) return;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_loaded) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(_file))
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(_file);
|
||||||
|
_history = JsonSerializer.Deserialize<List<string>>(json) ?? new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogService.Warn($"search_history.json 로드 실패: {ex.Message}");
|
||||||
|
_history = new();
|
||||||
|
}
|
||||||
|
_loaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task SaveAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> snapshot;
|
||||||
|
lock (_lock) snapshot = new List<string>(_history);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(_file)!);
|
||||||
|
var json = JsonSerializer.Serialize(snapshot);
|
||||||
|
await File.WriteAllTextAsync(_file, json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LogService.Warn($"search_history.json 저장 실패: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,10 @@ public partial class LauncherViewModel : INotifyPropertyChanged
|
|||||||
private const int DebounceMs = 30; // 30ms 디바운스 — 연속 입력 시 중간 검색 스킵
|
private const int DebounceMs = 30; // 30ms 디바운스 — 연속 입력 시 중간 검색 스킵
|
||||||
private string _lastSearchQuery = ""; // IME 조합 완성 후 동일 쿼리 재검색 방지용
|
private string _lastSearchQuery = ""; // IME 조합 완성 후 동일 쿼리 재검색 방지용
|
||||||
|
|
||||||
|
// ─── 검색 히스토리 탐색 ──────────────────────────────────────────────────
|
||||||
|
private bool _isHistoryNavigation; // InputText setter에서 히스토리 인덱스 리셋 방지
|
||||||
|
private int _historyIndex = -1; // -1 = 탐색 중 아님
|
||||||
|
|
||||||
// ─── 파일 액션 모드 ───────────────────────────────────────────────────────
|
// ─── 파일 액션 모드 ───────────────────────────────────────────────────────
|
||||||
private bool _isActionMode;
|
private bool _isActionMode;
|
||||||
private LauncherItem? _actionSourceItem;
|
private LauncherItem? _actionSourceItem;
|
||||||
@@ -66,6 +70,8 @@ public partial class LauncherViewModel : INotifyPropertyChanged
|
|||||||
{
|
{
|
||||||
if (_inputText == value) return;
|
if (_inputText == value) return;
|
||||||
_inputText = value;
|
_inputText = value;
|
||||||
|
// 사용자 직접 입력 시 히스토리 탐색 위치 초기화
|
||||||
|
if (!_isHistoryNavigation) _historyIndex = -1;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
OnPropertyChanged(nameof(HasActivePrefix));
|
OnPropertyChanged(nameof(HasActivePrefix));
|
||||||
OnPropertyChanged(nameof(ActivePrefixLabel));
|
OnPropertyChanged(nameof(ActivePrefixLabel));
|
||||||
@@ -276,10 +282,49 @@ public partial class LauncherViewModel : INotifyPropertyChanged
|
|||||||
if (IsActionMode) { IsActionMode = false; _actionSourceItem = null; }
|
if (IsActionMode) { IsActionMode = false; _actionSourceItem = null; }
|
||||||
Results.Clear();
|
Results.Clear();
|
||||||
_lastSearchQuery = "";
|
_lastSearchQuery = "";
|
||||||
|
_historyIndex = -1;
|
||||||
ClearMerge();
|
ClearMerge();
|
||||||
LoadQuickActions();
|
LoadQuickActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── 검색 히스토리 탐색 ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ↑ 키 — 이전(오래된) 히스토리 항목으로 이동.
|
||||||
|
/// null이면 히스토리 없음.
|
||||||
|
/// </summary>
|
||||||
|
public string? NavigateHistoryPrev()
|
||||||
|
{
|
||||||
|
var history = SearchHistoryService.GetAll();
|
||||||
|
if (history.Count == 0) return null;
|
||||||
|
_historyIndex = Math.Min(_historyIndex + 1, history.Count - 1);
|
||||||
|
return history[_historyIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ↓ 키 — 최신 히스토리 항목으로 이동.
|
||||||
|
/// _historyIndex가 0 이하면 빈 문자열(현재 입력으로 복귀).
|
||||||
|
/// </summary>
|
||||||
|
public string? NavigateHistoryNext()
|
||||||
|
{
|
||||||
|
if (_historyIndex <= 0) { _historyIndex = -1; return ""; }
|
||||||
|
_historyIndex--;
|
||||||
|
var history = SearchHistoryService.GetAll();
|
||||||
|
return _historyIndex >= 0 && _historyIndex < history.Count
|
||||||
|
? history[_historyIndex] : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 히스토리에서 탐색한 텍스트를 입력창에 설정합니다.
|
||||||
|
/// InputText setter의 히스토리 인덱스 리셋을 억제합니다.
|
||||||
|
/// </summary>
|
||||||
|
public void SetInputFromHistory(string text)
|
||||||
|
{
|
||||||
|
_isHistoryNavigation = true;
|
||||||
|
try { InputText = text; }
|
||||||
|
finally { _isHistoryNavigation = false; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UsageRankingService 상위 항목에서 퀵 액션 칩을 생성합니다.
|
/// UsageRankingService 상위 항목에서 퀵 액션 칩을 생성합니다.
|
||||||
/// 실제로 존재하는 파일/폴더만 표시하며 최대 8개로 제한합니다.
|
/// 실제로 존재하는 파일/폴더만 표시하며 최대 8개로 제한합니다.
|
||||||
@@ -405,6 +450,10 @@ public partial class LauncherViewModel : INotifyPropertyChanged
|
|||||||
|
|
||||||
if (SelectedItem == null) return;
|
if (SelectedItem == null) return;
|
||||||
|
|
||||||
|
// 실행 전 검색어 히스토리 저장 (2자 이상, prefix 포함)
|
||||||
|
if (InputText.Trim().Length >= 2)
|
||||||
|
SearchHistoryService.Add(InputText.Trim());
|
||||||
|
|
||||||
// 창을 먼저 닫아 체감 속도 확보 → 실행은 백그라운드
|
// 창을 먼저 닫아 체감 속도 확보 → 실행은 백그라운드
|
||||||
CloseRequested?.Invoke(this, EventArgs.Empty);
|
CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,16 @@ public partial class LauncherWindow
|
|||||||
_vm.ToggleMergeItem(_vm.SelectedItem);
|
_vm.ToggleMergeItem(_vm.SelectedItem);
|
||||||
_vm.SelectNext();
|
_vm.SelectNext();
|
||||||
}
|
}
|
||||||
|
else if (_vm.Results.Count == 0)
|
||||||
|
{
|
||||||
|
// 결과 없음 → 검색 히스토리 탐색 (최신 방향)
|
||||||
|
var histNext = _vm.NavigateHistoryNext();
|
||||||
|
if (histNext != null)
|
||||||
|
{
|
||||||
|
_vm.SetInputFromHistory(histNext);
|
||||||
|
InputBox.CaretIndex = InputBox.Text.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_vm.SelectNext();
|
_vm.SelectNext();
|
||||||
@@ -122,6 +132,16 @@ public partial class LauncherWindow
|
|||||||
_vm.ToggleMergeItem(_vm.SelectedItem);
|
_vm.ToggleMergeItem(_vm.SelectedItem);
|
||||||
_vm.SelectPrev();
|
_vm.SelectPrev();
|
||||||
}
|
}
|
||||||
|
else if (_vm.Results.Count == 0)
|
||||||
|
{
|
||||||
|
// 결과 없음 → 검색 히스토리 탐색 (이전 방향)
|
||||||
|
var histPrev = _vm.NavigateHistoryPrev();
|
||||||
|
if (histPrev != null)
|
||||||
|
{
|
||||||
|
_vm.SetInputFromHistory(histPrev);
|
||||||
|
InputBox.CaretIndex = InputBox.Text.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_vm.SelectPrev();
|
_vm.SelectPrev();
|
||||||
@@ -441,6 +461,14 @@ public partial class LauncherWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── F3 → 파일 빠른 미리보기 (QuickLook 토글) ────────────────────────
|
||||||
|
if (e.Key == Key.F3)
|
||||||
|
{
|
||||||
|
ToggleQuickLook();
|
||||||
|
e.Handled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Ctrl+1~9 → n번째 결과 즉시 실행 ───────────────────────────────
|
// ─── Ctrl+1~9 → n번째 결과 즉시 실행 ───────────────────────────────
|
||||||
if (mod == ModifierKeys.Control)
|
if (mod == ModifierKeys.Control)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user