A collection of prototype fixes (#158)

Probably the last real prototype commit. 

There was a collection of _little things_ that made the current state of the prototype a little wacky. This fixes a lot of them, so that the prototype is in a "demoable" state. 

* Updates the WinUI version - there was a bugfix in that minor version bump that's needed to help fix the `E_LAYOUTCYCLE`. 
* The list no longer flickers uncontrollably as it loads top-level commands
* filtering the list is a lot more efficient (still not what it used to be though)
* Forms us a `ListView` instead of an `ItemsRepeater`. `ItemsView` also had a layout cycle, go figure. 
* Fixes the namespace of the samples extension, so it doesn't conflict with the SSH one
* Adds a bunch of icons, subtitles
* When the list updates, we'll try to maintain the selected item (really useful for something like the mastodon extension)
* When we update tthe filter text, we'll do _way_ better at actually updating our own `SelectedItem`, 
  * so that the Details doesn't stay open on an app if you hit `esc`
  * so that the selection doesn't.... fuck off to space (closes #155)
* Fixes the calculator command to show up originally in the list
* Includes _some_ of the styling changes @niels9001 is working on (notably, the subtitle being on it's own line)
* I think also fixes #113
This commit is contained in:
Mike Griese
2024-11-15 11:00:15 -08:00
committed by GitHub
parent 42e7566926
commit dc8a529e86
34 changed files with 852 additions and 192 deletions

View File

@@ -51,7 +51,7 @@
-->
<PackageVersion Include="Microsoft.Windows.CsWinRT" Version="2.1.5" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.240923002" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.6.241106002" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="2.0.9" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
<PackageVersion Include="ModernWpfUI" Version="0.9.4" />

View File

@@ -630,7 +630,7 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EtwTrace", "src\common\Tele
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MouseWithoutBorders.UnitTests", "src\modules\MouseWithoutBorders\MouseWithoutBorders.UnitTests\MouseWithoutBorders.UnitTests.csproj", "{66614C26-314C-4B91-9071-76133422CFEF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommandPallete", "CommandPallete", "{3846508C-77EB-4034-A702-F8BB263C4F79}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CommandPalette", "CommandPalette", "{3846508C-77EB-4034-A702-F8BB263C4F79}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI.Poc", "src\modules\cmdpal\WindowsCommandPalette\Microsoft.CmdPal.UI.Poc.csproj", "{A60CB091-3E95-49F3-8315-18EA3B4334B9}"
EndProject
@@ -694,6 +694,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CmdPal.UI.ViewMod
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PokedexExtension", "src\modules\cmdpal\Exts\PokedexExtension\PokedexExtension.csproj", "{D8DD2E06-7956-4673-95E7-F395AB5A5485}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MenusExtension", "src\modules\cmdpal\Exts\Menus\MenusExtension.csproj", "{8ABE2195-7514-425E-9A89-685FA42CEFC3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -3232,6 +3234,24 @@ Global
{D8DD2E06-7956-4673-95E7-F395AB5A5485}.Release|x86.ActiveCfg = Release|x64
{D8DD2E06-7956-4673-95E7-F395AB5A5485}.Release|x86.Build.0 = Release|x64
{D8DD2E06-7956-4673-95E7-F395AB5A5485}.Release|x86.Deploy.0 = Release|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Debug|ARM64.ActiveCfg = Debug|ARM64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Debug|ARM64.Build.0 = Debug|ARM64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Debug|ARM64.Deploy.0 = Debug|ARM64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Debug|x64.ActiveCfg = Debug|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Debug|x64.Build.0 = Debug|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Debug|x64.Deploy.0 = Debug|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Debug|x86.ActiveCfg = Debug|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Debug|x86.Build.0 = Debug|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Debug|x86.Deploy.0 = Debug|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|ARM64.ActiveCfg = Release|ARM64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|ARM64.Build.0 = Release|ARM64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|ARM64.Deploy.0 = Release|ARM64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|x64.ActiveCfg = Release|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|x64.Build.0 = Release|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|x64.Deploy.0 = Release|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|x86.ActiveCfg = Release|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|x86.Build.0 = Release|x64
{8ABE2195-7514-425E-9A89-685FA42CEFC3}.Release|x86.Deploy.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -3495,6 +3515,7 @@ Global
{8FBDABA4-40EE-4C0E-9BC8-2F6444A6EF90} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
{C66020D1-CB10-4CF7-8715-84C97FD5E5E2} = {7520A2FE-00A2-49B8-83ED-DB216E874C04}
{D8DD2E06-7956-4673-95E7-F395AB5A5485} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
{8ABE2195-7514-425E-9A89-685FA42CEFC3} = {071E18A4-A530-46B8-AB7D-B862EE55E24E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -24,7 +24,7 @@ internal sealed partial class MastodonExtensionPage : ListPage
internal static readonly HttpClient Client = new();
internal static readonly JsonSerializerOptions Options = new() { PropertyNameCaseInsensitive = true };
private readonly List<MastodonStatus> _posts = new();
private readonly List<ListItem> _items = new();
public MastodonExtensionPage()
{
@@ -37,18 +37,11 @@ internal sealed partial class MastodonExtensionPage : ListPage
AccentColor = Color.FromArgb(255, 99, 100, 255);
}
public override IListItem[] GetItems()
private void AddPosts(List<MastodonStatus> posts)
{
if (_posts.Count == 0)
foreach (var p in posts)
{
var postsAsync = FetchExplorePage();
postsAsync.ConfigureAwait(false);
var posts = postsAsync.Result;
this._posts.AddRange(posts);
}
return _posts
.Select(p => new ListItem(new MastodonPostPage(p))
var postItem = new ListItem(new MastodonPostPage(p))
{
Title = p.Account.DisplayName, // p.ContentAsPlainText(),
Subtitle = $"@{p.Account.Username}",
@@ -76,18 +69,38 @@ internal sealed partial class MastodonExtensionPage : ListPage
MoreCommands = [
new CommandContextItem(new OpenUrlCommand(p.Url) { Name = "Open on web" }),
],
})
};
this._items.Add(postItem);
}
}
public override IListItem[] GetItems()
{
if (_items.Count == 0)
{
var postsAsync = FetchExplorePage();
postsAsync.ConfigureAwait(false);
var posts = postsAsync.Result;
this.AddPosts(posts);
}
return _items
.ToArray();
}
public override void LoadMore()
{
var postsAsync = FetchExplorePage(20, this._posts.Count);
this.Loading = true;
ExtensionHost.LogMessage(new LogMessage() { Message = $"Loading 20 posts, starting with {_items.Count}..." });
var postsAsync = FetchExplorePage(20, this._items.Count);
postsAsync.ContinueWith((res) =>
{
var posts = postsAsync.Result;
this._posts.AddRange(posts);
this.RaiseItemsChanged(this._posts.Count);
this.AddPosts(posts);
ExtensionHost.LogMessage(new LogMessage() { Message = $"... got {posts.Count} new posts" });
this.Loading = false;
this.RaiseItemsChanged(this._items.Count);
}).ConfigureAwait(false);
}

Binary file not shown.

View File

@@ -0,0 +1,231 @@
// 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;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Extensions.Helpers;
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Menus;
public partial class MenusActionsProvider : CommandProvider
{
public MenusActionsProvider()
{
DisplayName = $"Menus from the open windows";
}
private readonly IListItem[] _commands = [
new ListItem(new AllWindowsPage()) { Subtitle = "Search menus in open windows" },
];
public override IListItem[] TopLevelCommands()
{
return _commands;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
internal sealed partial class MenuItemCommand : InvokableCommand
{
private readonly MenuData _menuData;
private readonly HWND _hwnd;
public MenuItemCommand(MenuData data, HWND hwnd)
{
_menuData = data;
_hwnd = hwnd;
}
public override ICommandResult Invoke()
{
PInvoke.SetForegroundWindow(_hwnd);
PInvoke.SetActiveWindow(_hwnd);
PInvoke.PostMessage(_hwnd, 273/*WM_COMMAND*/, _menuData.WID, 0);
return CommandResult.KeepOpen();
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
internal sealed class MenuData
{
public string ItemText { get; set; }
public string PathText { get; set; }
public uint WID { get; set; }
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
internal sealed class WindowData
{
private readonly HWND handle;
private readonly string title = string.Empty;
public string Title => title;
public HWND Handle => handle;
internal WindowData(HWND hWnd)
{
handle = hWnd;
var textLen = PInvoke.GetWindowTextLength(handle);
if (textLen == 0)
{
return;
}
var bufferSize = textLen + 1;
unsafe
{
fixed (char* windowNameChars = new char[bufferSize])
{
if (PInvoke.GetWindowText(handle, windowNameChars, bufferSize) == 0)
{
var errorCode = Marshal.GetLastWin32Error();
if (errorCode != 0)
{
throw new Win32Exception(errorCode);
}
}
title = new string(windowNameChars);
}
}
}
public List<MenuData> GetMenuItems()
{
var hMenu = PInvoke.GetMenu_SafeHandle(handle);
return GetMenuItems(hMenu, string.Empty);
}
public List<MenuData> GetMenuItems(DestroyMenuSafeHandle hMenu, string menuPath)
{
// var s = new SafeMenu();
// s.SetHandle(hMenu);
List<MenuData> results = new();
var menuItemCount = PInvoke.GetMenuItemCount(hMenu);
for (var i = 0; i < menuItemCount; i++)
{
var mii = default(MENUITEMINFOW);
mii.cbSize = (uint)Marshal.SizeOf<MENUITEMINFOW>();
mii.fMask = MENU_ITEM_MASK.MIIM_STRING | MENU_ITEM_MASK.MIIM_ID | MENU_ITEM_MASK.MIIM_SUBMENU;
mii.cch = 256;
unsafe
{
fixed (char* menuTextBuffer = new char[mii.cch])
{
mii.dwTypeData = new PWSTR(menuTextBuffer); // Allocate memory for string
if (PInvoke.GetMenuItemInfo(hMenu, (uint)i, true, ref mii))
{
var itemText = mii.dwTypeData.ToString();
// Sanitize it. If it's got a tab, grab the text before that:
var withoutShortcut = itemText.Split("\t").First();
// Now remove a `&`
var sanitized = withoutShortcut.Replace("&", string.Empty);
var itemPath = $"{menuPath}{sanitized}";
// Leaf item
if (mii.hSubMenu == IntPtr.Zero)
{
// Console.WriteLine($"- Leaf Item: {itemText}");
// TriggerMenuItem(hWnd, mii.wID);
var data = new MenuData() { ItemText = sanitized, PathText = itemPath, WID = mii.wID };
results.Add(data);
}
else
{
// Recursively list submenu items
var subMenuTest = PInvoke.GetSubMenu(hMenu, i);
var otherTest = mii.hSubMenu;
_ = otherTest == subMenuTest.DangerousGetHandle();
var newPath = $"{sanitized} > ";
var subItems = GetMenuItems(subMenuTest, newPath);
results.AddRange(subItems);
}
}
}
}
}
return results;
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
internal sealed partial class WindowMenusPage : ListPage
{
private readonly WindowData _window;
public WindowMenusPage(WindowData window)
{
_window = window;
Icon = new(string.Empty);
Name = window.Title;
ShowDetails = false;
}
public override IListItem[] GetItems()
{
return _window.GetMenuItems().Select(menuData => new ListItem(new MenuItemCommand(menuData, _window.Handle)) { Title = menuData.ItemText, Subtitle = menuData.PathText }).ToArray();
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
internal sealed partial class AllWindowsPage : ListPage
{
private readonly List<WindowData> windows = new();
public AllWindowsPage()
{
Icon = new("\uf0b5"); // ChecklistMirrored
Name = "Open Windows";
ShowDetails = false;
}
public override IListItem[] GetItems()
{
PInvoke.EnumWindows(EnumWindowsCallback, IntPtr.Zero);
return windows
.Where(w => !string.IsNullOrEmpty(w.Title))
.Select(w => new ListItem(new WindowMenusPage(w))
{
Title = w.Title,
})
.ToArray();
}
private BOOL EnumWindowsCallback(HWND hWnd, LPARAM lParam)
{
// Only consider top-level visible windows with menus
if (/*PInvoke.IsWindowVisible(hWnd) &&*/ PInvoke.GetMenu(hWnd) != IntPtr.Zero)
{
try
{
windows.Add(new(hWnd));
}
catch (Exception)
{
}
return true;
}
return true; // Continue enumeration
}
}

View File

@@ -0,0 +1,57 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<RootNamespace>MenusExtension</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>false</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPalExtensions\$(RootNamespace)</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\extensionsdk\Microsoft.CmdPal.Extensions.Helpers\Microsoft.CmdPal.Extensions.Helpers.csproj" />
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,13 @@
GetMenu
GetMenuItemInfoW
SendMessageW
EnumWindows
IsWindowVisible
GetWindowTextW
GetWindowTextLength
GetMenuItemCount
GetSubMenu
PostMessage
SetActiveWindow
SetForegroundWindow

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap uap3 rescap">
<Identity
Name="MenusExtension"
Publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
Version="0.0.1.0" />
<Properties>
<DisplayName>Menus from the open windows</DisplayName>
<PublisherDisplayName>A Lone Developer</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="Menus from the open windows"
Description="Menus from the open windows"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="MenusExtension.exe" Arguments="-RegisterProcessAsComServer" DisplayName="ClementineExtensionApp">
<com:Class Id="9db16200-5579-4a62-9050-d67469316bba" DisplayName="Menus from the open windows" />
</com:ExeServer>
</com:ComServer>
</com:Extension>
<uap3:Extension Category="windows.appExtension">
<uap3:AppExtension Name="com.microsoft.windows.commandpalette"
Id="PG-SP-ID"
PublicFolder="Public"
DisplayName="Menus from the open windows"
Description="Menus from the open windows">
<uap3:Properties>
<CmdPalProvider>
<Activation>
<CreateInstance ClassId="9db16200-5579-4a62-9050-d67469316bba" />
</Activation>
<SupportedInterfaces>
<Commands/>
</SupportedInterfaces>
</CmdPalProvider>
</uap3:Properties>
</uap3:AppExtension>
</uap3:Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,25 @@
// 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;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Extensions.Helpers;
namespace Menus;
internal sealed partial class MenusPage : ListPage
{
public MenusPage()
{
Icon = new(string.Empty);
Name = "Menus from the open windows";
}
public override IListItem[] GetItems()
{
return [
new ListItem(new NoOpCommand()) { Title = "TODO: Implement your extension here" }
];
}
}

View File

@@ -0,0 +1,36 @@
// 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;
using System.Threading;
using Microsoft.CmdPal.Extensions;
namespace Menus;
public class Program
{
[MTAThread]
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "-RegisterProcessAsComServer")
{
using ExtensionServer server = new();
var extensionDisposedEvent = new ManualResetEvent(false);
var extensionInstance = new SampleExtension(extensionDisposedEvent);
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
// This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object.
// If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate.
server.RegisterExtension(() => extensionInstance);
// This will make the main thread wait until the event is signalled by the extension class.
// Since we have single instance of the extension object, we exit as sooon as it is disposed.
extensionDisposedEvent.WaitOne();
}
else
{
Console.WriteLine("Not being launched as a Extension... exiting.");
}
}
}

View File

@@ -0,0 +1,11 @@
{
"profiles": {
"Menus (Package)": {
"commandName": "MsixPackage",
"doNotLaunchApp": true
},
"Menus (Unpackaged)": {
"commandName": "Project"
}
}
}

View File

@@ -0,0 +1,39 @@
// 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;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CmdPal.Extensions;
namespace Menus;
[ComVisible(true)]
[Guid("9db16200-5579-4a62-9050-d67469316bba")]
[ComDefaultInterface(typeof(IExtension))]
public sealed partial class SampleExtension : IExtension
{
private readonly ManualResetEvent _extensionDisposedEvent;
public SampleExtension(ManualResetEvent extensionDisposedEvent)
{
this._extensionDisposedEvent = extensionDisposedEvent;
}
public object GetProvider(ProviderType providerType)
{
switch (providerType)
{
case ProviderType.Commands:
return new MenusActionsProvider();
default:
return null;
}
}
public void Dispose()
{
this._extensionDisposedEvent.Set();
}
}

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="HackerNewsExtension.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -90,14 +90,18 @@ public partial class BookmarksCommandProvider : CommandProvider
var listItem = new ListItem(action);
// Add actions for folder types
if (action is UrlAction urlAction && urlAction.Type == "folder")
if (action is UrlAction urlAction)
{
listItem.MoreCommands = [
new CommandContextItem(new OpenInTerminalAction(urlAction.Url))
];
if (urlAction.Type == "folder")
{
listItem.MoreCommands = [
new CommandContextItem(new OpenInTerminalAction(urlAction.Url))
];
}
listItem.Subtitle = urlAction.Url;
}
// listItem.Subtitle = "Bookmark";
if (action is not AddBookmarkPage)
{
listItem.Tags = [

View File

@@ -17,14 +17,14 @@ public partial class CalculatorTopLevelListItem : ListItem, IFallbackHandler
// In the case of the calculator, the ListItem itself is the fallback
// handler so that it can update its Title and Subtitle accordingly.
FallbackHandler = this;
Subtitle = "Type an equation";
SetDefaultTitle();
}
public void UpdateQuery(string query)
{
if (string.IsNullOrEmpty(query) || query == "=")
{
Title = "=";
SetDefaultTitle();
}
else if (query.StartsWith('='))
{
@@ -37,6 +37,12 @@ public partial class CalculatorTopLevelListItem : ListItem, IFallbackHandler
}
}
private void SetDefaultTitle()
{
Title = "=";
Subtitle = "Type an equation";
}
private string ParseQuery(string query)
{
var equation = query.Substring(1);

View File

@@ -443,36 +443,13 @@ internal sealed partial class PokedexExtensionPage : ListPage
public PokedexExtensionPage()
{
Icon = new(string.Empty);
Icon = new("https://e7.pngegg.com/pngimages/311/5/png-clipart-pokedex-pokemon-go-hoenn-pokemon-x-and-y-hoenn-pokedex-pokemon-ash-thumbnail.png");
Name = "Pokedex";
}
public override IListItem[] GetItems()
{
return _kanto.AsEnumerable().Concat(_johto.AsEnumerable()).Concat(_hoenn.AsEnumerable()).Select(p => GetPokemonListItem(p)).ToArray();
/*return [
new ListSection()
{
Title = "Kanto",
Items = _kanto
.AsEnumerable()
.Select(p => GetPokemonListItem(p)).ToArray(),
},
new ListSection()
{
Title = "Johto",
Items = _johto
.AsEnumerable()
.Select(p => GetPokemonListItem(p)).ToArray(),
},
new ListSection()
{
Title = "Hoenn",
Items = _hoenn
.AsEnumerable()
.Select(p => GetPokemonListItem(p)).ToArray(),
},
];*/
}
private static ListItem GetPokemonListItem(Pokemon pokemon)

View File

@@ -2,11 +2,6 @@
// 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.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Extensions.Helpers;
@@ -16,11 +11,11 @@ public partial class PokedexExtensionActionsProvider : CommandProvider
{
public PokedexExtensionActionsProvider()
{
DisplayName = "Pocket Monsters for the Command Palette Commands";
DisplayName = "Pocket Monsters for the Command Palette";
}
private readonly IListItem[] _commands = [
new ListItem(new PokedexExtensionPage()),
new ListItem(new PokedexExtensionPage()) { Subtitle = "Search your favorite pocket monsters" },
];
public override IListItem[] TopLevelCommands()

View File

@@ -2,7 +2,7 @@
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<RootNamespace>SSHKeychainExtension</RootNamespace>
<RootNamespace>SamplePagesExtension</RootNamespace>
<ApplicationManifest>app.manifest</ApplicationManifest>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>false</UseWinUI>

View File

@@ -5,6 +5,7 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Extensions.Helpers;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using WindowsCommandPalette.Models;
@@ -12,7 +13,7 @@ using WindowsCommandPalette.Views;
namespace WindowsCommandPalette;
public sealed class ListItemViewModel : INotifyPropertyChanged, IDisposable
public sealed class ListItemViewModel : INotifyPropertyChanged, IDisposable, IEquatable<ListItemViewModel>
{
private readonly DispatcherQueue _dispatcherQueue;
@@ -192,4 +193,45 @@ public sealed class ListItemViewModel : INotifyPropertyChanged, IDisposable
/* log something */
}
}
public bool Equals(ListItemViewModel? other)
{
return other == null ? false : other.ListItem.Unsafe == this.ListItem.Unsafe;
}
public override bool Equals(object? obj)
{
return Equals(obj as ListItemViewModel);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator ==(ListItemViewModel? l, ListItemViewModel? r)
{
return l is null ? r is null : l.Equals(r);
}
public static bool operator !=(ListItemViewModel? l, ListItemViewModel? r)
{
return !(l == r);
}
private struct ScoredListItemViewModel
{
public int Score;
public ListItemViewModel ViewModel;
}
public static IEnumerable<ListItemViewModel> FilterList(IEnumerable<ListItemViewModel> items, string query)
{
var scores = items
.Select(li => new ScoredListItemViewModel() { ViewModel = li, Score = ListHelpers.ScoreListItem(query, li.ListItem.Unsafe) })
.Where(score => score.Score > 0)
.OrderByDescending(score => score.Score);
return scores
.Select(score => score.ViewModel);
}
}

View File

@@ -5,8 +5,10 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.CmdPal.Extensions;
using Microsoft.CmdPal.Extensions.Helpers;
using Microsoft.UI.Dispatching;
using WindowsCommandPalette.Models;
using WindowsCommandPalette.Views;
@@ -19,8 +21,12 @@ public sealed partial class MainListPage : DynamicListPage
private readonly FilteredListSection _filteredSection;
private readonly ObservableCollection<MainListItem> topLevelItems = new();
private readonly DispatcherQueue _dispatcherQueue;
public MainListPage(MainViewModel viewModel)
{
this._dispatcherQueue = DispatcherQueue.GetForCurrentThread();
this._mainViewModel = viewModel;
// wacky: "All apps" is added to _mainViewModel.TopLevelCommands before
@@ -57,9 +63,6 @@ public sealed partial class MainListPage : DynamicListPage
private void UpdateQuery()
{
// Let our filtering wrapper know the newly typed search text:
_filteredSection.Query = SearchText;
// Update all the top-level commands which are fallback providers:
var fallbacks = topLevelItems
.Select(i => i?.FallbackHandler)
@@ -68,26 +71,42 @@ public sealed partial class MainListPage : DynamicListPage
foreach (var fb in fallbacks)
{
fb.UpdateQuery(SearchText);
try
{
fb.UpdateQuery(SearchText);
}
catch (COMException ex)
{
Debug.WriteLine("Failed to update fallback handler:");
Debug.WriteLine(ex);
}
}
// Let our filtering wrapper know the newly typed search text.
// Do this _after_ updating our fallback handlers.
_filteredSection.Query = SearchText;
var count = string.IsNullOrEmpty(SearchText) ? topLevelItems.Count : _filteredSection.Count;
RaiseItemsChanged(count);
}
public override IListItem[] GetItems()
{
if (string.IsNullOrEmpty(SearchText))
{
return topLevelItems.ToArray();
}
else
{
return _filteredSection.Items;
}
return string.IsNullOrEmpty(SearchText) ? topLevelItems
.Where(item => !string.IsNullOrEmpty(item.Title))
.ToArray()
: _filteredSection.Items;
}
private void TopLevelCommands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
this._dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
{
this.Handle_TopLevelCommands_CollectionChanged(sender, e);
});
}
private void Handle_TopLevelCommands_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
Debug.WriteLine("TopLevelCommands_CollectionChanged");
if (e.Action == NotifyCollectionChangedAction.Add && e.NewItems != null)

View File

@@ -107,6 +107,7 @@ public sealed partial class MainWindow : Window
_mainViewModel.QuitRequested += (s, e) =>
{
Activated -= MainWindow_Activated;
Close();
// Application.Current.Exit();
@@ -121,12 +122,12 @@ public sealed partial class MainWindow : Window
var success = Windows.Win32.PInvoke.RegisterHotKey(hwnd, 0, mod, vk);
hotKeyPrc = HotKeyPrc;
var hotKeyPrcPointer = Marshal.GetFunctionPointerForDelegate(hotKeyPrc);
origPrc = Marshal.GetDelegateForFunctionPointer<WNDPROC>((IntPtr)Windows.Win32.PInvoke.SetWindowLongPtr(hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
origPrc = Marshal.GetDelegateForFunctionPointer<WNDPROC>(Windows.Win32.PInvoke.SetWindowLongPtr(hwnd, WINDOW_LONG_PTR_INDEX.GWL_WNDPROC, hotKeyPrcPointer));
}
private void PositionCentered()
{
_appWindow.Resize(new SizeInt32 { Width = 860, Height = 560 });
_appWindow.Resize(new SizeInt32 { Width = 1000, Height = 680 });
DisplayArea displayArea = DisplayArea.GetFromWindowId(_appWindow.Id, DisplayAreaFallback.Nearest);
if (displayArea is not null)
{
@@ -150,7 +151,7 @@ public sealed partial class MainWindow : Window
var onLeft = false;
try
{
using RegistryKey? key = Registry.CurrentUser?.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced");
using var key = Registry.CurrentUser?.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced");
if (key != null)
{
var o = key.GetValue("TaskbarGlomLevel");
@@ -174,7 +175,7 @@ public sealed partial class MainWindow : Window
// react appropriately
}
Microsoft.UI.Windowing.DisplayArea displayArea = Microsoft.UI.Windowing.DisplayArea.GetFromWindowId(_appWindow.Id, Microsoft.UI.Windowing.DisplayAreaFallback.Nearest);
var displayArea = Microsoft.UI.Windowing.DisplayArea.GetFromWindowId(_appWindow.Id, Microsoft.UI.Windowing.DisplayAreaFallback.Nearest);
if (displayArea is not null)
{
var centeredPosition = _appWindow.Position;
@@ -233,14 +234,9 @@ public sealed partial class MainWindow : Window
private static string KeybindingToString(VirtualKey key, VirtualKeyModifiers modifiers)
{
var keyString = key.ToString();
if (keyString.Length == 1)
{
keyString = keyString.ToUpper(System.Globalization.CultureInfo.CurrentCulture);
}
else
{
keyString = Regex.Replace(keyString, "([a-z])([A-Z])", "$1+$2");
}
keyString = keyString.Length == 1
? keyString.ToUpper(System.Globalization.CultureInfo.CurrentCulture)
: Regex.Replace(keyString, "([a-z])([A-Z])", "$1+$2");
var modifierString = string.Empty;
if (modifiers.HasFlag(VirtualKeyModifiers.Control))
@@ -370,20 +366,16 @@ public sealed partial class MainWindow : Window
private DesktopAcrylicController GetAcrylicConfig()
{
if (((FrameworkElement)this.Content).ActualTheme == ElementTheme.Light)
{
return new DesktopAcrylicController()
return ((FrameworkElement)this.Content).ActualTheme == ElementTheme.Light
? new DesktopAcrylicController()
{
Kind = DesktopAcrylicKind.Thin,
TintColor = Windows.UI.Color.FromArgb(255, 243, 243, 243),
LuminosityOpacity = 0.90f,
TintOpacity = 0.0f,
FallbackColor = Windows.UI.Color.FromArgb(255, 238, 238, 238),
};
}
else
{
return new DesktopAcrylicController()
}
: new DesktopAcrylicController()
{
Kind = DesktopAcrylicKind.Thin,
TintColor = Windows.UI.Color.FromArgb(255, 32, 32, 32),
@@ -391,7 +383,6 @@ public sealed partial class MainWindow : Window
TintOpacity = 0.5f,
FallbackColor = Windows.UI.Color.FromArgb(255, 28, 28, 28),
};
}
}
private void MainWindow_ActualThemeChanged(FrameworkElement sender, object args)

View File

@@ -40,7 +40,8 @@
<PackageReference Include="AdaptiveCards.ObjectModel.WinUI3" GeneratePathProperty="true" />
<PackageReference Include="AdaptiveCards.Rendering.WinUI3" GeneratePathProperty="True" />
<PackageReference Include="AdaptiveCards.Templating" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<PackageReference Include="CommunityToolkit.WinUI.Animations" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
<!-- <PackageReference Include="CommunityToolkit.WinUI" /> -->
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Markdown" />

View File

@@ -21,7 +21,7 @@
<DataTemplate x:Key="CardTemplate" x:DataType="local:FormViewModel">
<Border
x:Name="DetailsContent"
Margin="0,0,0,0"
Margin="0, 4, 0, 4"
Padding="8"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
@@ -33,7 +33,7 @@
</ResourceDictionary>
</Page.Resources>
<Grid Padding="12" RowSpacing="16">
<Grid Padding="4, 4, 0, 0" RowSpacing="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
@@ -57,16 +57,12 @@
Text="{x:Bind ViewModel.Page.Name}" />
</StackPanel>
<ScrollViewer
x:Name="ScrollViewer"
Grid.Row="1">
<Grid x:Name="FormContent">
<ItemsRepeater
x:Name="FormsRepeater"
ItemTemplate="{StaticResource CardTemplate}"
ItemsSource="{x:Bind ViewModel.Forms, Mode=OneWay}"
Layout="{StaticResource VerticalStackLayout}" />
</Grid>
</ScrollViewer>
<ListView x:Name="FormItems"
Grid.Row="1"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
SelectionMode="None"
ItemTemplate="{StaticResource CardTemplate}"
ItemsSource="{x:Bind ViewModel.Forms, Mode=OneWay}"/>
</Grid>
</Page>

View File

@@ -22,7 +22,7 @@ public sealed partial class FormPage : Page
public FormPage()
{
this.InitializeComponent();
UISettings settings = new UISettings();
var settings = new UISettings();
// yep this is the way to check if you're in light theme or dark.
// yep it's this dumb
@@ -50,7 +50,7 @@ public sealed partial class FormPage : Page
{
DispatcherQueue.TryEnqueue(() =>
{
FormsRepeater.ItemsSource = ViewModel.Forms;
FormItems.ItemsSource = ViewModel.Forms;
Debug.WriteLine($"Rendering {this.ViewModel.Forms.Count} forms");
foreach (var form in this.ViewModel.Forms)
@@ -58,14 +58,14 @@ public sealed partial class FormPage : Page
AddCardElement(form);
}
FormContent.Focus(FocusState.Programmatic);
FormItems.Focus(FocusState.Programmatic);
});
});
}
private void Page_Loaded(object sender, RoutedEventArgs e)
{
FormContent.Focus(FocusState.Programmatic);
FormItems.Focus(FocusState.Programmatic);
}
private void BackButton_Tapped(object sender, TappedRoutedEventArgs e)

View File

@@ -3,22 +3,28 @@
x:Class="WindowsCommandPalette.Views.ListPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ani="using:CommunityToolkit.WinUI.Animations"
xmlns:converters="using:CommunityToolkit.WinUI.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:devpal="using:WindowsCommandPalette"
xmlns:local="using:WindowsCommandPalette.Views"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mtu="using:Microsoft.Terminal.UI"
xmlns:rsdk="using:Microsoft.CmdPal.Extensions"
xmlns:converters="using:CommunityToolkit.WinUI.UI.Converters"
Background="{x:Bind AccentColorBrush, Mode=OneWay}"
Loaded="Page_Loaded"
mc:Ignorable="d">
<Page.Resources>
<ResourceDictionary>
<converters:StringVisibilityConverter x:Key="StringNotEmptyToVisibilityConverter" EmptyValue="Collapsed" NotEmptyValue="Visible" />
<converters:BoolToVisibilityConverter x:Key="ReverseBoolToVisibilityConverter" TrueValue="Collapsed" FalseValue="Visible" />
<converters:StringVisibilityConverter
x:Key="StringNotEmptyToVisibilityConverter"
EmptyValue="Collapsed"
NotEmptyValue="Visible" />
<converters:BoolToVisibilityConverter
x:Key="ReverseBoolToVisibilityConverter"
FalseValue="Visible"
TrueValue="Collapsed" />
<StackLayout
x:Name="HorizontalStackLayout"
@@ -42,7 +48,7 @@
Visibility="{x:Bind HasIcon, Mode=OneWay}" />
<TextBlock
VerticalAlignment="Center"
FontSize="10"
FontSize="12"
Foreground="{x:Bind TextBrush, Mode=OneWay}"
Text="{x:Bind Text, Mode=OneWay}" />
</StackPanel>
@@ -52,13 +58,13 @@
<!-- Template for items in the main list view -->
<DataTemplate x:Key="ListItemTemplate" x:DataType="devpal:ListItemViewModel">
<ListViewItem
MinHeight="40"
MinHeight="56"
KeyDown="ListItem_KeyDown"
Tapped="ListItem_Tapped"
Visibility="{x:Bind Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="28" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
@@ -67,30 +73,30 @@
Width="20"
Height="20"
VerticalAlignment="Center">
<ContentControl
Width="24"
Height="24"
Content="{x:Bind IcoElement, Mode=OneWay}" />
<ContentControl Content="{x:Bind IcoElement, Mode=OneWay}" />
</Viewbox>
<StackPanel
Grid.Column="1"
VerticalAlignment="Center"
Orientation="Horizontal">
Orientation="Vertical"
Spacing="0">
<TextBlock
VerticalAlignment="Center"
FontSize="14"
FontWeight="Medium"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Title, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
TextTrimming="CharacterEllipsis"
TextWrapping="NoWrap" />
<TextBlock
Margin="4,0,0,0"
VerticalAlignment="Center"
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Subtitle, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
Visibility="{x:Bind Subtitle, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
<Run Text=" - " />
<Run Text="{x:Bind Subtitle, Mode=OneWay}" />
</TextBlock>
TextWrapping="NoWrap"
Visibility="{x:Bind Subtitle, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}" />
</StackPanel>
<ItemsRepeater ItemTemplate="{StaticResource TagTemplate}"
ItemsSource="{x:Bind Tags}"
@@ -146,8 +152,10 @@
<Grid.RowDefinitions>
<RowDefinition Height="56" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" MinHeight="36" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Row 0: Back button and search box -->
@@ -220,12 +228,45 @@
PlaceholderText="Type here to search..."
Style="{StaticResource SearchTextBoxStyle}"
TextChanged="FilterBox_TextChanged" />
</Grid>
<Grid
<Rectangle
Grid.Row="1"
Height="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<StackPanel
Grid.Row="2"
Padding="24,12,0,0"
Orientation="Horizontal"
Visibility="Collapsed"
Spacing="16">
<TextBlock
FontSize="12"
FontWeight="SemiBold"
Text="All" />
<TextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Apps" />
<TextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Extensions" />
<Button
Height="24"
Click="ToggleButton_Click"
Content="Tralalal"
Opacity="0" />
</StackPanel>
<Grid
Grid.Row="3"
Background="{ThemeResource LayerOnAcrylicFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="0,1,0,1">
BorderThickness="0,1,0,1"
CornerRadius="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition x:Name="DetailsColumn" Width="2*" />
@@ -234,29 +275,25 @@
<ListView
x:Name="ItemsList"
Grid.Column="0"
Margin="4,0,0,0"
Margin="0,0,0,0"
IsItemClickEnabled="True"
ItemTemplate="{StaticResource ListItemTemplate}"
ItemsSource="{x:Bind ViewModel.FilteredItems, Mode=OneWay}"
SelectionChanged="ItemsList_SelectionChanged"
Style="{StaticResource NoAnimationsPlease}">
</ListView>
Style="{StaticResource NoAnimationsPlease}" />
<Border
x:Name="DetailsContent"
Grid.Column="1"
Margin="12"
Padding="8"
Background="{ThemeResource LayerFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="{ThemeResource OverlayCornerRadius}"
BorderThickness="1,0,0,0"
Visibility="Collapsed" />
</Grid>
<!-- Footer -->
<Grid Grid.Row="3" Padding="8,0,8,0">
<Grid Grid.Row="4" Padding="8,0,8,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
@@ -305,6 +342,46 @@
</Flyout>
</SplitButton.Flyout>
</SplitButton>
</Grid>
<Grid
x:Name="InstallationDialog"
Grid.Row="5"
Visibility="Collapsed">
<ani:Implicit.ShowAnimations>
<ani:OpacityAnimation
From="0"
To="1"
Duration="0:0:0.4" />
<ani:TranslationAnimation
From="0,20,0"
To="0"
Duration="0:0:0.4" />
</ani:Implicit.ShowAnimations>
<ani:Implicit.HideAnimations>
<ani:OpacityAnimation
From="1"
To="0"
Duration="0:0:0.2" />
<ani:TranslationAnimation
From="0"
To="0,20,0"
Duration="0:0:0.2" />
</ani:Implicit.HideAnimations>
<Grid Background="{ThemeResource AccentAcrylicBackgroundFillColorBaseBrush}" Opacity="0.2" />
<StackPanel
Margin="22,12,12,12"
Orientation="Horizontal"
Spacing="18">
<ProgressRing
Width="20"
Height="20"
IsActive="True" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="Installing SpongeBot extension.." />
</StackPanel>
</Grid>
</Grid>
</Page>

View File

@@ -4,7 +4,6 @@
using System.ComponentModel;
using System.Diagnostics;
using Microsoft.CmdPal.Extensions.Helpers;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
@@ -34,6 +33,7 @@ public sealed partial class ListPage : Microsoft.UI.Xaml.Controls.Page, INotifyP
get => _selectedItem;
set
{
Debug.WriteLine($" Selected: {SelectedItem?.Title}");
_selectedItem = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MoreCommandsAvailable)));
@@ -128,6 +128,7 @@ public sealed partial class ListPage : Microsoft.UI.Xaml.Controls.Page, INotifyP
ViewModel.InitialRender().ContinueWith((t) =>
{
DispatcherQueue.TryEnqueue(() => { UpdateFilter(FilterBox.Text); });
ViewModel.FilteredItems.CollectionChanged += FilteredItems_CollectionChanged;
});
}
else
@@ -138,6 +139,37 @@ public sealed partial class ListPage : Microsoft.UI.Xaml.Controls.Page, INotifyP
this.ItemsList.SelectedIndex = 0;
}
private void FilteredItems_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
// *
// Debug.WriteLine($"FilteredItems_CollectionChanged");
// Try to maintain the selected item, if we can.
if (ItemsList.SelectedItem != null &&
ItemsList.SelectedItem is ListItemViewModel li)
{
var xamlListItem = ItemsList.ContainerFromItem(li);
if (xamlListItem != null)
{
var index = ItemsList.IndexFromContainer(xamlListItem);
if (index >= 0)
{
// Debug.WriteLine("Found original selected item");
this.ItemsList.SelectedIndex = index;
return;
}
}
else
{
// Debug.WriteLine($"Didn't find {li.Title} in new list");
}
}
// */
// Debug.WriteLine($"Selecting index 0");
this.ItemsList.SelectedIndex = 0;
}
private void DoAction(ActionViewModel actionViewModel)
{
ViewModel?.DoAction(actionViewModel);
@@ -200,6 +232,7 @@ public sealed partial class ListPage : Microsoft.UI.Xaml.Controls.Page, INotifyP
private void ItemsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Debug.WriteLine($" ItemsList_SelectionChanged");
if (sender is not ListView lv)
{
return;
@@ -301,6 +334,7 @@ public sealed partial class ListPage : Microsoft.UI.Xaml.Controls.Page, INotifyP
{
if (FilterBox.Text.Length > 0)
{
Debug.WriteLine("Clear seearch text");
FilterBox.Text = string.Empty;
}
else
@@ -340,31 +374,17 @@ public sealed partial class ListPage : Microsoft.UI.Xaml.Controls.Page, INotifyP
return;
}
Debug.WriteLine($"UpdateFilter({text})");
// Go ask the ViewModel for the items to display. This might:
// * do an async request to the extension (fixme after GH #77)
// * just return already filtered items.
// * return a subset of items matching the filter text
var items = ViewModel.GetFilteredItems(text);
Debug.WriteLine($" UpdateFilter after GetFilteredItems({text}) --> {items.Count()} ; {ViewModel.FilteredItems.Count}");
// Here, actually populate ViewModel.FilteredItems
// WARNING: if you do this off the UI thread, it sure won't work right.
ListHelpers.InPlaceUpdateList(ViewModel.FilteredItems, new(items.ToList()));
Debug.WriteLine($" UpdateFilter after InPlaceUpdateList --> {ViewModel.FilteredItems.Count}");
// set the selected index to the first item in the list
if (ItemsList.Items.Count > 0)
{
ItemsList.SelectedIndex = 0;
ItemsList.ScrollIntoView(ItemsList.SelectedItem);
}
// Debug.WriteLine($"UpdateFilter({text})");
ViewModel.UpdateSearchText(text);
}
private void BackButton_Tapped(object sender, TappedRoutedEventArgs e)
{
ViewModel?.GoBack();
}
private void ToggleButton_Click(object sender, RoutedEventArgs e)
{
InstallationDialog.Visibility = InstallationDialog.Visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
}
}

View File

@@ -91,28 +91,27 @@ public sealed class ListPageViewModel : PageViewModel
// still on main thread
// TODO! For dynamic lists, we're clearing out the whole list of items
// we already have, then rebuilding it. We shouldn't do that. We should
// still use the results from GetItems and put them into the code in
// UpdateFilter to intelligently add/remove as needed.
// TODODO! are we still? ^^
// This creates an entirely new list of ListItemViewModels, and we're
// really hoping that the equality check in `InPlaceUpdateList`
// properly uses ListItemViewModel.Equals to compare if the objects
// are literally the same.
Collection<ListItemViewModel> newItems = new(items.Select(i => new ListItemViewModel(i)).ToList());
Debug.WriteLine($" Found {newItems.Count} items");
// Debug.WriteLine($" Found {newItems.Count} items");
// THIS populates FilteredItems. If you do this off the UI thread, guess what -
// the list view won't update. So WATCH OUT
ListHelpers.InPlaceUpdateList(FilteredItems, newItems);
ListHelpers.InPlaceUpdateList(_items, newItems);
Debug.WriteLine($"Done with UpdateListItems, found {FilteredItems.Count} / {_items.Count}");
// Debug.WriteLine($"Done with UpdateListItems, found {FilteredItems.Count} / {_items.Count}");
}
internal IEnumerable<ListItemViewModel> GetFilteredItems(string query)
public void UpdateSearchText(string query)
{
// This method does NOT change any lists. It doesn't modify _items or FilteredItems...
if (query == _query)
{
return FilteredItems;
return;
}
_query = query;
@@ -120,25 +119,13 @@ public sealed class ListPageViewModel : PageViewModel
{
// Tell the dynamic page the new search text. If they need to update, they will.
IsDynamicPage.SearchText = _query;
return FilteredItems;
}
else
{
// Static lists don't need to re-fetch the items
if (string.IsNullOrEmpty(query))
{
return _items;
}
// TODO! Probably bad that this turns list view models into listitems back to NEW view models
// TODO! make this safer
// TODODO! ^ still relevant?
var newFilter = ListHelpers
.FilterList(_items.Select(vm => vm.ListItem.Unsafe), query)
.Select(li => new ListItemViewModel(li));
return newFilter;
var filtered = ListItemViewModel
.FilterList(_items, query);
Collection<ListItemViewModel> newItems = new(filtered.ToList());
ListHelpers.InPlaceUpdateList(FilteredItems, newItems);
}
}

View File

@@ -9,14 +9,13 @@ namespace Microsoft.CmdPal.Extensions.Helpers;
public class ListHelpers
{
// Generate a score for a list item.
// TODO! This has side effects! This calls UpdateQuery on fallback handlers and that's async
public static int ScoreListItem(string query, IListItem listItem)
{
var isFallback = false;
if (listItem.FallbackHandler != null)
{
isFallback = true;
listItem.FallbackHandler.UpdateQuery(query);
if (string.IsNullOrWhiteSpace(listItem.Title))
{
return 0;
@@ -58,7 +57,10 @@ public class ListHelpers
{
for (var j = i; j < original.Count; j++)
{
if (original[j] == newContents[i])
var og_2 = original[j];
var newItem_2 = newContents[i];
var areEqual_2 = og_2.Equals(newItem_2);
if (areEqual_2)
{
for (var k = i; k < j; k++)
{
@@ -70,14 +72,14 @@ public class ListHelpers
}
}
var og = original[i];
var newItem = newContents[i];
var areEqual = og.Equals(newItem);
// Is this new item already in the list?
if (original[i] == newContents[i])
if (areEqual)
{
// It is already in the list
if (original[i] is Collection<T> og && newContents[i] is Collection<T> newG)
{
InPlaceUpdateList(og, newG);
}
}
else
{