mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-24 04:00:02 +01:00
## Summary of the Pull Request This PR improves fuzzy matching in Command Palette by: - Precomputing normalized strings to enable faster comparisons - Reducing memory allocations during matching, effectively down to zero It also introduces several behavioral improvements: - Strips diacritics from the normalized search string to improve matching across languages - Suppresses the same-case bonus when the query consists entirely of lowercase characters -- reflecting typical user input patterns - Allows skipping word separators -- enabling queries like Power Point to match PowerPoint This implementation is currently kept internal and is used only on the home page. For other scenarios, the `FuzzyStringMatcher` from `Microsoft.CommandPalette.Extensions.Toolkit` is being improved instead. `PrecomputedFuzzyMatcher` offers up to a 100× performance improvement over the current `FuzzyStringMatcher`, and approximately 2–5× better performance compared to the improved version. The improvement might seem small, but it adds up and becomes quite noticeable when filtering the entire home page—whether the user starts a new search or changes the query non-incrementally (e.g., using backspace). <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #45226 - [x] Closes: #44066 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
629 lines
21 KiB
C#
629 lines
21 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System.Collections.Immutable;
|
|
using System.Collections.Specialized;
|
|
using System.Diagnostics;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using ManagedCommon;
|
|
using Microsoft.CmdPal.Core.Common.Helpers;
|
|
using Microsoft.CmdPal.Core.Common.Text;
|
|
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
|
using Microsoft.CmdPal.Ext.Apps;
|
|
using Microsoft.CmdPal.Ext.Apps.Programs;
|
|
using Microsoft.CmdPal.Ext.Apps.State;
|
|
using Microsoft.CmdPal.UI.ViewModels.Commands;
|
|
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
|
using Microsoft.CmdPal.UI.ViewModels.Properties;
|
|
using Microsoft.CommandPalette.Extensions;
|
|
using Microsoft.CommandPalette.Extensions.Toolkit;
|
|
|
|
namespace Microsoft.CmdPal.UI.ViewModels.MainPage;
|
|
|
|
/// <summary>
|
|
/// This class encapsulates the data we load from built-in providers and extensions to use within the same extension-UI system for a <see cref="ListPage"/>.
|
|
/// TODO: Need to think about how we structure/interop for the page -> section -> item between the main setup, the extensions, and our viewmodels.
|
|
/// </summary>
|
|
public sealed partial class MainListPage : DynamicListPage,
|
|
IRecipient<ClearSearchMessage>,
|
|
IRecipient<UpdateFallbackItemsMessage>, IDisposable
|
|
{
|
|
private readonly TopLevelCommandManager _tlcManager;
|
|
private readonly AliasManager _aliasManager;
|
|
private readonly SettingsModel _settings;
|
|
private readonly AppStateModel _appStateModel;
|
|
private readonly ScoringFunction<IListItem> _scoringFunction;
|
|
private readonly ScoringFunction<IListItem> _fallbackScoringFunction;
|
|
private readonly IFuzzyMatcherProvider _fuzzyMatcherProvider;
|
|
|
|
private RoScored<IListItem>[]? _filteredItems;
|
|
private RoScored<IListItem>[]? _filteredApps;
|
|
|
|
// Keep as IEnumerable for deferred execution. Fallback item titles are updated
|
|
// asynchronously, so scoring must happen lazily when GetItems is called.
|
|
private IEnumerable<RoScored<IListItem>>? _scoredFallbackItems;
|
|
private IEnumerable<RoScored<IListItem>>? _fallbackItems;
|
|
|
|
private bool _includeApps;
|
|
private bool _filteredItemsIncludesApps;
|
|
private int _appResultLimit = 10;
|
|
|
|
private InterlockedBoolean _refreshRunning;
|
|
private InterlockedBoolean _refreshRequested;
|
|
|
|
private CancellationTokenSource? _cancellationTokenSource;
|
|
|
|
public MainListPage(
|
|
TopLevelCommandManager topLevelCommandManager,
|
|
SettingsModel settings,
|
|
AliasManager aliasManager,
|
|
AppStateModel appStateModel,
|
|
IFuzzyMatcherProvider fuzzyMatcherProvider)
|
|
{
|
|
Title = Resources.builtin_home_name;
|
|
Icon = IconHelpers.FromRelativePath("Assets\\StoreLogo.scale-200.png");
|
|
PlaceholderText = Properties.Resources.builtin_main_list_page_searchbar_placeholder;
|
|
|
|
_settings = settings;
|
|
_aliasManager = aliasManager;
|
|
_appStateModel = appStateModel;
|
|
_tlcManager = topLevelCommandManager;
|
|
_fuzzyMatcherProvider = fuzzyMatcherProvider;
|
|
_scoringFunction = (in query, item) => ScoreTopLevelItem(in query, item, _appStateModel.RecentCommands, _fuzzyMatcherProvider.Current);
|
|
_fallbackScoringFunction = (in _, item) => ScoreFallbackItem(item, _settings.FallbackRanks);
|
|
|
|
_tlcManager.PropertyChanged += TlcManager_PropertyChanged;
|
|
_tlcManager.TopLevelCommands.CollectionChanged += Commands_CollectionChanged;
|
|
|
|
// The all apps page will kick off a BG thread to start loading apps.
|
|
// We just want to know when it is done.
|
|
var allApps = AllAppsCommandProvider.Page;
|
|
allApps.PropChanged += (s, p) =>
|
|
{
|
|
if (p.PropertyName == nameof(allApps.IsLoading))
|
|
{
|
|
IsLoading = ActuallyLoading();
|
|
}
|
|
};
|
|
|
|
WeakReferenceMessenger.Default.Register<ClearSearchMessage>(this);
|
|
WeakReferenceMessenger.Default.Register<UpdateFallbackItemsMessage>(this);
|
|
|
|
settings.SettingsChanged += SettingsChangedHandler;
|
|
HotReloadSettings(settings);
|
|
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
|
|
|
|
IsLoading = true;
|
|
}
|
|
|
|
private void TlcManager_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == nameof(IsLoading))
|
|
{
|
|
IsLoading = ActuallyLoading();
|
|
}
|
|
}
|
|
|
|
private void Commands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
{
|
|
_includeApps = _tlcManager.IsProviderActive(AllAppsCommandProvider.WellKnownId);
|
|
if (_includeApps != _filteredItemsIncludesApps)
|
|
{
|
|
ReapplySearchInBackground();
|
|
}
|
|
else
|
|
{
|
|
RaiseItemsChanged();
|
|
}
|
|
}
|
|
|
|
private void ReapplySearchInBackground()
|
|
{
|
|
_refreshRequested.Set();
|
|
if (!_refreshRunning.Set())
|
|
{
|
|
return;
|
|
}
|
|
|
|
_ = Task.Run(RunRefreshLoop);
|
|
}
|
|
|
|
private void RunRefreshLoop()
|
|
{
|
|
try
|
|
{
|
|
do
|
|
{
|
|
_refreshRequested.Clear();
|
|
lock (_tlcManager.TopLevelCommands)
|
|
{
|
|
if (_filteredItemsIncludesApps == _includeApps)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
var currentSearchText = SearchText;
|
|
UpdateSearchText(currentSearchText, currentSearchText);
|
|
}
|
|
while (_refreshRequested.Value);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Logger.LogError("Failed to reload search", e);
|
|
}
|
|
finally
|
|
{
|
|
_refreshRunning.Clear();
|
|
if (_refreshRequested.Value && _refreshRunning.Set())
|
|
{
|
|
_ = Task.Run(RunRefreshLoop);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override IListItem[] GetItems()
|
|
{
|
|
lock (_tlcManager.TopLevelCommands)
|
|
{
|
|
// Either return the top-level commands (no search text), or the merged and
|
|
// filtered results.
|
|
if (string.IsNullOrWhiteSpace(SearchText))
|
|
{
|
|
return _tlcManager.TopLevelCommands
|
|
.Where(tlc => !tlc.IsFallback && !string.IsNullOrEmpty(tlc.Title))
|
|
.ToArray();
|
|
}
|
|
else
|
|
{
|
|
var validScoredFallbacks = _scoredFallbackItems?
|
|
.Where(s => !string.IsNullOrWhiteSpace(s.Item.Title))
|
|
.ToList();
|
|
|
|
var validFallbacks = _fallbackItems?
|
|
.Where(s => !string.IsNullOrWhiteSpace(s.Item.Title))
|
|
.ToList();
|
|
|
|
return MainListPageResultFactory.Create(
|
|
_filteredItems,
|
|
validScoredFallbacks,
|
|
_filteredApps,
|
|
validFallbacks,
|
|
_appResultLimit);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ClearResults()
|
|
{
|
|
_filteredItems = null;
|
|
_filteredApps = null;
|
|
_fallbackItems = null;
|
|
_scoredFallbackItems = null;
|
|
}
|
|
|
|
public override void UpdateSearchText(string oldSearch, string newSearch)
|
|
{
|
|
var stopwatch = Stopwatch.StartNew();
|
|
|
|
_cancellationTokenSource?.Cancel();
|
|
_cancellationTokenSource?.Dispose();
|
|
_cancellationTokenSource = new CancellationTokenSource();
|
|
|
|
var token = _cancellationTokenSource.Token;
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Handle changes to the filter text here
|
|
if (!string.IsNullOrEmpty(SearchText))
|
|
{
|
|
var aliases = _aliasManager;
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (aliases.CheckAlias(newSearch))
|
|
{
|
|
if (_filteredItemsIncludesApps != _includeApps)
|
|
{
|
|
lock (_tlcManager.TopLevelCommands)
|
|
{
|
|
_filteredItemsIncludesApps = _includeApps;
|
|
ClearResults();
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var commands = _tlcManager.TopLevelCommands;
|
|
lock (commands)
|
|
{
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// prefilter fallbacks
|
|
var globalFallbacks = _settings.GetGlobalFallbacks();
|
|
var specialFallbacks = new List<TopLevelViewModel>(globalFallbacks.Length);
|
|
var commonFallbacks = new List<TopLevelViewModel>();
|
|
|
|
foreach (var s in commands)
|
|
{
|
|
if (!s.IsFallback)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (globalFallbacks.Contains(s.Id))
|
|
{
|
|
specialFallbacks.Add(s);
|
|
}
|
|
else
|
|
{
|
|
commonFallbacks.Add(s);
|
|
}
|
|
}
|
|
|
|
// start update of fallbacks; update special fallbacks separately,
|
|
// so they can finish faster
|
|
UpdateFallbacks(SearchText, specialFallbacks, token);
|
|
UpdateFallbacks(SearchText, commonFallbacks, token);
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Cleared out the filter text? easy. Reset _filteredItems, and bail out.
|
|
if (string.IsNullOrEmpty(newSearch))
|
|
{
|
|
_filteredItemsIncludesApps = _includeApps;
|
|
ClearResults();
|
|
RaiseItemsChanged(commands.Count);
|
|
return;
|
|
}
|
|
|
|
// If the new string doesn't start with the old string, then we can't
|
|
// re-use previous results. Reset _filteredItems, and keep er moving.
|
|
if (!newSearch.StartsWith(oldSearch, StringComparison.CurrentCultureIgnoreCase))
|
|
{
|
|
ClearResults();
|
|
}
|
|
|
|
// If the internal state has changed, reset _filteredItems to reset the list.
|
|
if (_filteredItemsIncludesApps != _includeApps)
|
|
{
|
|
ClearResults();
|
|
}
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var newFilteredItems = Enumerable.Empty<IListItem>();
|
|
var newFallbacks = Enumerable.Empty<IListItem>();
|
|
var newApps = Enumerable.Empty<IListItem>();
|
|
|
|
if (_filteredItems is not null)
|
|
{
|
|
newFilteredItems = _filteredItems.Select(s => s.Item);
|
|
}
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_filteredApps is not null)
|
|
{
|
|
newApps = _filteredApps.Select(s => s.Item);
|
|
}
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_fallbackItems is not null)
|
|
{
|
|
newFallbacks = _fallbackItems.Select(s => s.Item);
|
|
}
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If we don't have any previous filter results to work with, start
|
|
// with a list of all our commands & apps.
|
|
if (!newFilteredItems.Any() && !newApps.Any())
|
|
{
|
|
newFilteredItems = commands.Where(s => !s.IsFallback);
|
|
|
|
// Fallbacks are always included in the list, even if they
|
|
// don't match the search text. But we don't want to
|
|
// consider them when filtering the list.
|
|
newFallbacks = commonFallbacks;
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_filteredItemsIncludesApps = _includeApps;
|
|
|
|
if (_includeApps)
|
|
{
|
|
var allNewApps = AllAppsCommandProvider.Page.GetItems().Cast<AppListItem>().ToList();
|
|
|
|
// We need to remove pinned apps from allNewApps so they don't show twice.
|
|
var pinnedApps = PinnedAppsManager.Instance.GetPinnedAppIdentifiers();
|
|
|
|
if (pinnedApps.Length > 0)
|
|
{
|
|
newApps = allNewApps.Where(w => pinnedApps.IndexOf(w.AppIdentifier) < 0);
|
|
}
|
|
else
|
|
{
|
|
newApps = allNewApps;
|
|
}
|
|
}
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
var searchQuery = _fuzzyMatcherProvider.Current.PrecomputeQuery(SearchText);
|
|
|
|
// Produce a list of everything that matches the current filter.
|
|
_filteredItems = InternalListHelpers.FilterListWithScores(newFilteredItems, searchQuery, _scoringFunction);
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IEnumerable<IListItem> newFallbacksForScoring = commands.Where(s => s.IsFallback && globalFallbacks.Contains(s.Id));
|
|
_scoredFallbackItems = InternalListHelpers.FilterListWithScores(newFallbacksForScoring, searchQuery, _scoringFunction);
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_fallbackItems = InternalListHelpers.FilterListWithScores<IListItem>(newFallbacks ?? [], searchQuery, _fallbackScoringFunction);
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Produce a list of filtered apps with the appropriate limit
|
|
if (newApps.Any())
|
|
{
|
|
_filteredApps = InternalListHelpers.FilterListWithScores(newApps, searchQuery, _scoringFunction);
|
|
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
var filterDoneTimestamp = stopwatch.ElapsedMilliseconds;
|
|
Logger.LogDebug($"Filter with '{newSearch}' in {filterDoneTimestamp}ms");
|
|
|
|
RaiseItemsChanged();
|
|
|
|
var listPageUpdatedTimestamp = stopwatch.ElapsedMilliseconds;
|
|
Logger.LogDebug($"Render items with '{newSearch}' in {listPageUpdatedTimestamp}ms /d {listPageUpdatedTimestamp - filterDoneTimestamp}ms");
|
|
|
|
stopwatch.Stop();
|
|
}
|
|
}
|
|
|
|
private void UpdateFallbacks(string newSearch, IReadOnlyList<TopLevelViewModel> commands, CancellationToken token)
|
|
{
|
|
_ = Task.Run(
|
|
() =>
|
|
{
|
|
var needsToUpdate = false;
|
|
|
|
foreach (var command in commands)
|
|
{
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var changedVisibility = command.SafeUpdateFallbackTextSynchronous(newSearch);
|
|
needsToUpdate = needsToUpdate || changedVisibility;
|
|
}
|
|
|
|
if (needsToUpdate)
|
|
{
|
|
if (token.IsCancellationRequested)
|
|
{
|
|
return;
|
|
}
|
|
|
|
RaiseItemsChanged();
|
|
}
|
|
},
|
|
token);
|
|
}
|
|
|
|
private bool ActuallyLoading()
|
|
{
|
|
var allApps = AllAppsCommandProvider.Page;
|
|
return allApps.IsLoading || _tlcManager.IsLoading;
|
|
}
|
|
|
|
// Almost verbatim ListHelpers.ScoreListItem, but also accounting for the
|
|
// fact that we want fallback handlers down-weighted, so that they don't
|
|
// _always_ show up first.
|
|
internal static int ScoreTopLevelItem(
|
|
in FuzzyQuery query,
|
|
IListItem topLevelOrAppItem,
|
|
IRecentCommandsManager history,
|
|
IPrecomputedFuzzyMatcher precomputedFuzzyMatcher)
|
|
{
|
|
var title = topLevelOrAppItem.Title;
|
|
if (string.IsNullOrWhiteSpace(title))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var isFallback = false;
|
|
var isAliasSubstringMatch = false;
|
|
var isAliasMatch = false;
|
|
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
|
|
|
FuzzyTarget? extensionDisplayNameTarget = null;
|
|
if (topLevelOrAppItem is TopLevelViewModel topLevel)
|
|
{
|
|
isFallback = topLevel.IsFallback;
|
|
extensionDisplayNameTarget = topLevel.GetExtensionNameTarget(precomputedFuzzyMatcher);
|
|
|
|
if (topLevel.HasAlias)
|
|
{
|
|
var alias = topLevel.AliasText;
|
|
isAliasMatch = alias == query.Original;
|
|
isAliasSubstringMatch = isAliasMatch || alias.StartsWith(query.Original, StringComparison.CurrentCultureIgnoreCase);
|
|
}
|
|
}
|
|
|
|
// Handle whitespace query separately - FuzzySearch doesn't handle it well
|
|
if (string.IsNullOrWhiteSpace(query.Original))
|
|
{
|
|
return ScoreWhitespaceQuery(query.Original, title, topLevelOrAppItem.Subtitle, isFallback);
|
|
}
|
|
|
|
// Get precomputed targets
|
|
var (titleTarget, subtitleTarget) = topLevelOrAppItem is IPrecomputedListItem precomputedItem
|
|
? (precomputedItem.GetTitleTarget(precomputedFuzzyMatcher), precomputedItem.GetSubtitleTarget(precomputedFuzzyMatcher))
|
|
: (precomputedFuzzyMatcher.PrecomputeTarget(title), precomputedFuzzyMatcher.PrecomputeTarget(topLevelOrAppItem.Subtitle));
|
|
|
|
// Score components
|
|
var nameScore = precomputedFuzzyMatcher.Score(query, titleTarget);
|
|
var descriptionScore = (precomputedFuzzyMatcher.Score(query, subtitleTarget) - 4) / 2.0;
|
|
var extensionScore = extensionDisplayNameTarget is { } extTarget ? precomputedFuzzyMatcher.Score(query, extTarget) / 1.5 : 0;
|
|
|
|
// Take best match from title/description/fallback, then add extension score
|
|
// Extension adds to max so items matching both title AND extension bubble up
|
|
var baseScore = Math.Max(Math.Max(nameScore, descriptionScore), isFallback ? 1 : 0);
|
|
var matchScore = baseScore + extensionScore;
|
|
|
|
// Apply a penalty to fallback items so they rank below direct matches.
|
|
// Fallbacks that dynamically match queries (like RDP connections) should
|
|
// appear after apps and direct command matches.
|
|
if (isFallback && matchScore > 1)
|
|
{
|
|
// Reduce fallback scores by 50% to prioritize direct matches
|
|
matchScore = matchScore * 0.5;
|
|
}
|
|
|
|
// Alias matching: exact match is overwhelming priority, substring match adds a small boost
|
|
var aliasBoost = isAliasMatch ? 9001 : (isAliasSubstringMatch ? 1 : 0);
|
|
var totalMatch = matchScore + aliasBoost;
|
|
|
|
// Apply scaling and history boost only if we matched something real
|
|
var finalScore = totalMatch * 10;
|
|
if (totalMatch > 0)
|
|
{
|
|
finalScore += history.GetCommandHistoryWeight(id);
|
|
}
|
|
|
|
return (int)finalScore;
|
|
}
|
|
|
|
private static int ScoreWhitespaceQuery(string query, string title, string subtitle, bool isFallback)
|
|
{
|
|
// Simple contains check for whitespace queries
|
|
var nameMatch = title.Contains(query, StringComparison.Ordinal) ? 1.0 : 0;
|
|
var descriptionMatch = subtitle.Contains(query, StringComparison.Ordinal) ? 0.5 : 0;
|
|
var baseScore = Math.Max(Math.Max(nameMatch, descriptionMatch), isFallback ? 1 : 0);
|
|
|
|
return (int)(baseScore * 10);
|
|
}
|
|
|
|
private static int ScoreFallbackItem(IListItem topLevelOrAppItem, string[] fallbackRanks)
|
|
{
|
|
// Default to 1 so it always shows in list.
|
|
var finalScore = 1;
|
|
|
|
if (topLevelOrAppItem is TopLevelViewModel topLevelViewModel)
|
|
{
|
|
var index = Array.IndexOf(fallbackRanks, topLevelViewModel.Id);
|
|
|
|
if (index >= 0)
|
|
{
|
|
finalScore = fallbackRanks.Length - index + 1;
|
|
}
|
|
}
|
|
|
|
return finalScore;
|
|
}
|
|
|
|
public void UpdateHistory(IListItem topLevelOrAppItem)
|
|
{
|
|
var id = IdForTopLevelOrAppItem(topLevelOrAppItem);
|
|
var history = _appStateModel.RecentCommands;
|
|
history.AddHistoryItem(id);
|
|
AppStateModel.SaveState(_appStateModel);
|
|
}
|
|
|
|
private static string IdForTopLevelOrAppItem(IListItem topLevelOrAppItem)
|
|
{
|
|
if (topLevelOrAppItem is TopLevelViewModel topLevel)
|
|
{
|
|
return topLevel.Id;
|
|
}
|
|
else
|
|
{
|
|
// we've got an app here
|
|
return topLevelOrAppItem.Command?.Id ?? string.Empty;
|
|
}
|
|
}
|
|
|
|
public void Receive(ClearSearchMessage message) => SearchText = string.Empty;
|
|
|
|
public void Receive(UpdateFallbackItemsMessage message) => RaiseItemsChanged(_tlcManager.TopLevelCommands.Count);
|
|
|
|
private void SettingsChangedHandler(SettingsModel sender, object? args) => HotReloadSettings(sender);
|
|
|
|
private void HotReloadSettings(SettingsModel settings) => ShowDetails = settings.ShowAppDetails;
|
|
|
|
public void Dispose()
|
|
{
|
|
_cancellationTokenSource?.Cancel();
|
|
_cancellationTokenSource?.Dispose();
|
|
|
|
_tlcManager.PropertyChanged -= TlcManager_PropertyChanged;
|
|
_tlcManager.TopLevelCommands.CollectionChanged -= Commands_CollectionChanged;
|
|
|
|
if (_settings is not null)
|
|
{
|
|
_settings.SettingsChanged -= SettingsChangedHandler;
|
|
}
|
|
|
|
WeakReferenceMessenger.Default.UnregisterAll(this);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|