Users can now pin/unpin apps from the top of AllApps extension (#40544)

Users can now use Ctrl+P to toggle pinning and unpinning applications
from the top of the All Apps extension.

This pinned status persists through restarts & reboots.


https://github.com/user-attachments/assets/86895a38-7312-438a-9409-b50a85979d12

Reference: #40543

---------

Co-authored-by: Mike Griese <migrie@microsoft.com>
This commit is contained in:
Michael Jolley
2025-07-15 08:59:32 -05:00
committed by GitHub
parent 5800b81638
commit 19390e3198
20 changed files with 548 additions and 88 deletions

View File

@@ -2,14 +2,20 @@
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -18,16 +24,22 @@ namespace Microsoft.CmdPal.Ext.Apps;
public sealed partial class AllAppsPage : ListPage
{
private readonly Lock _listLock = new();
private AppListItem[] allAppsSection = [];
private AppItem[] allApps = [];
private AppListItem[] unpinnedApps = [];
private AppListItem[] pinnedApps = [];
public AllAppsPage()
{
this.Name = Resources.all_apps;
this.Icon = IconHelpers.FromRelativePath("Assets\\AllApps.svg");
this.Icon = Icons.AllAppsIcon;
this.ShowDetails = true;
this.IsLoading = true;
this.PlaceholderText = Resources.search_installed_apps_placeholder;
// Subscribe to pin state changes to refresh the command provider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
Task.Run(() =>
{
lock (_listLock)
@@ -37,89 +49,139 @@ public sealed partial class AllAppsPage : ListPage
});
}
internal AppListItem[] GetPinnedApps()
{
BuildListItems();
return pinnedApps;
}
public override IListItem[] GetItems()
{
if (allAppsSection.Length == 0 || AppCache.Instance.Value.ShouldReload())
{
lock (_listLock)
{
BuildListItems();
}
}
return allAppsSection;
// Build or update the list if needed
BuildListItems();
return pinnedApps.Concat(unpinnedApps).ToArray();
}
private void BuildListItems()
{
this.IsLoading = true;
if (allApps.Length == 0 || AppCache.Instance.Value.ShouldReload())
{
lock (_listLock)
{
this.IsLoading = true;
Stopwatch stopwatch = new();
stopwatch.Start();
Stopwatch stopwatch = new();
stopwatch.Start();
var apps = GetPrograms();
var apps = GetPrograms();
this.allApps = apps.AllApps;
this.pinnedApps = apps.PinnedItems;
this.unpinnedApps = apps.UnpinnedItems;
this.allAppsSection = apps
.Select((app) => new AppListItem(app, true))
.ToArray();
this.IsLoading = false;
this.IsLoading = false;
AppCache.Instance.Value.ResetReloadFlag();
AppCache.Instance.Value.ResetReloadFlag();
stopwatch.Stop();
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
stopwatch.Stop();
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
}
}
}
internal List<AppItem> GetPrograms()
private AppItem[] GetAllApps()
{
var uwpResults = AppCache.Instance.Value.UWPs
.Where((application) => application.Enabled)
.Select(UwpToAppItem);
.Where((application) => application.Enabled)
.Select(app => app.ToAppItem());
var win32Results = AppCache.Instance.Value.Win32s
.Where((application) => application.Enabled && application.Valid)
.Select(app =>
{
var icoPath = string.IsNullOrEmpty(app.IcoPath) ?
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
app.IcoPath :
app.FullPath) :
app.IcoPath;
.Select(app => app.ToAppItem());
// icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ? (icoPath + ",0") : icoPath;
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
app.FullPath :
icoPath;
return new AppItem()
{
Name = app.Name,
Subtitle = app.Description,
Type = app.Type(),
IcoPath = icoPath,
ExePath = !string.IsNullOrEmpty(app.LnkFilePath) ? app.LnkFilePath : app.FullPath,
DirPath = app.Location,
Commands = app.GetCommands(),
};
});
return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList();
var allApps = uwpResults.Concat(win32Results).ToArray();
return allApps;
}
private AppItem UwpToAppItem(UWPApplication app)
internal (AppItem[] AllApps, AppListItem[] PinnedItems, AppListItem[] UnpinnedItems) GetPrograms()
{
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;
var item = new AppItem()
var allApps = GetAllApps();
var pinned = new List<AppListItem>();
var unpinned = new List<AppListItem>();
foreach (var app in allApps)
{
Name = app.Name,
Subtitle = app.Description,
Type = UWPApplication.Type(),
IcoPath = iconPath,
DirPath = app.Location,
UserModelId = app.UserModelId,
IsPackaged = true,
Commands = app.GetCommands(),
};
return item;
var isPinned = PinnedAppsManager.Instance.IsAppPinned(app.AppIdentifier);
var appListItem = new AppListItem(app, true, isPinned);
if (isPinned)
{
appListItem.Tags = appListItem.Tags
.Concat([new Tag() { Icon = Icons.PinIcon }])
.ToArray();
pinned.Add(appListItem);
}
else
{
unpinned.Add(appListItem);
}
}
return (
allApps
.ToArray(),
pinned
.OrderBy(app => app.Title)
.ToArray(),
unpinned
.OrderBy(app => app.Title)
.ToArray());
}
private void OnPinStateChanged(object? sender, PinStateChangedEventArgs e)
{
/*
* Rebuilding all the lists is pretty expensive.
* So, instead, we'll just compare pinned items to move existing
* items between the two lists.
*/
var existingAppItem = allApps.FirstOrDefault(f => f.AppIdentifier == e.AppIdentifier);
if (existingAppItem != null)
{
var appListItem = new AppListItem(existingAppItem, true, e.IsPinned);
if (e.IsPinned)
{
// Remove it from the unpinned apps array
this.unpinnedApps = this.unpinnedApps
.Where(app => app.AppIdentifier != existingAppItem.AppIdentifier)
.OrderBy(app => app.Title)
.ToArray();
var newPinned = this.pinnedApps.ToList();
newPinned.Add(appListItem);
this.pinnedApps = newPinned
.OrderBy(app => app.Title)
.ToArray();
}
else
{
// Remove it from the pinned apps array
this.pinnedApps = this.pinnedApps
.Where(app => app.AppIdentifier != existingAppItem.AppIdentifier)
.OrderBy(app => app.Title)
.ToArray();
var newUnpinned = this.unpinnedApps.ToList();
newUnpinned.Add(appListItem);
this.unpinnedApps = newUnpinned
.OrderBy(app => app.Title)
.ToArray();
}
RaiseItemsChanged(0);
}
}
}